From 647fdd309a6f4e386a246f522817f0ba3f616b6f Mon Sep 17 00:00:00 2001 From: Mainak Jas Date: Fri, 6 Oct 2023 10:20:42 -0400 Subject: [PATCH 001/405] [MRG] update codeowners (#12089) --- .github/CODEOWNERS | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 8d17d366a06..0d333361914 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -33,7 +33,7 @@ *lcmv*.py @britta-wstnr # Channels -/mne/channels @agramfort @mscheltienne @dengemann +/mne/channels @agramfort @mscheltienne @dengemann @jasmainak # Core sensor-space classes /mne/epochs.py @drammock @agramfort @mscheltienne @dengemann @@ -45,13 +45,14 @@ # Decoding /mne/decoding/csp.py @cbrnr @agramfort @dengemann +/mne/decoding/*.py @jasmainak # fNIRS /mne/preprocessing/nirs @rob-luke *fnirs*.py @rob-luke # forward -/mne/forward/ @agramfort +/mne/forward/ @agramfort @jasmainak *forward*.py @agramfort # Intracranial @@ -69,6 +70,8 @@ /mne/io/nirx @rob-luke /mne/io/snirf @rob-luke /mne/export @sappelhoff @cbrnr +/mne/io/eeglab.py @jasmainak +/mne/io/eeglab/tests/test_eeglab.py @jasmainak # Minimum Norm /mne/minimum_norm @agramfort @@ -81,7 +84,7 @@ /mne/preprocessing/e*g.py @mscheltienne # Report -/mne/report @hoechenberger @dengemann +/mne/report @hoechenberger @dengemann @jasmainak # Simulation /mne/simulation/ @agramfort @@ -102,6 +105,9 @@ /tutorials/visualization @larsoner @wmvanvliet @dengemann /examples/visualization @larsoner @dengemann +# Datasets +/mne/datasets/brainstorm @jasmainak + ######################### # Project-level / other # ######################### From 37ae7e37354fddc5eced35aa96973e3f944c9780 Mon Sep 17 00:00:00 2001 From: Hamza Abdelhedi Date: Fri, 6 Oct 2023 10:43:57 -0400 Subject: [PATCH 002/405] Add raw stc (#12001) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Marijn van Vliet Co-authored-by: Daniel McCloy --- doc/changes/devel.rst | 1 + mne/source_estimate.py | 26 +++++++++-------- mne/tests/test_source_estimate.py | 46 +++++++++++++++++++++++++------ mne/utils/docs.py | 3 ++ 4 files changed, 57 insertions(+), 19 deletions(-) diff --git a/doc/changes/devel.rst b/doc/changes/devel.rst index eeb06c3d7be..81e16e8658e 100644 --- a/doc/changes/devel.rst +++ b/doc/changes/devel.rst @@ -29,6 +29,7 @@ Enhancements - Added public :func:`mne.io.write_info` to complement :func:`mne.io.read_info` (:gh:`11918` by `Eric Larson`_) - Added option ``remove_dc`` to to :meth:`Raw.compute_psd() `, :meth:`Epochs.compute_psd() `, and :meth:`Evoked.compute_psd() `, to allow skipping DC removal when computing Welch or multitaper spectra (:gh:`11769` by `Nikolai Chapochnikov`_) - Add the possibility to provide a float between 0 and 1 as ``n_grad``, ``n_mag`` and ``n_eeg`` in `~mne.compute_proj_raw`, `~mne.compute_proj_epochs` and `~mne.compute_proj_evoked` to select the number of vectors based on the cumulative explained variance (:gh:`11919` by `Mathieu Scheltienne`_) +- Add extracting all time courses in a label using :func:`mne.extract_label_time_course` without applying an aggregation function (like ``mean``) (:gh:`12001` by `Hamza Abdelhedi`_) - Added support for Artinis fNIRS data files to :func:`mne.io.read_raw_snirf` (:gh:`11926` by `Robert Luke`_) - Add helpful error messages when using methods on empty :class:`mne.Epochs`-objects (:gh:`11306` by `Martin Schulz`_) - Add support for passing a :class:`python:dict` as ``sensor_color`` to specify per-channel-type colors in :func:`mne.viz.plot_alignment` (:gh:`12067` by `Eric Larson`_) diff --git a/mne/source_estimate.py b/mne/source_estimate.py index 3f0674210ca..211d109222c 100644 --- a/mne/source_estimate.py +++ b/mne/source_estimate.py @@ -3240,6 +3240,7 @@ def _pca_flip(flip, data): "mean_flip": lambda flip, data: np.mean(flip * data, axis=0), "max": lambda flip, data: np.max(np.abs(data), axis=0), "pca_flip": _pca_flip, + None: lambda flip, data: data, # Return Identity: Preserves all vertices. } @@ -3494,7 +3495,7 @@ def _volume_labels(src, labels, mri_resolution): def _get_default_label_modes(): - return sorted(_label_funcs.keys()) + ["auto"] + return sorted(_label_funcs.keys(), key=lambda x: (x is None, x)) + ["auto"] def _get_allowed_label_modes(stc): @@ -3572,7 +3573,12 @@ def _gen_extract_label_time_course( ) # do the extraction - label_tc = np.zeros((n_labels,) + stc.data.shape[1:], dtype=stc.data.dtype) + if mode is None: + # prepopulate an empty list for easy array-like index-based assignment + label_tc = [None] * max(len(label_vertidx), len(src_flip)) + else: + # For other modes, initialize the label_tc array + label_tc = np.zeros((n_labels,) + stc.data.shape[1:], dtype=stc.data.dtype) for i, (vertidx, flip) in enumerate(zip(label_vertidx, src_flip)): if vertidx is not None: if isinstance(vertidx, sparse.csr_matrix): @@ -3585,15 +3591,13 @@ def _gen_extract_label_time_course( this_data = stc.data[vertidx] label_tc[i] = func(flip, this_data) - # extract label time series for the vol src space (only mean supported) - offset = nvert[:-n_mean].sum() # effectively :2 or :0 - for i, nv in enumerate(nvert[2:]): - if nv != 0: - v2 = offset + nv - label_tc[n_mode + i] = np.mean(stc.data[offset:v2], axis=0) - offset = v2 - - # this is a generator! + if mode is not None: + offset = nvert[:-n_mean].sum() # effectively :2 or :0 + for i, nv in enumerate(nvert[2:]): + if nv != 0: + v2 = offset + nv + label_tc[n_mode + i] = np.mean(stc.data[offset:v2], axis=0) + offset = v2 yield label_tc diff --git a/mne/tests/test_source_estimate.py b/mne/tests/test_source_estimate.py index 5e0373f718e..9b78113127c 100644 --- a/mne/tests/test_source_estimate.py +++ b/mne/tests/test_source_estimate.py @@ -678,12 +678,24 @@ def test_extract_label_time_course(kind, vector): label_tcs = dict(mean=np.arange(n_labels)[:, None] * np.ones((n_labels, n_times))) label_tcs["max"] = label_tcs["mean"] + label_tcs[None] = label_tcs["mean"] # compute the mean with sign flip label_tcs["mean_flip"] = np.zeros_like(label_tcs["mean"]) for i, label in enumerate(labels): label_tcs["mean_flip"][i] = i * np.mean(label_sign_flip(label, src[:2])) + # compute pca_flip + label_flip = [] + for i, label in enumerate(labels): + this_flip = i * label_sign_flip(label, src[:2]) + label_flip.append(this_flip) + # compute pca_flip + label_tcs["pca_flip"] = np.zeros_like(label_tcs["mean"]) + for i, (label, flip) in enumerate(zip(labels, label_flip)): + sign = np.sign(np.dot(np.full((flip.shape[0]), i), flip)) + label_tcs["pca_flip"][i] = sign * label_tcs["mean"][i] + # generate some stc's with known data stcs = list() pad = (((0, 0), (2, 0), (0, 0)), "constant") @@ -734,7 +746,7 @@ def test_extract_label_time_course(kind, vector): assert_array_equal(arr[1:], vol_means_t) # test the different modes - modes = ["mean", "mean_flip", "pca_flip", "max", "auto"] + modes = ["mean", "mean_flip", "pca_flip", "max", "auto", None] for mode in modes: if vector and mode not in ("mean", "max", "auto"): @@ -748,18 +760,36 @@ def test_extract_label_time_course(kind, vector): ] assert len(label_tc) == n_stcs assert len(label_tc_method) == n_stcs - for tc1, tc2 in zip(label_tc, label_tc_method): - assert tc1.shape == (n_labels + len(vol_means),) + end_shape - assert tc2.shape == (n_labels + len(vol_means),) + end_shape - assert_allclose(tc1, tc2, rtol=1e-8, atol=1e-16) + for j, (tc1, tc2) in enumerate(zip(label_tc, label_tc_method)): + if mode is None: + assert all(arr.shape[1] == tc1[0].shape[1] for arr in tc1) + assert all(arr.shape[1] == tc2[0].shape[1] for arr in tc2) + assert (len(tc1), tc1[0].shape[1]) == (n_labels,) + end_shape + assert (len(tc2), tc2[0].shape[1]) == (n_labels,) + end_shape + for arr1, arr2 in zip(tc1, tc2): # list of arrays + assert_allclose(arr1, arr2, rtol=1e-8, atol=1e-16) + else: + assert tc1.shape == (n_labels + len(vol_means),) + end_shape + assert tc2.shape == (n_labels + len(vol_means),) + end_shape + assert_allclose(tc1, tc2, rtol=1e-8, atol=1e-16) if mode == "auto": use_mode = "mean" if vector else "mean_flip" else: use_mode = mode - # XXX we don't check pca_flip, probably should someday... - if use_mode in ("mean", "max", "mean_flip"): + if mode == "pca_flip": + for arr1, arr2 in zip(tc1, label_tcs[use_mode]): + assert_array_almost_equal(arr1, arr2) + elif use_mode is None: + for arr1, arr2 in zip( + tc1[:n_labels], label_tcs[use_mode] + ): # list of arrays + assert_allclose( + arr1, np.tile(arr2, (arr1.shape[0], 1)), rtol=1e-8, atol=1e-16 + ) + elif use_mode in ("mean", "max", "mean_flip"): assert_array_almost_equal(tc1[:n_labels], label_tcs[use_mode]) - assert_array_almost_equal(tc1[n_labels:], vol_means_t) + if mode is not None: + assert_array_almost_equal(tc1[n_labels:], vol_means_t) # test label with very few vertices (check SVD conditionals) label = Label(vertices=src[0]["vertno"][:2], hemi="lh") diff --git a/mne/utils/docs.py b/mne/utils/docs.py index f1e8369c5a5..ef311f30a71 100644 --- a/mne/utils/docs.py +++ b/mne/utils/docs.py @@ -1203,6 +1203,9 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): - ``'auto'`` (default) Uses ``'mean_flip'`` when a standard source estimate is applied, and ``'mean'`` when a vector source estimate is supplied. +- ``None`` + No aggregation is performed, and an array of shape ``(n_vertices, n_times)`` is + returned. .. versionadded:: 0.21 Support for ``'auto'``, vector, and volume source estimates. From fdaeb86206d334242bcbcfd09460c970b33e4dcd Mon Sep 17 00:00:00 2001 From: Mathieu Scheltienne Date: Sat, 7 Oct 2023 05:21:33 +0200 Subject: [PATCH 003/405] Use constrained layout in matplotlib visualization (#12050) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Eric Larson --- README.rst | 2 +- doc/changes/devel.rst | 1 + doc/conf.py | 1 - examples/decoding/decoding_rsa_sgskip.py | 6 +- examples/decoding/decoding_spoc_CMC.py | 3 +- ...decoding_time_generalization_conditions.py | 2 +- examples/decoding/decoding_xdawn_eeg.py | 9 +- examples/decoding/receptive_field_mtrf.py | 16 +- examples/inverse/label_source_activations.py | 6 +- .../inverse/mixed_source_space_inverse.py | 3 +- examples/inverse/source_space_snr.py | 3 +- examples/preprocessing/eeg_bridging.py | 7 +- examples/preprocessing/eeg_csd.py | 3 +- .../preprocessing/eog_artifact_histogram.py | 3 +- examples/preprocessing/eog_regression.py | 7 +- examples/preprocessing/shift_evoked.py | 3 - examples/simulation/plot_stc_metrics.py | 6 +- .../source_label_time_frequency.py | 25 +-- .../source_power_spectrum_opm.py | 1 - .../time_frequency_simulated.py | 17 +- examples/visualization/3d_to_2d.py | 3 +- examples/visualization/evoked_topomap.py | 4 +- mne/conftest.py | 38 +++-- mne/preprocessing/eyetracking/calibration.py | 2 +- mne/preprocessing/ica.py | 1 - mne/report/report.py | 30 +--- mne/time_frequency/spectrum.py | 1 - mne/time_frequency/tfr.py | 38 +++-- mne/viz/_3d.py | 26 +-- mne/viz/__init__.pyi | 2 - mne/viz/_dipole.py | 4 +- mne/viz/_figure.py | 12 +- mne/viz/_mpl_figure.py | 29 +++- mne/viz/_proj.py | 2 +- mne/viz/backends/_abstract.py | 15 +- mne/viz/backends/tests/test_utils.py | 3 + mne/viz/circle.py | 2 +- mne/viz/epochs.py | 14 +- mne/viz/evoked.py | 75 ++++----- mne/viz/ica.py | 24 +-- mne/viz/misc.py | 51 +++--- mne/viz/tests/test_epochs.py | 9 +- mne/viz/tests/test_evoked.py | 4 +- mne/viz/tests/test_topomap.py | 6 +- mne/viz/topo.py | 6 +- mne/viz/topomap.py | 106 +++++------- mne/viz/utils.py | 159 ++---------------- requirements.txt | 2 +- requirements_base.txt | 2 +- tools/github_actions_env_vars.sh | 2 +- .../epochs/60_make_fixed_length_epochs.py | 7 +- .../forward/50_background_freesurfer_mne.py | 3 +- tutorials/intro/70_report.py | 2 +- tutorials/inverse/20_dipole_fit.py | 2 +- tutorials/inverse/60_visualize_stc.py | 3 +- .../inverse/80_brainstorm_phantom_elekta.py | 2 +- tutorials/machine-learning/30_strf.py | 40 ++--- .../preprocessing/25_background_filtering.py | 10 +- .../preprocessing/30_filtering_resampling.py | 3 - .../50_artifact_correction_ssp.py | 5 +- .../preprocessing/60_maxwell_filtering_sss.py | 5 +- .../preprocessing/70_fnirs_processing.py | 15 +- tutorials/preprocessing/80_opm_processing.py | 12 +- tutorials/raw/20_event_arrays.py | 1 - tutorials/simulation/80_dics.py | 3 +- .../stats-sensor-space/10_background_stats.py | 17 +- .../40_cluster_1samp_time_freq.py | 17 +- .../50_cluster_between_time_freq.py | 3 +- .../70_cluster_rmANOVA_time_freq.py | 10 +- .../75_cluster_ftest_spatiotemporal.py | 13 +- .../time-freq/20_sensors_time_frequency.py | 2 +- 71 files changed, 351 insertions(+), 620 deletions(-) diff --git a/README.rst b/README.rst index c601e318b51..a3d35deb76a 100644 --- a/README.rst +++ b/README.rst @@ -96,7 +96,7 @@ The minimum required dependencies to run MNE-Python are: - Python >= 3.8 - NumPy >= 1.21.2 - SciPy >= 1.7.1 -- Matplotlib >= 3.4.3 +- Matplotlib >= 3.5.0 - pooch >= 1.5 - tqdm - Jinja2 diff --git a/doc/changes/devel.rst b/doc/changes/devel.rst index 81e16e8658e..b46c2a6fc60 100644 --- a/doc/changes/devel.rst +++ b/doc/changes/devel.rst @@ -37,6 +37,7 @@ Enhancements - Add :class:`~mne.time_frequency.EpochsSpectrumArray` and :class:`~mne.time_frequency.SpectrumArray` to support creating power spectra from :class:`NumPy array ` data (:gh:`11803` by `Alex Rockhill`_) - Add support for writing forward solutions to HDF5 and convenience function :meth:`mne.Forward.save` (:gh:`12036` by `Eric Larson`_) - Refactored internals of :func:`mne.read_annotations` (:gh:`11964` by `Paul Roujansky`_) +- By default MNE-Python creates matplotlib figures with ``layout='constrained'`` rather than the default ``layout='tight'`` (:gh:`12050` by `Mathieu Scheltienne`_ and `Eric Larson`_) - Enhance :func:`~mne.viz.plot_evoked_field` with a GUI that has controls for time, colormap, and contour lines (:gh:`11942` by `Marijn van Vliet`_) - Add :class:`mne.viz.ui_events.UIEvent` linking for interactive colorbars, allowing users to link figures and change the colormap and limits interactively. This supports :func:`~mne.viz.plot_evoked_topomap`, :func:`~mne.viz.plot_ica_components`, :func:`~mne.viz.plot_tfr_topomap`, :func:`~mne.viz.plot_projs_topomap`, :meth:`~mne.Evoked.plot_image`, and :meth:`~mne.Epochs.plot_image` (:gh:`12057` by `Santeri Ruuskanen`_) diff --git a/doc/conf.py b/doc/conf.py index d8c9f52ad6e..b8086500640 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -1291,7 +1291,6 @@ def reset_warnings(gallery_conf, fname): warnings.filterwarnings("default", module="sphinx") # allow these warnings, but don't show them for key in ( - "The module matplotlib.tight_layout is deprecated", # nilearn "invalid version and will not be supported", # pyxdf "distutils Version classes are deprecated", # seaborn and neo "`np.object` is a deprecated alias for the builtin `object`", # pyxdf diff --git a/examples/decoding/decoding_rsa_sgskip.py b/examples/decoding/decoding_rsa_sgskip.py index 7cc6dbfbb01..3cc8467deb3 100644 --- a/examples/decoding/decoding_rsa_sgskip.py +++ b/examples/decoding/decoding_rsa_sgskip.py @@ -150,7 +150,7 @@ ############################################################################## # Plot labels = [""] * 5 + ["face"] + [""] * 11 + ["bodypart"] + [""] * 6 -fig, ax = plt.subplots(1) +fig, ax = plt.subplots(1, layout="constrained") im = ax.matshow(confusion, cmap="RdBu_r", clim=[0.3, 0.7]) ax.set_yticks(range(len(classes))) ax.set_yticklabels(labels) @@ -159,14 +159,13 @@ ax.axhline(11.5, color="k") ax.axvline(11.5, color="k") plt.colorbar(im) -plt.tight_layout() plt.show() ############################################################################## # Confusion matrix related to mental representations have been historically # summarized with dimensionality reduction using multi-dimensional scaling [1]. # See how the face samples cluster together. -fig, ax = plt.subplots(1) +fig, ax = plt.subplots(1, layout="constrained") mds = MDS(2, random_state=0, dissimilarity="precomputed") chance = 0.5 summary = mds.fit_transform(chance - confusion) @@ -186,7 +185,6 @@ ) ax.axis("off") ax.legend(loc="lower right", scatterpoints=1, ncol=2) -plt.tight_layout() plt.show() ############################################################################## diff --git a/examples/decoding/decoding_spoc_CMC.py b/examples/decoding/decoding_spoc_CMC.py index d73e9af9bbc..4e689d338d5 100644 --- a/examples/decoding/decoding_spoc_CMC.py +++ b/examples/decoding/decoding_spoc_CMC.py @@ -68,7 +68,7 @@ y_preds = cross_val_predict(clf, X, y, cv=cv) # Plot the True EMG power and the EMG power predicted from MEG data -fig, ax = plt.subplots(1, 1, figsize=[10, 4]) +fig, ax = plt.subplots(1, 1, figsize=[10, 4], layout="constrained") times = raw.times[meg_epochs.events[:, 0] - raw.first_samp] ax.plot(times, y_preds, color="b", label="Predicted EMG") ax.plot(times, y, color="r", label="True EMG") @@ -76,7 +76,6 @@ ax.set_ylabel("EMG Power") ax.set_title("SPoC MEG Predictions") plt.legend() -mne.viz.tight_layout() plt.show() ############################################################################## diff --git a/examples/decoding/decoding_time_generalization_conditions.py b/examples/decoding/decoding_time_generalization_conditions.py index 08ca0d9c0c3..a018ebbe75b 100644 --- a/examples/decoding/decoding_time_generalization_conditions.py +++ b/examples/decoding/decoding_time_generalization_conditions.py @@ -88,7 +88,7 @@ # %% # Plot -fig, ax = plt.subplots(constrained_layout=True) +fig, ax = plt.subplots(layout="constrained") im = ax.matshow( scores, vmin=0, diff --git a/examples/decoding/decoding_xdawn_eeg.py b/examples/decoding/decoding_xdawn_eeg.py index 3bdff716228..e7fac8c52e6 100644 --- a/examples/decoding/decoding_xdawn_eeg.py +++ b/examples/decoding/decoding_xdawn_eeg.py @@ -99,14 +99,13 @@ cm_normalized = cm.astype(float) / cm.sum(axis=1)[:, np.newaxis] # Plot confusion matrix -fig, ax = plt.subplots(1) +fig, ax = plt.subplots(1, layout="constrained") im = ax.imshow(cm_normalized, interpolation="nearest", cmap=plt.cm.Blues) ax.set(title="Normalized Confusion matrix") fig.colorbar(im) tick_marks = np.arange(len(target_names)) plt.xticks(tick_marks, target_names, rotation=45) plt.yticks(tick_marks, target_names) -fig.tight_layout() ax.set(ylabel="True label", xlabel="Predicted label") # %% @@ -114,7 +113,10 @@ # cross-validation fold) can be used for visualization. fig, axes = plt.subplots( - nrows=len(event_id), ncols=n_filter, figsize=(n_filter, len(event_id) * 2) + nrows=len(event_id), + ncols=n_filter, + figsize=(n_filter, len(event_id) * 2), + layout="constrained", ) fitted_xdawn = clf.steps[0][1] info = create_info(epochs.ch_names, 1, epochs.get_channel_types()) @@ -131,7 +133,6 @@ show=False, ) axes[ii, 0].set(ylabel=cur_class) -fig.tight_layout(h_pad=1.0, w_pad=1.0, pad=0.1) # %% # References diff --git a/examples/decoding/receptive_field_mtrf.py b/examples/decoding/receptive_field_mtrf.py index 0d24d5ebfa1..e927cd3cf25 100644 --- a/examples/decoding/receptive_field_mtrf.py +++ b/examples/decoding/receptive_field_mtrf.py @@ -67,12 +67,11 @@ n_channels = len(raw.ch_names) # Plot a sample of brain and stimulus activity -fig, ax = plt.subplots() +fig, ax = plt.subplots(layout="constrained") lns = ax.plot(scale(raw[:, :800][0].T), color="k", alpha=0.1) ln1 = ax.plot(scale(speech[0, :800]), color="r", lw=2) ax.legend([lns[0], ln1[0]], ["EEG", "Speech Envelope"], frameon=False) ax.set(title="Sample activity", xlabel="Time (s)") -mne.viz.tight_layout() # %% # Create and fit a receptive field model @@ -117,12 +116,11 @@ mean_scores = scores.mean(axis=0) # Plot mean prediction scores across all channels -fig, ax = plt.subplots() +fig, ax = plt.subplots(layout="constrained") ix_chs = np.arange(n_channels) ax.plot(ix_chs, mean_scores) ax.axhline(0, ls="--", color="r") ax.set(title="Mean prediction score", xlabel="Channel", ylabel="Score ($r$)") -mne.viz.tight_layout() # %% # Investigate model coefficients @@ -134,7 +132,7 @@ # Print mean coefficients across all time delays / channels (see Fig 1) time_plot = 0.180 # For highlighting a specific time. -fig, ax = plt.subplots(figsize=(4, 8)) +fig, ax = plt.subplots(figsize=(4, 8), layout="constrained") max_coef = mean_coefs.max() ax.pcolormesh( times, @@ -155,16 +153,14 @@ xticks=np.arange(tmin, tmax + 0.2, 0.2), ) plt.setp(ax.get_xticklabels(), rotation=45) -mne.viz.tight_layout() # Make a topographic map of coefficients for a given delay (see Fig 2C) ix_plot = np.argmin(np.abs(time_plot - times)) -fig, ax = plt.subplots() +fig, ax = plt.subplots(layout="constrained") 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) -mne.viz.tight_layout() # %% # Create and fit a stimulus reconstruction model @@ -240,7 +236,7 @@ y_pred = sr.predict(Y[test]) time = np.linspace(0, 2.0, 5 * int(sfreq)) -fig, ax = plt.subplots(figsize=(8, 4)) +fig, ax = plt.subplots(figsize=(8, 4), layout="constrained") ax.plot( time, speech[test][sr.valid_samples_][: int(5 * sfreq)], color="grey", lw=2, ls="--" ) @@ -248,7 +244,6 @@ ax.legend([lns[0], ln1[0]], ["Envelope", "Reconstruction"], frameon=False) ax.set(title="Stimulus reconstruction") ax.set_xlabel("Time (s)") -mne.viz.tight_layout() # %% # Investigate model coefficients @@ -292,7 +287,6 @@ title="Inverse-transformed coefficients\nbetween delays %s and %s" % (time_plot[0], time_plot[1]) ) -mne.viz.tight_layout() # %% # References diff --git a/examples/inverse/label_source_activations.py b/examples/inverse/label_source_activations.py index 599fff4c2f8..035533b4b9a 100644 --- a/examples/inverse/label_source_activations.py +++ b/examples/inverse/label_source_activations.py @@ -62,7 +62,7 @@ # View source activations # ----------------------- -fig, ax = plt.subplots(1) +fig, ax = plt.subplots(1, layout="constrained") t = 1e3 * stc_label.times ax.plot(t, stc_label.data.T, "k", linewidth=0.5, alpha=0.5) pe = [ @@ -81,7 +81,6 @@ xlim=xlim, ylim=ylim, ) -mne.viz.tight_layout() # %% # Using vector solutions @@ -92,7 +91,7 @@ pick_ori = "vector" stc_vec = apply_inverse(evoked, inverse_operator, lambda2, method, pick_ori=pick_ori) data = stc_vec.extract_label_time_course(label, src) -fig, ax = plt.subplots(1) +fig, ax = plt.subplots(1, layout="constrained") stc_vec_label = stc_vec.in_label(label) colors = ["#EE6677", "#228833", "#4477AA"] for ii, name in enumerate("XYZ"): @@ -117,4 +116,3 @@ xlim=xlim, ylim=ylim, ) -mne.viz.tight_layout() diff --git a/examples/inverse/mixed_source_space_inverse.py b/examples/inverse/mixed_source_space_inverse.py index 9baac7da379..f069b5e89ac 100644 --- a/examples/inverse/mixed_source_space_inverse.py +++ b/examples/inverse/mixed_source_space_inverse.py @@ -194,9 +194,8 @@ ) # plot the times series of 2 labels -fig, axes = plt.subplots(1) +fig, axes = plt.subplots(1, layout="constrained") axes.plot(1e3 * stc.times, label_ts[0][0, :], "k", label="bankssts-lh") axes.plot(1e3 * stc.times, label_ts[0][-1, :].T, "r", label="Brain-stem") axes.set(xlabel="Time (ms)", ylabel="MNE current (nAm)") axes.legend() -mne.viz.tight_layout() diff --git a/examples/inverse/source_space_snr.py b/examples/inverse/source_space_snr.py index 12d081f5c61..c7077d091e5 100644 --- a/examples/inverse/source_space_snr.py +++ b/examples/inverse/source_space_snr.py @@ -51,10 +51,9 @@ # Plot an average SNR across source points over time: ave = np.mean(snr_stc.data, axis=0) -fig, ax = plt.subplots() +fig, ax = plt.subplots(layout="constrained") ax.plot(evoked.times, ave) ax.set(xlabel="Time (s)", ylabel="SNR MEG-EEG") -fig.tight_layout() # Find time point of maximum SNR maxidx = np.argmax(ave) diff --git a/examples/preprocessing/eeg_bridging.py b/examples/preprocessing/eeg_bridging.py index d95ac709513..30cdde8502b 100644 --- a/examples/preprocessing/eeg_bridging.py +++ b/examples/preprocessing/eeg_bridging.py @@ -88,7 +88,7 @@ bridged_idx, ed_matrix = ed_data[6] -fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(8, 4)) +fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(8, 4), layout="constrained") fig.suptitle("Subject 6 Electrical Distance Matrix") # take median across epochs, only use upper triangular, lower is NaNs @@ -110,8 +110,6 @@ ax.set_xlabel("Channel Index") ax.set_ylabel("Channel Index") -fig.tight_layout() - # %% # Examine the Distribution of Electrical Distances # ------------------------------------------------ @@ -208,7 +206,7 @@ # reflect neural or at least anatomical differences as well (i.e. the # distance from the sensors to the brain). -fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(8, 4)) +fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(8, 4), layout="constrained") fig.suptitle("Electrical Distance Distribution for EEGBCI Subjects") for ax in (ax1, ax2): ax.set_ylabel("Count") @@ -229,7 +227,6 @@ ax1.axvspan(0, 30, color="r", alpha=0.5) ax2.legend(loc=(1.04, 0)) -fig.subplots_adjust(right=0.725, bottom=0.15, wspace=0.4) # %% # For the group of subjects, let's look at their electrical distances diff --git a/examples/preprocessing/eeg_csd.py b/examples/preprocessing/eeg_csd.py index dffe94e3f1e..892f856e75e 100644 --- a/examples/preprocessing/eeg_csd.py +++ b/examples/preprocessing/eeg_csd.py @@ -78,8 +78,7 @@ # CSD has parameters ``stiffness`` and ``lambda2`` affecting smoothing and # spline flexibility, respectively. Let's see how they affect the solution: -fig, ax = plt.subplots(4, 4) -fig.subplots_adjust(hspace=0.5) +fig, ax = plt.subplots(4, 4, layout="constrained") fig.set_size_inches(10, 10) for i, lambda2 in enumerate([0, 1e-7, 1e-5, 1e-3]): for j, m in enumerate([5, 4, 3, 2]): diff --git a/examples/preprocessing/eog_artifact_histogram.py b/examples/preprocessing/eog_artifact_histogram.py index 5aa209228d7..2d51370b571 100644 --- a/examples/preprocessing/eog_artifact_histogram.py +++ b/examples/preprocessing/eog_artifact_histogram.py @@ -50,7 +50,6 @@ # %% # Plot EOG artifact distribution -fig, ax = plt.subplots() +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)) -fig.tight_layout() diff --git a/examples/preprocessing/eog_regression.py b/examples/preprocessing/eog_regression.py index 6c88cb01d9a..2123974dde4 100644 --- a/examples/preprocessing/eog_regression.py +++ b/examples/preprocessing/eog_regression.py @@ -69,10 +69,9 @@ epochs_after = mne.Epochs(raw_clean, events, event_id, tmin, tmax, baseline=(tmin, 0)) evoked_after = epochs_after.average() -fig, ax = plt.subplots(nrows=3, ncols=2, figsize=(10, 7), sharex=True, sharey="row") +fig, ax = plt.subplots( + nrows=3, ncols=2, figsize=(10, 7), sharex=True, sharey="row", layout="constrained" +) evoked_before.plot(axes=ax[:, 0], spatial_colors=True) evoked_after.plot(axes=ax[:, 1], spatial_colors=True) -fig.subplots_adjust( - top=0.905, bottom=0.09, left=0.08, right=0.975, hspace=0.325, wspace=0.145 -) fig.suptitle("Before --> After") diff --git a/examples/preprocessing/shift_evoked.py b/examples/preprocessing/shift_evoked.py index 3cd70715ac8..c16becc679c 100644 --- a/examples/preprocessing/shift_evoked.py +++ b/examples/preprocessing/shift_evoked.py @@ -14,7 +14,6 @@ import matplotlib.pyplot as plt import mne -from mne.viz import tight_layout from mne.datasets import sample print(__doc__) @@ -60,5 +59,3 @@ titles=dict(grad="Absolute shift: 500 ms"), time_unit="s", ) - -tight_layout() diff --git a/examples/simulation/plot_stc_metrics.py b/examples/simulation/plot_stc_metrics.py index 750dcab0c21..105c66d7e12 100644 --- a/examples/simulation/plot_stc_metrics.py +++ b/examples/simulation/plot_stc_metrics.py @@ -234,7 +234,7 @@ ] # Plot the results -f, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2, sharex="col", constrained_layout=True) +f, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2, sharex="col", layout="constrained") for ax, (title, results) in zip([ax1, ax2, ax3, ax4], region_results.items()): ax.plot(thresholds, results, ".-") ax.set(title=title, ylabel="score", xlabel="Threshold", xticks=thresholds) @@ -243,7 +243,7 @@ ax1.ticklabel_format(axis="y", style="sci", scilimits=(0, 1)) # tweak RLE # Cosine score with respect to time -f, ax1 = plt.subplots(constrained_layout=True) +f, ax1 = plt.subplots(layout="constrained") ax1.plot(stc_true_region.times, cosine_score(stc_true_region, stc_est_region)) ax1.set(title="Cosine score", xlabel="Time", ylabel="Score") @@ -277,6 +277,6 @@ # Plot the results for name, results in dipole_results.items(): - f, ax1 = plt.subplots(constrained_layout=True) + f, ax1 = plt.subplots(layout="constrained") ax1.plot(thresholds, 100 * np.array(results), ".-") ax1.set(title=name, ylabel="Error (cm)", xlabel="Threshold", xticks=thresholds) diff --git a/examples/time_frequency/source_label_time_frequency.py b/examples/time_frequency/source_label_time_frequency.py index da3af06e4dc..2e7cc4d3592 100644 --- a/examples/time_frequency/source_label_time_frequency.py +++ b/examples/time_frequency/source_label_time_frequency.py @@ -76,8 +76,7 @@ # subtract the evoked response in order to exclude evoked activity epochs_induced = epochs.copy().subtract_evoked() -plt.close("all") - +fig, axes = plt.subplots(2, 2, layout="constrained") for ii, (this_epochs, title) in enumerate( zip([epochs, epochs_induced], ["evoked + induced", "induced only"]) ): @@ -99,9 +98,8 @@ ########################################################################## # View time-frequency plots - plt.subplots_adjust(0.1, 0.08, 0.96, 0.94, 0.2, 0.43) - plt.subplot(2, 2, 2 * ii + 1) - plt.imshow( + ax = axes[ii, 0] + ax.imshow( 20 * power, extent=[times[0], times[-1], freqs[0], freqs[-1]], aspect="auto", @@ -110,13 +108,10 @@ vmax=30.0, cmap="RdBu_r", ) - plt.xlabel("Time (s)") - plt.ylabel("Frequency (Hz)") - plt.title("Power (%s)" % title) - plt.colorbar() + ax.set(xlabel="Time (s)", ylabel="Frequency (Hz)", title=f"Power ({title})") - plt.subplot(2, 2, 2 * ii + 2) - plt.imshow( + ax = axes[ii, 1] + ax.imshow( itc, extent=[times[0], times[-1], freqs[0], freqs[-1]], aspect="auto", @@ -125,9 +120,5 @@ vmax=0.7, cmap="RdBu_r", ) - plt.xlabel("Time (s)") - plt.ylabel("Frequency (Hz)") - plt.title("ITC (%s)" % title) - plt.colorbar() - -plt.show() + ax.set(xlabel="Time (s)", ylabel="Frequency (Hz)", title=f"ITC ({title})") + fig.colorbar(ax.images[0], ax=axes[ii]) diff --git a/examples/time_frequency/source_power_spectrum_opm.py b/examples/time_frequency/source_power_spectrum_opm.py index 14fcfa7039f..ce2ad03f607 100644 --- a/examples/time_frequency/source_power_spectrum_opm.py +++ b/examples/time_frequency/source_power_spectrum_opm.py @@ -84,7 +84,6 @@ .plot(picks="data", exclude="bads") ) fig.suptitle(titles[kind]) - fig.subplots_adjust(0.1, 0.1, 0.95, 0.85) ############################################################################## # Alignment and forward diff --git a/examples/time_frequency/time_frequency_simulated.py b/examples/time_frequency/time_frequency_simulated.py index 46747b6ae69..c6f00a9da32 100644 --- a/examples/time_frequency/time_frequency_simulated.py +++ b/examples/time_frequency/time_frequency_simulated.py @@ -100,7 +100,7 @@ freqs = np.arange(5.0, 100.0, 3.0) vmin, vmax = -3.0, 3.0 # Define our color limits. -fig, axs = plt.subplots(1, 3, figsize=(15, 5), sharey=True) +fig, axs = plt.subplots(1, 3, figsize=(15, 5), sharey=True, layout="constrained") for n_cycles, time_bandwidth, ax, title in zip( [freqs / 2, freqs, freqs / 2], # number of cycles [2.0, 4.0, 8.0], # time bandwidth @@ -130,7 +130,6 @@ show=False, colorbar=False, ) -plt.tight_layout() ############################################################################## # Stockwell (S) transform @@ -143,7 +142,7 @@ # we control the spectral / temporal resolution by specifying different widths # of the gaussian window using the ``width`` parameter. -fig, axs = plt.subplots(1, 3, figsize=(15, 5), sharey=True) +fig, axs = plt.subplots(1, 3, figsize=(15, 5), sharey=True, layout="constrained") fmin, fmax = freqs[[0, -1]] for width, ax in zip((0.2, 0.7, 3.0), axs): power = tfr_stockwell(epochs, fmin=fmin, fmax=fmax, width=width) @@ -151,7 +150,6 @@ [0], baseline=(0.0, 0.1), mode="mean", axes=ax, show=False, colorbar=False ) ax.set_title("Sim: Using S transform, width = {:0.1f}".format(width)) -plt.tight_layout() # %% # Morlet Wavelets @@ -162,7 +160,7 @@ # temporal resolution with the ``n_cycles`` parameter, which defines the # number of cycles to include in the window. -fig, axs = plt.subplots(1, 3, figsize=(15, 5), sharey=True) +fig, axs = plt.subplots(1, 3, figsize=(15, 5), sharey=True, layout="constrained") all_n_cycles = [1, 3, freqs / 2.0] for n_cycles, ax in zip(all_n_cycles, axs): power = tfr_morlet(epochs, freqs=freqs, n_cycles=n_cycles, return_itc=False) @@ -178,7 +176,6 @@ ) n_cycles = "scaled by freqs" if not isinstance(n_cycles, int) else n_cycles ax.set_title(f"Sim: Using Morlet wavelet, n_cycles = {n_cycles}") -plt.tight_layout() # %% # Narrow-bandpass Filter and Hilbert Transform @@ -189,7 +186,7 @@ # important so that you isolate only one oscillation of interest, generally # the width of this filter is recommended to be about 2 Hz. -fig, axs = plt.subplots(1, 3, figsize=(15, 5), sharey=True) +fig, axs = plt.subplots(1, 3, figsize=(15, 5), sharey=True, layout="constrained") bandwidths = [1.0, 2.0, 4.0] for bandwidth, ax in zip(bandwidths, axs): data = np.zeros((len(ch_names), freqs.size, epochs.times.size), dtype=complex) @@ -233,7 +230,6 @@ f"bandwidth = {bandwidth}, " f"transition bandwidth = {4 * bandwidth}" ) -plt.tight_layout() # %% # Calculating a TFR without averaging over epochs @@ -277,12 +273,9 @@ ) # Baseline the output rescale(power, epochs.times, (0.0, 0.1), mode="mean", copy=False) -fig, ax = plt.subplots() +fig, ax = plt.subplots(layout="constrained") x, y = centers_to_edges(epochs.times * 1000, freqs) mesh = ax.pcolormesh(x, y, power[0], cmap="RdBu_r", vmin=vmin, vmax=vmax) ax.set_title("TFR calculated on a numpy array") ax.set(ylim=freqs[[0, -1]], xlabel="Time (ms)") fig.colorbar(mesh) -plt.tight_layout() - -plt.show() diff --git a/examples/visualization/3d_to_2d.py b/examples/visualization/3d_to_2d.py index 590cc9df639..966e97f76ac 100644 --- a/examples/visualization/3d_to_2d.py +++ b/examples/visualization/3d_to_2d.py @@ -129,8 +129,7 @@ lt = mne.channels.read_layout(layout_path / layout_name, scale=False) x = lt.pos[:, 0] * float(im.shape[1]) y = (1 - lt.pos[:, 1]) * float(im.shape[0]) # Flip the y-position -fig, ax = plt.subplots() +fig, ax = plt.subplots(layout="constrained") ax.imshow(im) ax.scatter(x, y, s=80, color="r") -fig.tight_layout() ax.set_axis_off() diff --git a/examples/visualization/evoked_topomap.py b/examples/visualization/evoked_topomap.py index 1497e91bda8..dfd6be7f0f3 100644 --- a/examples/visualization/evoked_topomap.py +++ b/examples/visualization/evoked_topomap.py @@ -94,7 +94,7 @@ # and ``'head'`` otherwise. Here we show each option: extrapolations = ["local", "head", "box"] -fig, axes = plt.subplots(figsize=(7.5, 4.5), nrows=2, ncols=3) +fig, axes = plt.subplots(figsize=(7.5, 4.5), nrows=2, ncols=3, layout="constrained") # Here we look at EEG channels, and use a custom head sphere to get all the # sensors to be well within the drawn head surface @@ -111,7 +111,6 @@ sphere=(0.0, 0.0, 0.0, 0.09), ) ax.set_title("%s %s" % (ch_type.upper(), extr), fontsize=14) -fig.tight_layout() # %% # More advanced usage @@ -123,7 +122,6 @@ fig = evoked.plot_topomap( 0.1, ch_type="mag", show_names=True, colorbar=False, size=6, res=128 ) -fig.subplots_adjust(left=0.01, right=0.99, bottom=0.01, top=0.88) fig.suptitle("Auditory response") # %% diff --git a/mne/conftest.py b/mne/conftest.py index c1e6b36a93b..a0eeaf18dfb 100644 --- a/mne/conftest.py +++ b/mne/conftest.py @@ -33,7 +33,6 @@ Bunch, _check_qt_version, _TempDir, - check_version, ) # data from sample dataset @@ -84,6 +83,7 @@ def pytest_configure(config): "slowtest", "ultraslowtest", "pgtest", + "pvtest", "allow_unclosed", "allow_unclosed_pyside2", ): @@ -104,6 +104,13 @@ def pytest_configure(config): if os.getenv("PYTEST_QT_API") is None and os.getenv("QT_API") is not None: os.environ["PYTEST_QT_API"] = os.environ["QT_API"] + # suppress: + # Debugger warning: It seems that frozen modules are being used, which may + # make the debugger miss breakpoints. Please pass -Xfrozen_modules=off + # to python to disable frozen modules. + if os.getenv("PYDEVD_DISABLE_FILE_VALIDATION") is None: + os.environ["PYDEVD_DISABLE_FILE_VALIDATION"] = "1" + # https://numba.readthedocs.io/en/latest/reference/deprecation.html#deprecation-of-old-style-numba-captured-errors # noqa: E501 if "NUMBA_CAPTURED_ERRORS" not in os.environ: os.environ["NUMBA_CAPTURED_ERRORS"] = "new_style" @@ -514,8 +521,9 @@ def pg_backend(request, garbage_collect): import mne_qt_browser mne_qt_browser._browser_instances.clear() - if check_version("mne_qt_browser", min_version="0.4"): - _assert_no_instances(MNEQtBrowser, f"Closure of {request.node.name}") + if not _test_passed(request): + return + _assert_no_instances(MNEQtBrowser, f"Closure of {request.node.name}") @pytest.fixture( @@ -541,35 +549,35 @@ def browser_backend(request, garbage_collect, monkeypatch): mne_qt_browser._browser_instances.clear() -@pytest.fixture(params=["pyvistaqt"]) +@pytest.fixture(params=[pytest.param("pyvistaqt", marks=pytest.mark.pvtest)]) def renderer(request, options_3d, garbage_collect): """Yield the 3D backends.""" with _use_backend(request.param, interactive=False) as renderer: yield renderer -@pytest.fixture(params=["pyvistaqt"]) +@pytest.fixture(params=[pytest.param("pyvistaqt", marks=pytest.mark.pvtest)]) def renderer_pyvistaqt(request, options_3d, garbage_collect): """Yield the PyVista backend.""" with _use_backend(request.param, interactive=False) as renderer: yield renderer -@pytest.fixture(params=["notebook"]) +@pytest.fixture(params=[pytest.param("notebook", marks=pytest.mark.pvtest)]) def renderer_notebook(request, options_3d): """Yield the 3D notebook renderer.""" with _use_backend(request.param, interactive=False) as renderer: yield renderer -@pytest.fixture(params=["pyvistaqt"]) +@pytest.fixture(params=[pytest.param("pyvistaqt", marks=pytest.mark.pvtest)]) def renderer_interactive_pyvistaqt(request, options_3d, qt_windows_closed): """Yield the interactive PyVista backend.""" with _use_backend(request.param, interactive=True) as renderer: yield renderer -@pytest.fixture(params=["pyvistaqt"]) +@pytest.fixture(params=[pytest.param("pyvistaqt", marks=pytest.mark.pvtest)]) def renderer_interactive(request, options_3d): """Yield the interactive 3D backends.""" with _use_backend(request.param, interactive=True) as renderer: @@ -872,6 +880,14 @@ def protect_config(): yield +def _test_passed(request): + try: + outcome = request.node.harvest_rep_call + except Exception: + outcome = "passed" + return outcome == "passed" + + @pytest.fixture() def brain_gc(request): """Ensure that brain can be properly garbage collected.""" @@ -897,11 +913,7 @@ def brain_gc(request): yield close_func() # no need to warn if the test itself failed, pytest-harvest helps us here - try: - outcome = request.node.harvest_rep_call - except Exception: - outcome = "failed" - if outcome != "passed": + if not _test_passed(request): return _assert_no_instances(Brain, "after") # Check VTK diff --git a/mne/preprocessing/eyetracking/calibration.py b/mne/preprocessing/eyetracking/calibration.py index 962299f3a84..1891ebacb30 100644 --- a/mne/preprocessing/eyetracking/calibration.py +++ b/mne/preprocessing/eyetracking/calibration.py @@ -147,7 +147,7 @@ def plot(self, show_offsets=True, axes=None, show=True): ax = axes fig = ax.get_figure() else: # create new figure and axes - fig, ax = plt.subplots(constrained_layout=True) + fig, ax = plt.subplots(layout="constrained") px, py = self["positions"].T gaze_x, gaze_y = self["gaze"].T diff --git a/mne/preprocessing/ica.py b/mne/preprocessing/ica.py index 15c1d286d6e..fdb7d920267 100644 --- a/mne/preprocessing/ica.py +++ b/mne/preprocessing/ica.py @@ -3366,7 +3366,6 @@ def corrmap( template=True, sphere=sphere, ) - template_fig.subplots_adjust(top=0.8) template_fig.canvas.draw() # first run: use user-selected map diff --git a/mne/report/report.py b/mne/report/report.py index 89154d3de76..faf12a79bd6 100644 --- a/mne/report/report.py +++ b/mne/report/report.py @@ -78,7 +78,7 @@ ) from ..viz._brain.view import views_dicts from ..viz.misc import _plot_mri_contours, _get_bem_plotting_surfaces -from ..viz.utils import _ndarray_to_fig, tight_layout +from ..viz.utils import _ndarray_to_fig from ..viz._scraper import _mne_qt_browser_screenshot from ..forward import read_forward_solution, Forward from ..epochs import read_epochs, BaseEpochs @@ -431,11 +431,6 @@ def _fig_to_img(fig, *, image_format="png", own_figure=True): # matplotlib modifies the passed dict, which is a bug mpl_kwargs["pil_kwargs"] = pil_kwargs.copy() with warnings.catch_warnings(): - warnings.filterwarnings( - action="ignore", - message=".*Axes that are not compatible with tight_layout.*", - category=UserWarning, - ) fig.savefig(output, format=image_format, dpi=dpi, **mpl_kwargs) if own_figure: @@ -1648,7 +1643,6 @@ def _add_ica_overlay(self, *, ica, inst, image_format, section, tags, replace): fig = ica.plot_overlay(inst=inst_, show=False, on_baseline="reapply") del inst_ - tight_layout(fig=fig) _constrain_fig_resolution(fig, max_width=MAX_IMG_WIDTH, max_res=MAX_IMG_RES) self._add_figure( fig=fig, @@ -1770,9 +1764,6 @@ def _add_ica_components(self, *, ica, picks, image_format, section, tags, replac if not isinstance(figs, list): figs = [figs] - for fig in figs: - tight_layout(fig=fig) - title = "ICA component topographies" if len(figs) == 1: fig = figs[0] @@ -3241,7 +3232,6 @@ def _add_raw( init_kwargs.setdefault("fmax", fmax) plot_kwargs.setdefault("show", False) fig = raw.compute_psd(**init_kwargs).plot(**plot_kwargs) - tight_layout(fig=fig) _constrain_fig_resolution(fig, max_width=MAX_IMG_WIDTH, max_res=MAX_IMG_RES) self._add_figure( fig=fig, @@ -3323,7 +3313,6 @@ def _add_projs( # hard to see how (6, 4) could work in all number-of-projs by # number-of-channel-types conditions... fig.set_size_inches((6, 4)) - tight_layout(fig=fig) _constrain_fig_resolution(fig, max_width=MAX_IMG_WIDTH, max_res=MAX_IMG_RES) self._add_figure( fig=fig, @@ -3488,6 +3477,7 @@ def _plot_one_evoked_topomap_timepoint( len(ch_types) * 2, gridspec_kw={"width_ratios": [8, 0.5] * len(ch_types)}, figsize=(2.5 * len(ch_types), 2), + layout="constrained", ) _constrain_fig_resolution(fig, max_width=MAX_IMG_WIDTH, max_res=MAX_IMG_RES) ch_type_ax_map = dict( @@ -3508,8 +3498,6 @@ def _plot_one_evoked_topomap_timepoint( ) ch_type_ax_map[ch_type][0].set_title(ch_type) - tight_layout(fig=fig) - with BytesIO() as buff: fig.savefig(buff, format="png", pad_inches=0) plt.close(fig) @@ -3616,7 +3604,7 @@ def _add_evoked_gfp( import matplotlib.pyplot as plt - fig, ax = plt.subplots(len(ch_types), 1, sharex=True) + fig, ax = plt.subplots(len(ch_types), 1, sharex=True, layout="constrained") if len(ch_types) == 1: ax = [ax] for idx, ch_type in enumerate(ch_types): @@ -3636,7 +3624,6 @@ def _add_evoked_gfp( if idx < len(ch_types) - 1: ax[idx].set_xlabel(None) - tight_layout(fig=fig) _constrain_fig_resolution(fig, max_width=MAX_IMG_WIDTH, max_res=MAX_IMG_RES) title = "Global field power" self._add_figure( @@ -3655,7 +3642,6 @@ def _add_evoked_whitened( ): """Render whitened evoked.""" fig = evoked.plot_white(noise_cov=noise_cov, show=False) - tight_layout(fig=fig) _constrain_fig_resolution(fig, max_width=MAX_IMG_WIDTH, max_res=MAX_IMG_RES) title = "Whitened" @@ -4003,7 +3989,6 @@ def _add_epochs( fig = epochs.plot_drop_log( subject=self.subject, ignore=drop_log_ignore, show=False ) - tight_layout(fig=fig) _constrain_fig_resolution( fig, max_width=MAX_IMG_WIDTH, max_res=MAX_IMG_RES ) @@ -4179,18 +4164,17 @@ def _add_stc( if backend_is_3d: brain.set_time(t) - fig, ax = plt.subplots(figsize=(4.5, 4.5)) + fig, ax = plt.subplots(figsize=(4.5, 4.5), layout="constrained") ax.imshow(brain.screenshot(time_viewer=True, mode="rgb")) ax.axis("off") - tight_layout(fig=fig) _constrain_fig_resolution( fig, max_width=stc_plot_kwargs["size"][0], max_res=MAX_IMG_RES ) figs.append(fig) plt.close(fig) else: - fig_lh = plt.figure() - fig_rh = plt.figure() + fig_lh = plt.figure(layout="constrained") + fig_rh = plt.figure(layout="constrained") brain_lh = stc.plot( views="lat", @@ -4210,8 +4194,6 @@ def _add_stc( backend="matplotlib", figure=fig_rh, ) - tight_layout(fig=fig_lh) # TODO is this necessary? - tight_layout(fig=fig_rh) # TODO is this necessary? _constrain_fig_resolution( fig_lh, max_width=stc_plot_kwargs["size"][0], diff --git a/mne/time_frequency/spectrum.py b/mne/time_frequency/spectrum.py index 52ca167ee6c..1fc2c6ce2bd 100644 --- a/mne/time_frequency/spectrum.py +++ b/mne/time_frequency/spectrum.py @@ -742,7 +742,6 @@ def plot( sphere=sphere, xlabels_list=xlabels_list, ) - fig.subplots_adjust(hspace=0.3) plt_show(show, fig) return fig diff --git a/mne/time_frequency/tfr.py b/mne/time_frequency/tfr.py index 1a061b8b173..83445a64690 100644 --- a/mne/time_frequency/tfr.py +++ b/mne/time_frequency/tfr.py @@ -70,7 +70,6 @@ figure_nobar, plt_show, _setup_cmap, - _connection_line, _prepare_joint_axes, _setup_vmin_vmax, _set_title_multiple_electrodes, @@ -141,7 +140,7 @@ def morlet(sfreq, freqs, n_cycles=7.0, sigma=None, zero_mean=False): s = w * sfreq / (2 * freq * np.pi) # from SciPy docs wavelet_sp = sp_morlet(M, s, w) * np.sqrt(2) # match our normalization - _, ax = plt.subplots(constrained_layout=True) + _, ax = plt.subplots(layout="constrained") colors = { ('MNE', 'real'): '#66CCEE', ('SciPy', 'real'): '#4477AA', @@ -1732,7 +1731,7 @@ def _plot( elif isinstance(axes, plt.Axes): figs_and_axes = [(ax.get_figure(), ax) for ax in [axes]] elif axes is None: - figs = [plt.figure() for i in range(n_picks)] + figs = [plt.figure(layout="constrained") for i in range(n_picks)] figs_and_axes = [(fig, fig.add_subplot(111)) for fig in figs] else: raise ValueError("axes must be None, plt.Axes, or list " "of plt.Axes.") @@ -1921,7 +1920,7 @@ def plot_joint( .. versionadded:: 0.16.0 """ # noqa: E501 - import matplotlib.pyplot as plt + from matplotlib.patches import ConnectionPatch ##################################### # Handle channels (picks and types) # @@ -2007,7 +2006,7 @@ def plot_joint( # Image plot # ############## - fig, tf_ax, map_ax, cbar_ax = _prepare_joint_axes(n_timefreqs) + fig, tf_ax, map_ax = _prepare_joint_axes(n_timefreqs) cmap = _setup_cmap(cmap) @@ -2162,28 +2161,32 @@ def plot_joint( ############# # Finish up # ############# - if colorbar: from matplotlib import ticker - cbar = plt.colorbar(ax.images[0], cax=cbar_ax) + cbar = fig.colorbar(ax.images[0]) if locator is None: locator = ticker.MaxNLocator(nbins=5) cbar.locator = locator cbar.update_ticks() - plt.subplots_adjust( - left=0.12, right=0.925, bottom=0.14, top=1.0 if title is not None else 1.2 - ) - # draw the connection lines between time series and topoplots - lines = [ - _connection_line( - time_, fig, tf_ax, map_ax_, y=freq_, y_source_transform="transData" + for (time_, freq_), map_ax_ in zip(timefreqs_array, map_ax): + con = ConnectionPatch( + xyA=[time_, freq_], + xyB=[0.5, 0], + coordsA="data", + coordsB="axes fraction", + axesA=tf_ax, + axesB=map_ax_, + color="grey", + linestyle="-", + linewidth=1.5, + alpha=0.66, + zorder=1, + clip_on=False, ) - for (time_, freq_), map_ax_ in zip(timefreqs_array, map_ax) - ] - fig.lines.extend(lines) + fig.add_artist(con) plt_show(show) return fig @@ -2289,7 +2292,6 @@ def _onselect( axes=ax, ) ax.set_title(ch_type) - fig.tight_layout() @verbose def plot_topo( diff --git a/mne/viz/_3d.py b/mne/viz/_3d.py index ce99f2e6352..680d52022b5 100644 --- a/mne/viz/_3d.py +++ b/mne/viz/_3d.py @@ -88,7 +88,6 @@ _get_color_list, _get_cmap, plt_show, - tight_layout, figure_nobar, _check_time_unit, ) @@ -314,7 +313,9 @@ def plot_head_positions( from mpl_toolkits.mplot3d.art3d import Line3DCollection from mpl_toolkits.mplot3d import Axes3D # noqa: F401, analysis:ignore - fig, ax = plt.subplots(1, subplot_kw=dict(projection="3d")) + fig, ax = plt.subplots( + 1, subplot_kw=dict(projection="3d"), layout="constrained" + ) # First plot the trajectory as a colormap: # http://matplotlib.org/examples/pylab_examples/multicolored_line.html @@ -374,7 +375,6 @@ def plot_head_positions( ax.set(xlabel="x", ylabel="y", zlabel="z", xlim=xlim, ylim=ylim, zlim=zlim) _set_aspect_equal(ax) ax.view_init(30, 45) - tight_layout(fig=fig) plt_show(show) return fig @@ -1901,7 +1901,7 @@ def _key_pressed_slider(event, params): time_viewer.slider.set_val(this_time) -def _smooth_plot(this_time, params): +def _smooth_plot(this_time, params, *, draw=True): """Smooth source estimate data and plot with mpl.""" from ..morph import _hemi_morph @@ -1957,7 +1957,8 @@ def _smooth_plot(this_time, params): _set_aspect_equal(ax) ax.axis("off") ax.set(xlim=[-80, 80], ylim=(-80, 80), zlim=[-80, 80]) - ax.figure.canvas.draw() + if draw: + ax.figure.canvas.draw() def _plot_mpl_stc( @@ -2022,7 +2023,8 @@ def _plot_mpl_stc( del transparent, mapdata time_label, times = _handle_time(time_label, time_unit, stc.times) - fig = plt.figure(figsize=(6, 6)) if figure is None else figure + # don't use constrained layout because Axes3D does not play well with it + fig = plt.figure(figsize=(6, 6), layout=None) if figure is None else figure try: ax = Axes3D(fig, auto_add_to_figure=False) except Exception: # old mpl @@ -2072,7 +2074,7 @@ def _plot_mpl_stc( time_label=time_label, time_unit=time_unit, ) - _smooth_plot(initial_time, params) + _smooth_plot(initial_time, params, draw=False) ax.view_init(**kwargs[hemi][views]) @@ -2100,7 +2102,6 @@ def _plot_mpl_stc( callback_key = partial(_key_pressed_slider, params=params) time_viewer.canvas.mpl_connect("key_press_event", callback_key) - time_viewer.subplots_adjust(left=0.12, bottom=0.05, right=0.75, top=0.95) fig.subplots_adjust(left=0.0, bottom=0.0, right=1.0, top=1.0) # add colorbar @@ -2932,7 +2933,7 @@ def _onclick(event, params, verbose=None): del ijk # Plot initial figure - fig, (axes, ax_time) = plt.subplots(2) + fig, (axes, ax_time) = plt.subplots(2, layout="constrained") axes.set(xticks=[], yticks=[]) marker = "o" if len(stc.times) == 1 else None ydata = stc.data[loc_idx] @@ -2943,7 +2944,6 @@ def _onclick(event, params, verbose=None): vert_legend = ax_time.legend([h], [""], title="Vertex") _update_vertlabel(loc_idx) lx = ax_time.axvline(stc.times[time_idx], color="g") - fig.tight_layout() allow_pos_lims = mode != "glass_brain" mapdata = _process_clim(clim, colormap, transparent, stc.data, allow_pos_lims) @@ -3390,7 +3390,7 @@ def plot_sparse_source_estimates( ) # Show time courses - fig = plt.figure(fig_number) + fig = plt.figure(fig_number, layout="constrained") fig.clf() ax = fig.add_subplot(111) @@ -3757,7 +3757,9 @@ def _plot_dipole_mri_orthoview( dims = len(data) # Symmetric size assumed. dd = dims // 2 if ax is None: - fig, ax = plt.subplots(1, subplot_kw=dict(projection="3d")) + fig, ax = plt.subplots( + 1, subplot_kw=dict(projection="3d"), layout="constrained" + ) else: _validate_type(ax, Axes3D, "ax", "Axes3D", extra='when mode is "orthoview"') fig = ax.get_figure() diff --git a/mne/viz/__init__.pyi b/mne/viz/__init__.pyi index e73226b6909..b709ebc2a05 100644 --- a/mne/viz/__init__.pyi +++ b/mne/viz/__init__.pyi @@ -82,7 +82,6 @@ __all__ = [ "set_3d_view", "set_browser_backend", "snapshot_brain_montage", - "tight_layout", "ui_events", "use_3d_backend", "use_browser_backend", @@ -149,7 +148,6 @@ from .topomap import ( plot_regression_weights, ) from .utils import ( - tight_layout, mne_analyze_colormap, compare_fiff, ClickableImage, diff --git a/mne/viz/_dipole.py b/mne/viz/_dipole.py index 64ab5774ba4..24fc4735f3c 100644 --- a/mne/viz/_dipole.py +++ b/mne/viz/_dipole.py @@ -53,9 +53,7 @@ def _plot_dipole_mri_outlines( _validate_type(surf, (str, None), "surf") _check_option("surf", surf, ("white", "pial", None)) if ax is None: - _, ax = plt.subplots( - 1, 3, figsize=(7, 2.5), squeeze=True, constrained_layout=True - ) + _, ax = plt.subplots(1, 3, figsize=(7, 2.5), squeeze=True, layout="constrained") _validate_if_list_of_axes(ax, 3, name="ax") dipoles = _check_concat_dipoles(dipoles) color = "r" if color is None else color diff --git a/mne/viz/_figure.py b/mne/viz/_figure.py index 82359f585ed..738bf838ce3 100644 --- a/mne/viz/_figure.py +++ b/mne/viz/_figure.py @@ -535,7 +535,7 @@ def _create_epoch_image_fig(self, pick): title = f"Epochs image ({ch_name})" fig = self._new_child_figure(figsize=(6, 4), fig_name=None, window_title=title) fig.suptitle = title - gs = GridSpec(nrows=3, ncols=10) + gs = GridSpec(nrows=3, ncols=10, figure=fig) fig.add_subplot(gs[:2, :9]) fig.add_subplot(gs[2, :9]) fig.add_subplot(gs[:2, 9]) @@ -580,16 +580,6 @@ def _create_epoch_histogram(self): ax.plot((reject, reject), (0, ax.get_ylim()[1]), color="r") # finalize fig.suptitle(title, y=0.99) - if hasattr(fig, "_inch_to_rel"): - kwargs = dict( - bottom=fig._inch_to_rel(0.5, horiz=False), - top=1 - fig._inch_to_rel(0.5, horiz=False), - left=fig._inch_to_rel(0.75), - right=1 - fig._inch_to_rel(0.25), - ) - else: - kwargs = dict() - fig.subplots_adjust(hspace=0.7, **kwargs) self.mne.fig_histogram = fig return fig diff --git a/mne/viz/_mpl_figure.py b/mne/viz/_mpl_figure.py index 2974df90958..c313bfe1edf 100644 --- a/mne/viz/_mpl_figure.py +++ b/mne/viz/_mpl_figure.py @@ -118,7 +118,7 @@ def __init__(self, **kwargs): for key in [k for k in kwargs if not hasattr(self.mne, k)]: setattr(self.mne, key, kwargs[key]) - def _close(self, event): + def _close(self, event=None): """Handle close events.""" logger.debug(f"Closing {self!r}") # remove references from parent fig to child fig @@ -886,9 +886,15 @@ def _create_ch_context_fig(self, idx): fig = super()._create_ch_context_fig(idx) plt_show(fig=fig) - def _new_child_figure(self, fig_name, **kwargs): + def _new_child_figure(self, fig_name, *, layout=None, **kwargs): """Instantiate a new MNE dialog figure (with event listeners).""" - fig = _figure(toolbar=False, parent_fig=self, fig_name=fig_name, **kwargs) + fig = _figure( + toolbar=False, + parent_fig=self, + fig_name=fig_name, + layout=layout, + **kwargs, + ) fig._add_default_callbacks() self.mne.child_figs.append(fig) if isinstance(fig_name, str): @@ -2324,8 +2330,8 @@ def _get_scale_bar_texts(self): class MNELineFigure(MNEFigure): """Interactive figure for non-scrolling line plots.""" - def __init__(self, inst, n_axes, figsize, **kwargs): - super().__init__(figsize=figsize, inst=inst, **kwargs) + def __init__(self, inst, n_axes, figsize, *, layout=None, **kwargs): + super().__init__(figsize=figsize, inst=inst, layout=layout, **kwargs) # AXES: default margins (inches) l_margin = 0.8 @@ -2372,6 +2378,8 @@ def _figure(toolbar=True, FigureClass=MNEFigure, **kwargs): from matplotlib import rc_context title = kwargs.pop("window_title", None) # extract title before init + if "layout" not in kwargs: + kwargs["layout"] = "constrained" rc = dict() if toolbar else dict(toolbar="none") with rc_context(rc=rc): fig = plt.figure(FigureClass=FigureClass, **kwargs) @@ -2379,6 +2387,14 @@ def _figure(toolbar=True, FigureClass=MNEFigure, **kwargs): fig.mne.backend = BACKEND if title is not None: _set_window_title(fig, title) + # 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) + # add event callbacks fig._add_default_callbacks() return fig @@ -2409,6 +2425,7 @@ def _line_figure(inst, axes=None, picks=None, **kwargs): FigureClass=MNELineFigure, figsize=figsize, n_axes=n_axes, + layout=None, **kwargs, ) fig.mne.fig_size_px = fig._get_size_px() # can't do in __init__ @@ -2483,7 +2500,7 @@ def _init_browser(**kwargs): """Instantiate a new MNE browse-style figure.""" from mne.io import BaseRaw - fig = _figure(toolbar=False, FigureClass=MNEBrowseFigure, **kwargs) + fig = _figure(toolbar=False, FigureClass=MNEBrowseFigure, layout=None, **kwargs) # splash is ignored (maybe we could do it for mpl if we get_backend() and # check if it's Qt... but seems overkill) diff --git a/mne/viz/_proj.py b/mne/viz/_proj.py index 5a40df7dc03..0f8f02a3089 100644 --- a/mne/viz/_proj.py +++ b/mne/viz/_proj.py @@ -102,7 +102,7 @@ def plot_projs_joint( n_row = len(ch_types) shape = (n_row, n_col) fig = plt.figure( - figsize=(n_col * 1.1 + 0.5, n_row * 1.8 + 0.5), constrained_layout=True + figsize=(n_col * 1.1 + 0.5, n_row * 1.8 + 0.5), layout="constrained" ) ri = 0 # pick some sufficiently distinct colors (6 per proj type, e.g., ECG, diff --git a/mne/viz/backends/_abstract.py b/mne/viz/backends/_abstract.py index c2c3e08eb2b..e924e7deae9 100644 --- a/mne/viz/backends/_abstract.py +++ b/mne/viz/backends/_abstract.py @@ -7,10 +7,8 @@ # License: Simplified BSD from abc import ABC, abstractmethod, abstractclassmethod -from contextlib import nullcontext import warnings -from ..utils import tight_layout from ..ui_events import publish, TimeChange @@ -1333,19 +1331,10 @@ def _mpl_initialize(): class _AbstractMplCanvas(ABC): def __init__(self, width, height, dpi): """Initialize the MplCanvas.""" - from matplotlib import rc_context from matplotlib.figure import Figure - # prefer constrained layout here but live with tight_layout otherwise - context = nullcontext self._extra_events = ("resize",) - try: - context = rc_context({"figure.constrained_layout.use": True}) - self._extra_events = () - except KeyError: - pass - with context: - self.fig = Figure(figsize=(width, height), dpi=dpi) + self.fig = Figure(figsize=(width, height), dpi=dpi, layout="constrained") self.axes = self.fig.add_subplot(111) self.axes.set(xlabel="Time (s)", ylabel="Activation (AU)") self.manager = None @@ -1408,7 +1397,7 @@ def clear(self): def on_resize(self, event): """Handle resize events.""" - tight_layout(fig=self.axes.figure) + pass class _AbstractBrainMplCanvas(_AbstractMplCanvas): diff --git a/mne/viz/backends/tests/test_utils.py b/mne/viz/backends/tests/test_utils.py index 3bec2aafcc9..cfa0c65535f 100644 --- a/mne/viz/backends/tests/test_utils.py +++ b/mne/viz/backends/tests/test_utils.py @@ -7,6 +7,7 @@ from colorsys import rgb_to_hls from contextlib import nullcontext +import platform import numpy as np import pytest @@ -79,6 +80,8 @@ def test_theme_colors(pg_backend, theme, monkeypatch, tmp_path): return # we could add a ton of conditionals below, but KISS is_dark = _qt_is_dark(fig) # on Darwin these checks get complicated, so don't bother for now + if platform.system() == "Darwin": + pytest.skip("Problems on macOS") if theme == "dark": assert is_dark, theme elif theme == "light": diff --git a/mne/viz/circle.py b/mne/viz/circle.py index af160141741..983eef69c5c 100644 --- a/mne/viz/circle.py +++ b/mne/viz/circle.py @@ -212,7 +212,7 @@ def _plot_connectivity_circle( # Use a polar axes if ax is None: - fig = plt.figure(figsize=(8, 8), facecolor=facecolor) + fig = plt.figure(figsize=(8, 8), facecolor=facecolor, layout="constrained") ax = fig.add_subplot(polar=True) else: fig = ax.figure diff --git a/mne/viz/epochs.py b/mne/viz/epochs.py index d173c80a45b..7bd1785ada9 100644 --- a/mne/viz/epochs.py +++ b/mne/viz/epochs.py @@ -13,7 +13,6 @@ from collections import Counter from copy import deepcopy -import warnings import numpy as np from scipy.ndimage import gaussian_filter1d @@ -31,7 +30,6 @@ _VALID_CHANNEL_TYPES, ) from .utils import ( - tight_layout, _setup_vmin_vmax, plt_show, _check_cov, @@ -453,7 +451,7 @@ def _validate_fig_and_axes(fig, axes, group_by, evoked, colorbar, clear=False): rowspan = 2 if evoked else 3 shape = (3, 10) for this_group in group_by: - this_fig = figure() + this_fig = figure(layout="constrained") _set_window_title(this_fig, this_group) subplot2grid(shape, (0, 0), colspan=colspan, rowspan=rowspan, fig=this_fig) if evoked: @@ -602,8 +600,6 @@ def _plot_epochs_image( tmax = epochs.times[-1] ax_im = ax["image"] - fig = ax_im.get_figure() - # draw the image cmap = _setup_cmap(cmap, norm=norm) n_epochs = len(image) @@ -664,13 +660,10 @@ def _plot_epochs_image( ax_im.CB = DraggableColorbar( this_colorbar, im, kind="epochs_image", ch_type=unit ) - with warnings.catch_warnings(record=True): - warnings.simplefilter("ignore") - tight_layout(fig=fig) # finish plt_show(show) - return fig + return ax_im.get_figure() def plot_drop_log( @@ -733,7 +726,7 @@ def plot_drop_log( ch_names = np.array(list(scores.keys())) counts = np.array(list(scores.values())) # init figure, handle easy case (no drops) - fig, ax = plt.subplots() + fig, ax = plt.subplots(layout="constrained") title = f"{absolute} of {n_epochs_before_drop} epochs removed " f"({percent:.1f}%)" if subject is not None: title = f"{subject}: {title}" @@ -755,7 +748,6 @@ def plot_drop_log( ) ax.set_ylabel("% of epochs removed") ax.grid(axis="y") - tight_layout(pad=1, fig=fig) plt_show(show) return fig diff --git a/mne/viz/evoked.py b/mne/viz/evoked.py index 687203cad49..5886bb26db3 100644 --- a/mne/viz/evoked.py +++ b/mne/viz/evoked.py @@ -30,7 +30,6 @@ from ..defaults import _handle_default from .utils import ( _draw_proj_checkbox, - tight_layout, _check_delayed_ssp, plt_show, _process_times, @@ -41,7 +40,6 @@ _make_combine_callable, _validate_if_list_of_axes, _triage_rank_sss, - _connection_line, _get_color_list, _setup_ax_spines, _setup_plot_projector, @@ -165,7 +163,11 @@ def _line_plot_onselect( minidx = np.abs(times - xmin).argmin() maxidx = np.abs(times - xmax).argmin() fig, axarr = plt.subplots( - 1, len(ch_types), squeeze=False, figsize=(3 * len(ch_types), 3) + 1, + len(ch_types), + squeeze=False, + figsize=(3 * len(ch_types), 3), + layout="constrained", ) for idx, ch_type in enumerate(ch_types): @@ -211,7 +213,6 @@ def _line_plot_onselect( unit = "Hz" if psd else time_unit fig.suptitle("Average over %.2f%s - %.2f%s" % (xmin, unit, xmax, unit), y=0.1) - tight_layout(pad=2.0, fig=fig) plt_show() if text is not None: text.set_visible(False) @@ -332,7 +333,7 @@ def _plot_evoked( if axes is None: axes = dict() for sel in group_by: - plt.figure() + plt.figure(layout="constrained") axes[sel] = plt.axes() if not isinstance(axes, dict): raise ValueError( @@ -458,8 +459,7 @@ def _plot_evoked( fig = None if axes is None: - fig, axes = plt.subplots(len(ch_types_used), 1) - fig.subplots_adjust(left=0.125, bottom=0.1, right=0.975, top=0.92, hspace=0.63) + fig, axes = plt.subplots(len(ch_types_used), 1, layout="constrained") if isinstance(axes, plt.Axes): axes = [axes] fig.set_size_inches(6.4, 2 + len(axes)) @@ -738,6 +738,7 @@ def _plot_lines( else: y_offset = this_ylim[0] this_gfp += y_offset + ax.autoscale(False) ax.fill_between( times, y_offset, @@ -1628,7 +1629,7 @@ def whitened_gfp(x, rank=None): sharex=True, sharey=False, figsize=(8.8, 2.2 * n_rows), - constrained_layout=True, + layout="constrained", ) else: axes = np.array(axes) @@ -1772,7 +1773,7 @@ def plot_snr_estimate(evoked, inv, show=True, axes=None, verbose=None): snr, snr_est = estimate_snr(evoked, inv) _validate_type(axes, (None, plt.Axes)) if axes is None: - _, ax = plt.subplots(1, 1) + _, ax = plt.subplots(1, 1, layout="constrained") else: ax = axes del axes @@ -1858,7 +1859,7 @@ def plot_evoked_joint( ----- .. versionadded:: 0.12.0 """ - import matplotlib.pyplot as plt + from matplotlib.patches import ConnectionPatch if ts_args is not None and not isinstance(ts_args, dict): raise TypeError("ts_args must be dict or None, got type %s" % (type(ts_args),)) @@ -1955,9 +1956,8 @@ def plot_evoked_joint( # prepare axes for topomap if not got_axes: - fig, ts_ax, map_ax, cbar_ax = _prepare_joint_axes( - len(times_sec), figsize=(8.0, 4.2) - ) + fig, ts_ax, map_ax = _prepare_joint_axes(len(times_sec), figsize=(8.0, 4.2)) + cbar_ax = None else: ts_ax = ts_args["axes"] del ts_args["axes"] @@ -1995,20 +1995,10 @@ def plot_evoked_joint( old_title = ts_ax.get_title() ts_ax.set_title("") - # XXX BUG destroys ax -> fig assignment if title & axes are passed if title is not None: - title_ax = fig.add_subplot(4, 3, 2) if title == "": title = old_title - title_ax.text( - 0.5, - 0.5, - title, - transform=title_ax.transAxes, - horizontalalignment="center", - verticalalignment="center", - ) - title_ax.axis("off") + fig.suptitle(title) # topomap contours = topomap_args.get("contours", 6) @@ -2034,8 +2024,8 @@ def plot_evoked_joint( if topomap_args.get("colorbar", True): from matplotlib import ticker - cbar_ax.grid(False) # auto-removal deprecated as of 2021/10/05 - cbar = plt.colorbar(map_ax[0].images[0], cax=cbar_ax) + cbar = fig.colorbar(map_ax[0].images[0], ax=map_ax, cax=cbar_ax) + cbar.ax.grid(False) # auto-removal deprecated as of 2021/10/05 if isinstance(contours, (list, np.ndarray)): cbar.set_ticks(contours) else: @@ -2044,19 +2034,24 @@ def plot_evoked_joint( cbar.locator = locator cbar.update_ticks() - if not got_axes: - plt.subplots_adjust( - left=0.1, right=0.93, bottom=0.14, top=1.0 if title is not None else 1.2 - ) - # connection lines # draw the connection lines between time series and topoplots - lines = [ - _connection_line(timepoint, fig, ts_ax, map_ax_) - for timepoint, map_ax_ in zip(times_ts, map_ax) - ] - for line in lines: - fig.lines.append(line) + for timepoint, map_ax_ in zip(times_ts, map_ax): + con = ConnectionPatch( + xyA=[timepoint, ts_ax.get_ylim()[1]], + xyB=[0.5, 0], + coordsA="data", + coordsB="axes fraction", + axesA=ts_ax, + axesB=map_ax_, + color="grey", + linestyle="-", + linewidth=1.5, + alpha=0.66, + zorder=1, + clip_on=False, + ) + fig.add_artist(con) # mark times in time series plot for timepoint in times_ts: @@ -2941,7 +2936,9 @@ def plot_compare_evokeds( axes = ["topo"] * len(ch_types) else: if axes is None: - axes = (plt.subplots(figsize=(8, 6))[1] for _ in ch_types) + axes = ( + plt.subplots(figsize=(8, 6), layout="constrained")[1] for _ in ch_types + ) elif isinstance(axes, plt.Axes): axes = [axes] _validate_if_list_of_axes(axes, obligatory_len=len(ch_types)) @@ -3015,7 +3012,7 @@ def plot_compare_evokeds( from .topo import iter_topography from ..channels.layout import find_layout - fig = plt.figure(figsize=(18, 14)) + fig = plt.figure(figsize=(18, 14), layout=None) # Not "constrained" for topo def click_func( ax_, diff --git a/mne/viz/ica.py b/mne/viz/ica.py index a414775b635..d80ed9aec65 100644 --- a/mne/viz/ica.py +++ b/mne/viz/ica.py @@ -14,7 +14,6 @@ from scipy.stats import gaussian_kde from .utils import ( - tight_layout, _make_event_color_dict, _get_cmap, plt_show, @@ -767,7 +766,7 @@ def _plot_ica_sources_evoked(evoked, picks, exclude, title, show, ica, labels=No if title is None: title = "Reconstructed latent sources, time-locked" - fig, axes = plt.subplots(1) + fig, axes = plt.subplots(1, layout="constrained") ax = axes axes = [axes] times = evoked.times * 1e3 @@ -852,7 +851,6 @@ def _plot_ica_sources_evoked(evoked, picks, exclude, title, show, ica, labels=No ax.set(title=title, xlim=times[[0, -1]], xlabel="Time (ms)", ylabel="(NA)") if len(exclude) > 0: plt.legend(loc="best") - tight_layout(fig=fig) texts.append( ax.text( @@ -959,7 +957,9 @@ def plot_ica_scores( if figsize is None: figsize = (6.4 * n_cols, 2.7 * n_rows) - fig, axes = plt.subplots(n_rows, n_cols, figsize=figsize, sharex=True, sharey=True) + fig, axes = plt.subplots( + n_rows, n_cols, figsize=figsize, sharex=True, sharey=True, layout="constrained" + ) if isinstance(axes, np.ndarray): axes = axes.flatten() @@ -1012,11 +1012,6 @@ def plot_ica_scores( ax.set_title("(%s)" % label) ax.set_xlabel("ICA components") ax.set_xlim(-0.6, len(this_scores) - 0.4) - - tight_layout(fig=fig) - - adjust_top = 0.8 if len(fig.axes) == 1 else 0.9 - fig.subplots_adjust(top=adjust_top) fig.canvas.draw() plt_show(show) return fig @@ -1159,13 +1154,13 @@ def _plot_ica_overlay_raw(*, raw, raw_cln, picks, start, stop, title, show): ch_types = raw.get_channel_types(picks=picks, unique=True) for ch_type in ch_types: if ch_type in ("mag", "grad"): - fig, ax = plt.subplots(3, 1, sharex=True, constrained_layout=True) + fig, ax = plt.subplots(3, 1, sharex=True, layout="constrained") elif ch_type == "eeg" and not _has_eeg_average_ref_proj( raw.info, check_active=True ): - fig, ax = plt.subplots(3, 1, sharex=True, constrained_layout=True) + fig, ax = plt.subplots(3, 1, sharex=True, layout="constrained") else: - fig, ax = plt.subplots(2, 1, sharex=True, constrained_layout=True) + fig, ax = plt.subplots(2, 1, sharex=True, layout="constrained") fig.suptitle(title) # select sensors and retrieve data array @@ -1236,7 +1231,7 @@ def _plot_ica_overlay_evoked(evoked, evoked_cln, title, show): if len(ch_types_used) != len(ch_types_used_cln): raise ValueError("Raw and clean evokeds must match. Found different channels.") - fig, axes = plt.subplots(n_rows, 1) + fig, axes = plt.subplots(n_rows, 1, layout="constrained") if title is None: title = "Average signal before (red) and after (black) ICA" fig.suptitle(title) @@ -1248,9 +1243,6 @@ def _plot_ica_overlay_evoked(evoked, evoked_cln, title, show): line.set_color("r") fig.canvas.draw() evoked_cln.plot(axes=axes, show=False, time_unit="s", spatial_colors=False) - tight_layout(fig=fig) - - fig.subplots_adjust(top=0.90) fig.canvas.draw() plt_show(show) return fig diff --git a/mne/viz/misc.py b/mne/viz/misc.py index d2c1a4242dc..c903244f9ff 100644 --- a/mne/viz/misc.py +++ b/mne/viz/misc.py @@ -50,7 +50,6 @@ ) from ..filter import estimate_ringing_samples from .utils import ( - tight_layout, _get_color_list, _prepare_trellis, plt_show, @@ -172,7 +171,11 @@ def plot_cov( C = np.sqrt((C * C.conj()).real) fig_cov, axes = plt.subplots( - 1, len(idx_names), squeeze=False, figsize=(3.8 * len(idx_names), 3.7) + 1, + len(idx_names), + squeeze=False, + figsize=(3.8 * len(idx_names), 3.7), + layout="constrained", ) for k, (idx, name, _, _, _) in enumerate(idx_names): vlim = np.max(np.abs(C[idx][:, idx])) @@ -192,13 +195,14 @@ def plot_cov( cax.grid(False) # avoid mpl warning about auto-removal plt.colorbar(im, cax=cax, format="%.0e") - fig_cov.subplots_adjust(0.04, 0.0, 0.98, 0.94, 0.2, 0.26) - tight_layout(fig=fig_cov) - fig_svd = None if show_svd: fig_svd, axes = plt.subplots( - 1, len(idx_names), squeeze=False, figsize=(3.8 * len(idx_names), 3.7) + 1, + len(idx_names), + squeeze=False, + figsize=(3.8 * len(idx_names), 3.7), + layout="constrained", ) for k, (idx, name, unit, scaling, key) in enumerate(idx_names): this_C = C[idx][:, idx] @@ -233,10 +237,8 @@ def plot_cov( title=name, xlim=[0, len(s) - 1], ) - tight_layout(fig=fig_svd) plt_show(show) - return fig_cov, fig_svd @@ -321,7 +323,7 @@ def plot_source_spectrogram( time_grid, freq_grid = np.meshgrid(time_bounds, freq_bounds) # Plotting the results - fig = plt.figure(figsize=(9, 6)) + fig = plt.figure(figsize=(9, 6), layout="constrained") plt.pcolor(time_grid, freq_grid, source_power[:, source_index, :], cmap="Reds") ax = plt.gca() @@ -344,7 +346,6 @@ def plot_source_spectrogram( plt.grid(True, ls="-") if colorbar: plt.colorbar() - tight_layout(fig=fig) # Covering frequency gaps with horizontal bars for lower_bound, upper_bound in gap_bounds: @@ -481,6 +482,8 @@ def _plot_mri_contours( if slices_as_subplots: ax = axs[ai] else: + # No need for constrained layout here because we make our axes fill the + # entire figure fig = _figure_agg(figsize=figsize, dpi=dpi, facecolor="k") ax = fig.add_axes([0, 0, 1, 1], frame_on=False, facecolor="k") @@ -588,9 +591,6 @@ def _plot_mri_contours( figs.append(fig) if slices_as_subplots: - fig.subplots_adjust( - left=0.0, bottom=0.0, right=1.0, top=1.0, wspace=0.0, hspace=0.0 - ) plt_show(show, fig=fig) return fig else: @@ -848,7 +848,7 @@ def plot_events( fig = None if axes is None: - fig = plt.figure() + fig = plt.figure(layout="constrained") ax = axes if axes else plt.gca() unique_events_id = np.array(unique_events_id) @@ -948,7 +948,7 @@ def plot_dipole_amplitudes(dipoles, colors=None, show=True): if colors is None: colors = cycle(_get_color_list()) - fig, ax = plt.subplots(1, 1) + fig, ax = plt.subplots(1, 1, layout="constrained") xlim = [np.inf, -np.inf] for dip, color in zip(dipoles, colors): ax.plot(dip.times, dip.amplitude * 1e9, color=color, linewidth=1.5) @@ -1191,7 +1191,7 @@ def plot_filter( fig = None if axes is None: - fig, axes = plt.subplots(len(plot), 1) + fig, axes = plt.subplots(len(plot), 1, layout="constrained") if isinstance(axes, plt.Axes): axes = [axes] elif isinstance(axes, np.ndarray): @@ -1263,7 +1263,6 @@ def plot_filter( ) adjust_axes(axes) - tight_layout() plt_show(show) return fig @@ -1357,7 +1356,7 @@ def plot_ideal_filter( my_gain.append(gain[ii]) my_gain = 10 * np.log10(np.maximum(my_gain, 10 ** (alim[0] / 10.0))) if axes is None: - axes = plt.subplots(1)[1] + axes = plt.subplots(1, layout="constrained")[1] for transition in transitions: axes.axvspan(*transition, color=color, alpha=0.1) axes.plot( @@ -1378,7 +1377,6 @@ def plot_ideal_filter( if title: axes.set(title=title) adjust_axes(axes) - tight_layout() plt_show(show) return axes.figure @@ -1508,7 +1506,11 @@ def plot_csd( continue fig, axes = plt.subplots( - n_rows, n_cols, squeeze=False, figsize=(2 * n_cols + 1, 2.2 * n_rows) + n_rows, + n_cols, + squeeze=False, + figsize=(2 * n_cols + 1, 2.2 * n_rows), + layout="constrained", ) csd_mats = [] @@ -1535,8 +1537,6 @@ def plot_csd( ax.set_title("%.1f Hz." % freq) plt.suptitle(title) - plt.subplots_adjust(top=0.8) - if colorbar: cb = plt.colorbar(im, ax=[a for ax_ in axes for a in ax_]) if mode == "csd": @@ -1580,9 +1580,7 @@ def plot_chpi_snr(snr_dict, axes=None): ----- If you supply a list of existing `~matplotlib.axes.Axes`, then the figure legend will not be drawn automatically. If you still want it, running - ``fig.legend(loc='right', title='cHPI frequencies')`` will recreate it, - though you may also need to manually adjust the margin to make room for it - (e.g., using ``fig.subplots_adjust(right=0.8)``). + ``fig.legend(loc='right', title='cHPI frequencies')`` will recreate it. .. versionadded:: 0.24 """ @@ -1593,7 +1591,7 @@ def plot_chpi_snr(snr_dict, axes=None): full_names = dict(mag="magnetometers", grad="gradiometers") axes_was_none = axes is None if axes_was_none: - fig, axes = plt.subplots(len(valid_keys), 1, sharex=True) + fig, axes = plt.subplots(len(valid_keys), 1, sharex=True, layout="constrained") else: fig = axes[0].get_figure() if len(axes) != len(valid_keys): @@ -1627,6 +1625,5 @@ def plot_chpi_snr(snr_dict, axes=None): if axes_was_none: ax.set(xlabel="Time (s)") fig.align_ylabels() - fig.subplots_adjust(left=0.1, right=0.825, bottom=0.075, top=0.95, hspace=0.7) fig.legend(loc="right", title="cHPI frequencies") return fig diff --git a/mne/viz/tests/test_epochs.py b/mne/viz/tests/test_epochs.py index 711afdea480..bfe5d07eebf 100644 --- a/mne/viz/tests/test_epochs.py +++ b/mne/viz/tests/test_epochs.py @@ -272,14 +272,7 @@ def test_plot_epochs_nodata(browser_backend): @pytest.mark.slowtest def test_plot_epochs_image(epochs): - """Test plotting of epochs image. - - Note that some of these tests that should pass are triggering MPL - UserWarnings about tight_layout not being applied ("tight_layout cannot - make axes width small enough to accommodate all axes decorations"). Calling - `plt.close('all')` just before the offending test seems to prevent this - warning, though it's unclear why. - """ + """Test plotting of epochs image.""" figs = epochs.plot_image() assert len(figs) == 2 # one fig per ch_type (test data has mag, grad) assert len(plt.get_fignums()) == 2 diff --git a/mne/viz/tests/test_evoked.py b/mne/viz/tests/test_evoked.py index ce67febd0a9..644b2fb4e3e 100644 --- a/mne/viz/tests/test_evoked.py +++ b/mne/viz/tests/test_evoked.py @@ -231,7 +231,7 @@ def test_plot_evoked(): def test_constrained_layout(): """Test that we handle constrained layouts correctly.""" - fig, ax = plt.subplots(1, 1, constrained_layout=True) + fig, ax = plt.subplots(1, 1, layout="constrained") assert fig.get_constrained_layout() evoked = mne.read_evokeds(evoked_fname)[0] evoked.pick(evoked.ch_names[:2]) @@ -612,7 +612,7 @@ def test_plot_ctf(): fig = plt.figure() # create custom axes for topomaps, colorbar and the timeseries - gs = gridspec.GridSpec(3, 7, hspace=0.5, top=0.8) + gs = gridspec.GridSpec(3, 7, hspace=0.5, top=0.8, figure=fig) topo_axes = [ fig.add_subplot(gs[0, idx * 2 : (idx + 1) * 2]) for idx in range(len(times)) ] diff --git a/mne/viz/tests/test_topomap.py b/mne/viz/tests/test_topomap.py index 4f95f586d98..e20b1987dd1 100644 --- a/mne/viz/tests/test_topomap.py +++ b/mne/viz/tests/test_topomap.py @@ -75,8 +75,8 @@ fast_test = dict(res=8, contours=0, sensors=False) -@pytest.mark.parametrize("constrained_layout", (False, True)) -def test_plot_topomap_interactive(constrained_layout): +@pytest.mark.parametrize("layout", (None, "constrained")) +def test_plot_topomap_interactive(layout): """Test interactive topomap projection plotting.""" evoked = read_evokeds(evoked_fname, baseline=(None, 0))[0] evoked.pick(picks="mag") @@ -86,7 +86,7 @@ def test_plot_topomap_interactive(constrained_layout): evoked.add_proj(compute_proj_evoked(evoked, n_mag=1)) plt.close("all") - fig, ax = plt.subplots(constrained_layout=constrained_layout) + fig, ax = plt.subplots(layout=layout) canvas = fig.canvas kwargs = dict( diff --git a/mne/viz/topo.py b/mne/viz/topo.py index 683c22d9a6a..5a832c954a3 100644 --- a/mne/viz/topo.py +++ b/mne/viz/topo.py @@ -145,7 +145,8 @@ def _iter_topography( from ..channels.layout import find_layout if fig is None: - fig = plt.figure() + # Don't use constrained layout because we place axes manually + fig = plt.figure(layout=None) def format_coord_unified(x, y, pos=None, ch_names=None): """Update status bar with channel name under cursor.""" @@ -296,7 +297,8 @@ def _plot_topo( ) if axes is None: - fig = plt.figure() + # Don't use constrained layout because we place axes manually + fig = plt.figure(layout=None) axes = plt.axes([0.015, 0.025, 0.97, 0.95]) axes.set_facecolor(fig_facecolor) else: diff --git a/mne/viz/topomap.py b/mne/viz/topomap.py index d47ec145e07..a90400c6421 100644 --- a/mne/viz/topomap.py +++ b/mne/viz/topomap.py @@ -54,7 +54,6 @@ ) from ..utils.spectrum import _split_psd_kwargs from .utils import ( - tight_layout, _setup_vmin_vmax, _prepare_trellis, _check_delayed_ssp, @@ -301,8 +300,8 @@ def _add_colorbar( ax, im, cmap, + *, side="right", - pad=0.05, title=None, format=None, size="5%", @@ -310,14 +309,10 @@ def _add_colorbar( ch_type=None, ): """Add a colorbar to an axis.""" - import matplotlib.pyplot as plt - from mpl_toolkits.axes_grid1 import make_axes_locatable - - divider = make_axes_locatable(ax) - cax = divider.append_axes(side, size=size, pad=pad) - cbar = plt.colorbar(im, cax=cax, format=format) + cbar = ax.figure.colorbar(im, format=format) if cmap is not None and cmap[1]: ax.CB = DraggableColorbar(cbar, im, kind, ch_type) + cax = cbar.ax if title is not None: cax.set_title(title, y=1.05, fontsize=10) return cbar, cax @@ -450,7 +445,6 @@ def plot_projs_topomap( ) with warnings.catch_warnings(record=True): warnings.simplefilter("ignore") - tight_layout(fig=fig) plt_show(show) return fig @@ -1020,7 +1014,7 @@ def plot_topomap( from matplotlib.colors import Normalize if axes is None: - _, axes = plt.subplots(figsize=(size, size)) + _, axes = plt.subplots(figsize=(size, size), layout="constrained") sphere = _check_sphere(sphere, pos if isinstance(pos, Info) else None) _validate_type(cnorm, (Normalize, None), "cnorm") if cnorm is not None and (vlim[0] is not None or vlim[1] is not None): @@ -1379,9 +1373,6 @@ def _plot_topomap( size="x-small", ) - if not axes.figure.get_constrained_layout(): - axes.figure.subplots_adjust(top=0.95) - if onselect is not None: lim = axes.dataLim x0, y0, width, height = lim.x0, lim.y0, lim.width, lim.height @@ -1475,7 +1466,6 @@ def _plot_ica_topomap( axes, im, cmap, - pad=0.05, title="AU", format="%3.2f", kind="ica_topomap", @@ -1716,7 +1706,6 @@ def plot_ica_components( cmap, title="AU", side="right", - pad=0.05, format=cbar_fmt, kind="ica_comp_topomap", ch_type=ch_type, @@ -1725,9 +1714,6 @@ def plot_ica_components( cbar.set_ticks(_vlim) _hide_frame(ax) del pos - if not user_passed_axes: - tight_layout(fig=fig) - fig.subplots_adjust(top=0.88, bottom=0.0) fig.canvas.draw() # add title selection interactivity @@ -1934,7 +1920,11 @@ def plot_tfr_topomap( vlim = _setup_vmin_vmax(data, *vlim, norm) cmap = _setup_cmap(cmap, norm=norm) - axes = plt.subplots(figsize=(size, size))[1] if axes is None else axes + axes = ( + plt.subplots(figsize=(size, size), layout="constrained")[1] + if axes is None + else axes + ) fig = axes.figure _hide_frame(axes) @@ -2204,18 +2194,17 @@ def plot_evoked_topomap( if interactive: height_ratios = [5, 1] nrows = 2 - ncols = want_axes - width = size * ncols + ncols = n_times + width = size * want_axes height = size + max(0, 0.1 * (4 - size)) fig = figure_nobar(figsize=(width * 1.5, height * 1.5)) - g_kwargs = {"left": 0.2, "right": 0.8, "bottom": 0.05, "top": 0.9} - gs = GridSpec(nrows, ncols, height_ratios=height_ratios, **g_kwargs) + gs = GridSpec(nrows, ncols, height_ratios=height_ratios, figure=fig) axes = [] for ax_idx in range(n_times): axes.append(plt.subplot(gs[0, ax_idx])) elif axes is None: fig, axes, ncols, nrows = _prepare_trellis( - n_times, ncols=ncols, nrows=nrows, colorbar=colorbar, size=size + n_times, ncols=ncols, nrows=nrows, size=size ) else: nrows, ncols = None, None # Deactivate ncols when axes were passed @@ -2227,13 +2216,7 @@ def plot_evoked_topomap( f"You must provide {want_axes} axes (one for " f"each time{cbar_err}), got {len(axes)}." ) - # figure margins - if not fig.get_constrained_layout(): - side_margin = plt.rcParams["figure.subplot.wspace"] / (2 * want_axes) - top_margin = max(0.05, 0.2 / size) - fig.subplots_adjust( - left=side_margin, right=1 - side_margin, bottom=0, top=1 - top_margin - ) + del want_axes # find first index that's >= (to rounding error) to each time point time_idx = [ np.where( @@ -2336,12 +2319,10 @@ def plot_evoked_topomap( images, contours_ = [], [] # loop over times for average_idx, (time, this_average) in enumerate(zip(times, average)): - adjust_for_cbar = colorbar and ncols is not None and average_idx >= ncols - 1 - ax_idx = average_idx + 1 if adjust_for_cbar else average_idx tp, cn, interp = _plot_topomap( data[:, average_idx], pos, - axes=axes[ax_idx], + axes=axes[average_idx], mask=mask_[:, average_idx] if mask is not None else None, vmin=_vlim[0], vmax=_vlim[1], @@ -2362,13 +2343,13 @@ def plot_evoked_topomap( to_time = time_format % (tmax_ * scaling_time) axes_title = f"{from_time} – {to_time}" del from_time, to_time, tmin_, tmax_ - axes[ax_idx].set_title(axes_title) + axes[average_idx].set_title(axes_title) if interactive: # Add a slider to the figure and start publishing and subscribing to time_change # events. kwargs.update(vlim=_vlim) - axes.append(plt.subplot(gs[1, :-1])) + axes.append(fig.add_subplot(gs[1])) slider = Slider( axes[-1], "Time", @@ -2412,19 +2393,15 @@ def _slider_changed(val): ) if colorbar: - if interactive: - cax = plt.subplot(gs[0, -1]) - _resize_cbar(cax, ncols, size) - elif nrows is None or ncols is None: + if nrows is None or ncols is None: # axes were given by the user, so don't resize the colorbar cax = axes[-1] - else: # use the entire last column - cax = axes[ncols - 1] - _resize_cbar(cax, ncols, size) + else: # use the default behavior + cax = None + cbar = fig.colorbar(images[-1], ax=axes, cax=cax, format=cbar_fmt, shrink=0.6) if unit is not None: - cax.set_title(unit) - cbar = fig.colorbar(images[-1], ax=cax, cax=cax, format=cbar_fmt) + cbar.ax.set_title(unit) if cn is not None: cbar.set_ticks(contours) cbar.ax.tick_params(labelsize=7) @@ -2578,9 +2555,7 @@ def _plot_topomap_multi_cbar( ) if colorbar: - cbar, cax = _add_colorbar( - ax, im, cmap, pad=0.25, title=None, size="10%", format=cbar_fmt - ) + cbar, cax = _add_colorbar(ax, im, cmap, title=None, size="10%", format=cbar_fmt) cbar.set_ticks(_vlim) if unit is not None: cbar.ax.set_ylabel(unit, fontsize=8) @@ -2857,7 +2832,9 @@ def plot_psds_topomap( _validate_if_list_of_axes(axes, n_axes) fig = axes[0].figure else: - fig, axes = plt.subplots(1, n_axes, figsize=(2 * n_axes, 1.5)) + fig, axes = plt.subplots( + 1, n_axes, figsize=(2 * n_axes, 1.5), layout="constrained" + ) if n_axes == 1: axes = [axes] # loop over subplots/frequency bands @@ -2892,7 +2869,6 @@ def plot_psds_topomap( ) if not user_passed_axes: - tight_layout(fig=fig) fig.canvas.draw() plt_show(show) return fig @@ -2923,9 +2899,10 @@ def plot_layout(layout, picks=None, show_axes=False, show=True): """ import matplotlib.pyplot as plt - fig = plt.figure(figsize=(max(plt.rcParams["figure.figsize"]),) * 2) + fig = plt.figure( + figsize=(max(plt.rcParams["figure.figsize"]),) * 2, layout="constrained" + ) ax = fig.add_subplot(111) - fig.subplots_adjust(left=0, bottom=0, right=1, top=1, wspace=None, hspace=None) ax.set(xticks=[], yticks=[], aspect="equal") outlines = dict(border=([0, 1, 1, 0, 0], [0, 0, 1, 1, 0])) _draw_outlines(ax, outlines) @@ -2945,7 +2922,6 @@ def plot_layout(layout, picks=None, show_axes=False, show=True): x1, x2, y1, y2 = p[0], p[0] + p[2], p[1], p[1] + p[3] ax.plot([x1, x1, x2, x2, x1], [y1, y2, y2, y1, y1], color="k") ax.axis("off") - tight_layout(fig=fig, pad=0, w_pad=0, h_pad=0) plt_show(show) return fig @@ -3163,7 +3139,6 @@ def _init_anim( outlines_ = _draw_outlines(ax, outlines) params.update({"patch": patch_, "outlines": outlines_}) - tight_layout(fig=ax.figure) return tuple(items) + cont_collections @@ -3306,7 +3281,7 @@ def _topomap_animation( norm = np.min(data) >= 0 vmin, vmax = _setup_vmin_vmax(data, vmin, vmax, norm) - fig = plt.figure(figsize=(6, 5)) + fig = plt.figure(figsize=(6, 5), layout="constrained") shape = (8, 12) colspan = shape[1] - 1 rowspan = shape[0] - bool(butterfly) @@ -3491,8 +3466,6 @@ def _plot_corrmap( border=border, ) _hide_frame(ax) - tight_layout(fig=fig) - fig.subplots_adjust(top=0.8) fig.canvas.draw() plt_show(show) return fig @@ -3652,7 +3625,7 @@ def plot_arrowmap( ) outlines = _make_head_outlines(sphere, pos, outlines, clip_origin) if axes is None: - fig, axes = plt.subplots() + fig, axes = plt.subplots(layout="constrained") else: fig = axes.figure plot_topomap( @@ -3679,11 +3652,7 @@ def plot_arrowmap( dx, dy = _trigradient(x, y, data) dxx = dy.data dyy = -dx.data - axes.quiver(x, y, dxx, dyy, scale=scale, color="k", lw=1, clip_on=False) - axes.figure.canvas.draw_idle() - with warnings.catch_warnings(record=True): - warnings.simplefilter("ignore") - tight_layout(fig=fig) + axes.quiver(x, y, dxx, dyy, scale=scale, color="k", lw=1) plt_show(show) return fig @@ -3735,7 +3704,7 @@ def plot_bridged_electrodes( topomap_args.setdefault("contours", False) sphere = topomap_args.get("sphere", _check_sphere(None)) if "axes" not in topomap_args: - fig, ax = plt.subplots() + fig, ax = plt.subplots(layout="constrained") topomap_args["axes"] = ax else: fig = None @@ -4075,7 +4044,11 @@ def plot_regression_weights( axes_was_none = axes is None if axes_was_none: fig, axes = plt.subplots( - nrows, ncols, squeeze=False, figsize=(ncols * 2, nrows * 1.5 + 1) + nrows, + ncols, + squeeze=False, + figsize=(ncols * 2, nrows * 1.5 + 1), + layout="constrained", ) axes = axes.T.ravel() else: @@ -4143,8 +4116,5 @@ def plot_regression_weights( ) if axes_was_none: fig.suptitle(title) - fig.subplots_adjust( - top=0.88, bottom=0.06, left=0.025, right=0.911, hspace=0.2, wspace=0.5 - ) plt_show(show) return fig diff --git a/mne/viz/utils.py b/mne/viz/utils.py index 78f05ee9109..08d4e69ec48 100644 --- a/mne/viz/utils.py +++ b/mne/viz/utils.py @@ -21,7 +21,6 @@ import sys import tempfile import traceback -import warnings import webbrowser from decorator import decorator @@ -203,63 +202,6 @@ def _show_browser(show=True, block=True, fig=None, **kwargs): _qt_app_exec(QApplication.instance()) -def tight_layout(pad=1.2, h_pad=None, w_pad=None, fig=None): - """Adjust subplot parameters to give specified padding. - - .. note:: For plotting please use this function instead of - ``plt.tight_layout``. - - Parameters - ---------- - pad : float - Padding between the figure edge and the edges of subplots, as a - fraction of the font-size. - h_pad : float - Padding height between edges of adjacent subplots. - Defaults to ``pad_inches``. - w_pad : float - Padding width between edges of adjacent subplots. - Defaults to ``pad_inches``. - fig : instance of Figure - Figure to apply changes to. - - Notes - ----- - This will not force constrained_layout=False if the figure was created - with that method. - """ - _validate_type(pad, "numeric", "pad") - import matplotlib.pyplot as plt - - fig = plt.gcf() if fig is None else fig - - fig.canvas.draw() - constrained = fig.get_constrained_layout() - kwargs = dict(pad=pad, h_pad=h_pad, w_pad=w_pad) - if constrained: - return # no-op - try: # see https://github.com/matplotlib/matplotlib/issues/2654 - with warnings.catch_warnings(record=True) as ws: - fig.tight_layout(**kwargs) - except Exception: - try: - with warnings.catch_warnings(record=True) as ws: - if hasattr(fig, "set_layout_engine"): - fig.set_layout_engine("tight", **kwargs) - else: - fig.set_tight_layout(kwargs) - except Exception: - warn( - 'Matplotlib function "tight_layout" is not supported.' - " Skipping subplot adjustment." - ) - return - for w in ws: - w_msg = str(w.message) if hasattr(w, "message") else w.get_message() - if not w_msg.startswith("This figure includes Axes"): - warn(w_msg, w.category, "matplotlib") - - def _check_delayed_ssp(container): """Handle interactive SSP selection.""" if container.proj is True or all(p["active"] for p in container.info["projs"]): @@ -489,7 +431,6 @@ def _prepare_trellis( ncols, nrows="auto", title=False, - colorbar=False, size=1.3, sharex=False, sharey=False, @@ -517,22 +458,13 @@ def _prepare_trellis( "figure.".format(n_cells, nrows, ncols) ) - if colorbar: - ncols += 1 width = size * ncols height = (size + max(0, 0.1 * (4 - size))) * nrows + bool(title) * 0.5 - height_ratios = None fig = _figure(toolbar=False, figsize=(width * 1.5, 0.25 + height * 1.5)) - gs = GridSpec(nrows, ncols, figure=fig, height_ratios=height_ratios) + gs = GridSpec(nrows, ncols, figure=fig) axes = [] - if colorbar: - # exclude last axis of each row except top row, which is for colorbar - exclude = set(range(2 * ncols - 1, nrows * ncols, ncols)) - ax_idxs = sorted(set(range(nrows * ncols)) - exclude)[: n_cells + 1] - else: - ax_idxs = range(n_cells) - for ax_idx in ax_idxs: + for ax_idx in range(n_cells): subplot_kw = dict() if ax_idx > 0: if sharex: @@ -560,7 +492,8 @@ def _draw_proj_checkbox(event, params, draw_current_state=True): width = max([4.0, max([len(p["desc"]) for p in projs]) / 6.0 + 0.5]) height = (len(projs) + 1) / 6.0 + 1.5 - fig_proj = figure_nobar(figsize=(width, height)) + # We manually place everything here so avoid constrained layouts + fig_proj = figure_nobar(figsize=(width, height), layout=None) _set_window_title(fig_proj, "SSP projection vectors") offset = 1.0 / 6.0 / height params["fig_proj"] = fig_proj # necessary for proper toggling @@ -707,6 +640,8 @@ def figure_nobar(*args, **kwargs): old_val = rcParams["toolbar"] try: rcParams["toolbar"] = "none" + if "layout" not in kwargs: + kwargs["layout"] = "constrained" fig = plt.figure(*args, **kwargs) # remove button press catchers (for toolbar) cbs = list(fig.canvas.callbacks.callbacks["key_press_event"].keys()) @@ -1319,7 +1254,10 @@ def _plot_sensors( if kind == "3d": subplot_kw.update(projection="3d") fig, ax = plt.subplots( - 1, figsize=(max(rcParams["figure.figsize"]),) * 2, subplot_kw=subplot_kw + 1, + figsize=(max(rcParams["figure.figsize"]),) * 2, + subplot_kw=subplot_kw, + layout="constrained", ) else: fig = ax.get_figure() @@ -1367,8 +1305,6 @@ def _plot_sensors( # Equal aspect for 3D looks bad, so only use for 2D ax.set(aspect="equal") - if axes_was_none: # we'll show the plot title as the window title - fig.subplots_adjust(left=0, bottom=0, right=1, top=1) ax.axis("off") # remove border around figure del sphere @@ -1393,14 +1329,6 @@ def _plot_sensors( connect_picker = kind == "select" # make sure no names go off the edge of the canvas xmin, ymin, xmax, ymax = fig.get_window_extent().bounds - renderer = fig.canvas.get_renderer() - extents = [x.get_window_extent(renderer=renderer) for x in ax.texts] - xmaxs = np.array([x.max[0] for x in extents]) - bad_xmax_ixs = np.nonzero(xmaxs > xmax)[0] - if len(bad_xmax_ixs): - needed_space = (xmaxs[bad_xmax_ixs] - xmax).max() / xmax - fig.subplots_adjust(right=1 - 1.1 * needed_space) - if connect_picker: picker = partial( _onpick_sensor, @@ -1530,38 +1458,14 @@ def _setup_cmap(cmap, n_axes=1, norm=False): def _prepare_joint_axes(n_maps, figsize=None): - """Prepare axes for topomaps and colorbar in joint plot figure. - - Parameters - ---------- - n_maps: int - Number of topomaps to include in the figure - figsize: tuple - Figure size, see plt.figsize - - Returns - ------- - fig : matplotlib.figure.Figure - Figure with initialized axes - main_ax: matplotlib.axes._subplots.AxesSubplot - Axes in which to put the main plot - map_ax: list - List of axes for each topomap - cbar_ax: matplotlib.axes._subplots.AxesSubplot - Axes for colorbar next to topomaps - """ import matplotlib.pyplot as plt + from matplotlib.gridspec import GridSpec - fig = plt.figure(figsize=figsize) - main_ax = fig.add_subplot(212) - ts = n_maps + 2 - map_ax = [plt.subplot(4, ts, x + 2 + ts) for x in range(n_maps)] - # Position topomap subplots on the second row, starting on the - # second column - cbar_ax = plt.subplot(4, 5 * (ts + 1), 10 * (ts + 1)) - # Position colorbar at the very end of a more finely divided - # second row of subplots - return fig, main_ax, map_ax, cbar_ax + fig = plt.figure(figsize=figsize, layout="constrained") + gs = GridSpec(2, n_maps, height_ratios=[1, 2], figure=fig) + map_ax = [fig.add_subplot(gs[0, x]) for x in range(n_maps)] # first row + main_ax = fig.add_subplot(gs[1, :]) # second row + return fig, main_ax, map_ax class DraggableColorbar: @@ -1908,37 +1812,6 @@ def _merge_annotations(start, stop, description, annotations, current=()): annotations.append(onset, duration, description) -def _connection_line(x, fig, sourceax, targetax, y=1.0, y_source_transform="transAxes"): - """Connect source and target plots with a line. - - Connect source and target plots with a line, such as time series - (source) and topolots (target). Primarily used for plot_joint - functions. - """ - from matplotlib.lines import Line2D - - trans_fig = fig.transFigure - trans_fig_inv = fig.transFigure.inverted() - - xt, yt = trans_fig_inv.transform(targetax.transAxes.transform([0.5, 0.0])) - xs, _ = trans_fig_inv.transform(sourceax.transData.transform([x, 0.0])) - _, ys = trans_fig_inv.transform( - getattr(sourceax, y_source_transform).transform([0.0, y]) - ) - - return Line2D( - (xt, xs), - (yt, ys), - transform=trans_fig, - color="grey", - linestyle="-", - linewidth=1.5, - alpha=0.66, - zorder=1, - clip_on=False, - ) - - class DraggableLine: """Custom matplotlib line for moving around by drag and drop. diff --git a/requirements.txt b/requirements.txt index 39ae2c37815..90944200247 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ # requirements for full MNE-Python functionality (other than raw/epochs export) numpy>=1.15.4 scipy>=1.7.1 -matplotlib>=3.4.3 +matplotlib>=3.5.0 tqdm pooch>=1.5 decorator diff --git a/requirements_base.txt b/requirements_base.txt index 551156522c3..2e6ba6e6c80 100644 --- a/requirements_base.txt +++ b/requirements_base.txt @@ -1,7 +1,7 @@ # requirements for basic MNE-Python functionality numpy>=1.21.2 scipy>=1.7.1 -matplotlib>=3.4.3 +matplotlib>=3.5.0 tqdm pooch>=1.5 decorator diff --git a/tools/github_actions_env_vars.sh b/tools/github_actions_env_vars.sh index a0ab494a9db..ba1cac712a5 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.4.3 pandas=1.3.2 scikit-learn=1.0" >> $GITHUB_ENV + 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 "MNE_IGNORE_WARNINGS_IN_TESTS=true" >> $GITHUB_ENV echo "MNE_SKIP_NETWORK_TESTS=1" >> $GITHUB_ENV elif [[ "$MNE_CI_KIND" == "minimal" ]]; then diff --git a/tutorials/epochs/60_make_fixed_length_epochs.py b/tutorials/epochs/60_make_fixed_length_epochs.py index a3186ca25c2..9a6eace0ab9 100644 --- a/tutorials/epochs/60_make_fixed_length_epochs.py +++ b/tutorials/epochs/60_make_fixed_length_epochs.py @@ -113,13 +113,10 @@ color_lims = np.percentile(np.array(corr_matrices), [5, 95]) titles = ["First 30 Seconds", "Last 30 Seconds"] -fig, axes = plt.subplots(nrows=1, ncols=2) +fig, axes = plt.subplots(nrows=1, ncols=2, layout="constrained") fig.suptitle("Correlation Matrices from First 30 Seconds and Last 30 Seconds") for ci, corr_matrix in enumerate(corr_matrices): ax = axes[ci] mpbl = ax.imshow(corr_matrix, clim=color_lims) ax.set_xlabel(titles[ci]) -fig.subplots_adjust(right=0.8) -cax = fig.add_axes([0.85, 0.2, 0.025, 0.6]) -cbar = fig.colorbar(ax.images[0], cax=cax) -cbar.set_label("Correlation Coefficient") +cbar = fig.colorbar(ax.images[0], label="Correlation Coefficient") diff --git a/tutorials/forward/50_background_freesurfer_mne.py b/tutorials/forward/50_background_freesurfer_mne.py index 5efcc07d0d1..0150088de83 100644 --- a/tutorials/forward/50_background_freesurfer_mne.py +++ b/tutorials/forward/50_background_freesurfer_mne.py @@ -124,7 +124,7 @@ def imshow_mri(data, img, vox, xyz, suptitle): """Show an MRI slice with a voxel annotated.""" i, j, k = vox - fig, ax = plt.subplots(1, figsize=(6, 6)) + fig, ax = plt.subplots(1, figsize=(6, 6), layout="constrained") codes = nibabel.orientations.aff2axcodes(img.affine) # Figure out the title based on the code of this axis ori_slice = dict( @@ -157,7 +157,6 @@ def imshow_mri(data, img, vox, xyz, suptitle): title=f"{title} view: i={i} ({ori_names[codes[0]]}+)", ) fig.suptitle(suptitle) - fig.subplots_adjust(0.1, 0.1, 0.95, 0.85) return fig diff --git a/tutorials/intro/70_report.py b/tutorials/intro/70_report.py index b23c8852694..12f04772ce8 100644 --- a/tutorials/intro/70_report.py +++ b/tutorials/intro/70_report.py @@ -463,7 +463,7 @@ fig_array_rotated = fig_array_rotated.clip(min=0, max=1) # Create the figure - fig, ax = plt.subplots(figsize=(3, 3), constrained_layout=True) + fig, ax = plt.subplots(figsize=(3, 3), layout="constrained") ax.imshow(fig_array_rotated) ax.set_axis_off() diff --git a/tutorials/inverse/20_dipole_fit.py b/tutorials/inverse/20_dipole_fit.py index 89cf81af671..c81c16f3252 100644 --- a/tutorials/inverse/20_dipole_fit.py +++ b/tutorials/inverse/20_dipole_fit.py @@ -100,6 +100,7 @@ ncols=4, figsize=[10.0, 3.4], gridspec_kw=dict(width_ratios=[1, 1, 1, 0.1], top=0.85), + layout="constrained", ) vmin, vmax = -400, 400 # make sure each plot has same colour range @@ -119,7 +120,6 @@ "at {:.0f} ms".format(best_time * 1000.0), fontsize=16, ) -fig.tight_layout() # %% # Estimate the time course of a single dipole with fixed position and diff --git a/tutorials/inverse/60_visualize_stc.py b/tutorials/inverse/60_visualize_stc.py index 01bd0c28a84..3be86643c61 100644 --- a/tutorials/inverse/60_visualize_stc.py +++ b/tutorials/inverse/60_visualize_stc.py @@ -156,7 +156,7 @@ label_tc = stc.extract_label_time_course(fname_aseg, src=src) lidx, tidx = np.unravel_index(np.argmax(label_tc), label_tc.shape) -fig, ax = plt.subplots(1) +fig, ax = plt.subplots(1, layout="constrained") ax.plot(stc.times, label_tc.T, "k", lw=1.0, alpha=0.5) xy = np.array([stc.times[tidx], label_tc[lidx, tidx]]) xytext = xy + [0.01, 1] @@ -164,7 +164,6 @@ ax.set(xlim=stc.times[[0, -1]], xlabel="Time (s)", ylabel="Activation") for key in ("right", "top"): ax.spines[key].set_visible(False) -fig.tight_layout() # %% # We can plot several labels with the most activation in their time course diff --git a/tutorials/inverse/80_brainstorm_phantom_elekta.py b/tutorials/inverse/80_brainstorm_phantom_elekta.py index cca2c3470af..95a2a8e8f59 100644 --- a/tutorials/inverse/80_brainstorm_phantom_elekta.py +++ b/tutorials/inverse/80_brainstorm_phantom_elekta.py @@ -144,7 +144,7 @@ actual_amp = 100.0 # nAm fig, (ax1, ax2, ax3) = plt.subplots( - nrows=3, ncols=1, figsize=(6, 7), constrained_layout=True + nrows=3, ncols=1, figsize=(6, 7), layout="constrained" ) diffs = 1000 * np.sqrt(np.sum((dip.pos - actual_pos) ** 2, axis=-1)) diff --git a/tutorials/machine-learning/30_strf.py b/tutorials/machine-learning/30_strf.py index af0db4d1d20..9cc53a7a2da 100644 --- a/tutorials/machine-learning/30_strf.py +++ b/tutorials/machine-learning/30_strf.py @@ -86,12 +86,10 @@ shading="gouraud", ) -fig, ax = plt.subplots() +fig, ax = plt.subplots(layout="constrained") ax.pcolormesh(delays_sec, freqs, weights, **kwargs) ax.set(title="Simulated STRF", xlabel="Time Lags (s)", ylabel="Frequency (Hz)") plt.setp(ax.get_xticklabels(), rotation=45) -plt.autoscale(tight=True) -mne.viz.tight_layout() # %% # Simulate a neural response @@ -147,7 +145,7 @@ X_plt = scale(np.hstack(X[:2]).T).T y_plt = scale(np.hstack(y[:2])) time = np.arange(X_plt.shape[-1]) / sfreq -_, (ax1, ax2) = plt.subplots(2, 1, figsize=(6, 6), sharex=True) +_, (ax1, ax2) = plt.subplots(2, 1, figsize=(6, 6), sharex=True, layout="constrained") ax1.pcolormesh(time, freqs, X_plt, vmin=0, vmax=4, cmap="Reds", shading="gouraud") ax1.set_title("Input auditory features") ax1.set(ylim=[freqs.min(), freqs.max()], ylabel="Frequency (Hz)") @@ -158,7 +156,6 @@ xlabel="Time (s)", ylabel="Activity (a.u.)", ) -mne.viz.tight_layout() # %% @@ -197,14 +194,19 @@ best_pred = best_mod.predict(X_test)[:, 0] # Plot the original STRF, and the one that we recovered with modeling. -_, (ax1, ax2) = plt.subplots(1, 2, figsize=(6, 3), sharey=True, sharex=True) +_, (ax1, ax2) = plt.subplots( + 1, + 2, + figsize=(6, 3), + sharey=True, + sharex=True, + layout="constrained", +) ax1.pcolormesh(delays_sec, freqs, weights, **kwargs) ax2.pcolormesh(times, rf.feature_names, coefs, **kwargs) ax1.set_title("Original STRF") ax2.set_title("Best Reconstructed STRF") plt.setp([iax.get_xticklabels() for iax in [ax1, ax2]], rotation=45) -plt.autoscale(tight=True) -mne.viz.tight_layout() # Plot the actual response and the predicted response on a held out stimulus time_pred = np.arange(best_pred.shape[0]) / sfreq @@ -213,8 +215,6 @@ ax.plot(time_pred, best_pred, color="r", lw=1) ax.set(title="Original and predicted activity", xlabel="Time (s)") ax.legend(["Original", "Predicted"]) -plt.autoscale(tight=True) -mne.viz.tight_layout() # %% @@ -229,7 +229,7 @@ # in :footcite:`TheunissenEtAl2001,WillmoreSmyth2003,HoldgrafEtAl2016`. # Plot model score for each ridge parameter -fig = plt.figure(figsize=(10, 4)) +fig = plt.figure(figsize=(10, 4), layout="constrained") ax = plt.subplot2grid([2, len(alphas)], [1, 0], 1, len(alphas)) ax.plot(np.arange(len(alphas)), scores, marker="o", color="r") ax.annotate( @@ -244,7 +244,6 @@ ylabel="Score ($R^2$)", xlim=[-0.4, len(alphas) - 0.6], ) -mne.viz.tight_layout() # Plot the STRF of each ridge parameter for ii, (rf, i_alpha) in enumerate(zip(models, alphas)): @@ -252,9 +251,7 @@ ax.pcolormesh(times, rf.feature_names, rf.coef_[0], **kwargs) plt.xticks([], []) plt.yticks([], []) - plt.autoscale(tight=True) fig.suptitle("Model coefficients / scores for many ridge parameters", y=1) -mne.viz.tight_layout() # %% # Using different regularization types @@ -308,7 +305,7 @@ # This matches the "true" receptive field structure and results in a better # model fit. -fig = plt.figure(figsize=(10, 6)) +fig = plt.figure(figsize=(10, 6), layout="constrained") ax = plt.subplot2grid([3, len(alphas)], [2, 0], 1, len(alphas)) ax.plot(np.arange(len(alphas)), scores_lap, marker="o", color="r") ax.plot(np.arange(len(alphas)), scores, marker="o", color="0.5", ls=":") @@ -330,7 +327,6 @@ ylabel="Score ($R^2$)", xlim=[-0.4, len(alphas) - 0.6], ) -mne.viz.tight_layout() # Plot the STRF of each ridge parameter xlim = times[[0, -1]] @@ -346,13 +342,19 @@ if ii == 0: ax.set(ylabel="Ridge") fig.suptitle("Model coefficients / scores for laplacian regularization", y=1) -mne.viz.tight_layout() # %% # Plot the original STRF, and the one that we recovered with modeling. rf = models[ix_best_alpha] rf_lap = models_lap[ix_best_alpha_lap] -_, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(9, 3), sharey=True, sharex=True) +_, (ax1, ax2, ax3) = plt.subplots( + 1, + 3, + figsize=(9, 3), + sharey=True, + sharex=True, + layout="constrained", +) ax1.pcolormesh(delays_sec, freqs, weights, **kwargs) ax2.pcolormesh(times, rf.feature_names, rf.coef_[0], **kwargs) ax3.pcolormesh(times, rf_lap.feature_names, rf_lap.coef_[0], **kwargs) @@ -360,8 +362,6 @@ ax2.set_title("Best Ridge STRF") ax3.set_title("Best Laplacian STRF") plt.setp([iax.get_xticklabels() for iax in [ax1, ax2, ax3]], rotation=45) -plt.autoscale(tight=True) -mne.viz.tight_layout() # %% # References diff --git a/tutorials/preprocessing/25_background_filtering.py b/tutorials/preprocessing/25_background_filtering.py index a5ec433ac7c..09e5db8173e 100644 --- a/tutorials/preprocessing/25_background_filtering.py +++ b/tutorials/preprocessing/25_background_filtering.py @@ -478,7 +478,7 @@ # and the time-domain ringing is thus more pronounced for the steep-slope, # long-duration filter than the shorter, shallower-slope filter: -axes = plt.subplots(1, 2)[1] +axes = plt.subplots(1, 2, layout="constrained")[1] def plot_signal(x, offset): @@ -524,7 +524,6 @@ def plot_signal(x, offset): for text in axes[0].get_yticklabels(): text.set(rotation=45, size=8) axes[1].set(xlim=flim, ylim=(-60, 10), xlabel="Frequency (Hz)", ylabel="Magnitude (dB)") -mne.viz.tight_layout() plt.show() # %% @@ -665,7 +664,7 @@ def plot_signal(x, offset): # Now let's look at how our shallow and steep Butterworth IIR filters # perform on our Morlet signal from before: -axes = plt.subplots(1, 2)[1] +axes = plt.subplots(1, 2, layout="constrained")[1] yticks = np.arange(4) / -30.0 yticklabels = ["Original", "Noisy", "Butterworth-2", "Butterworth-8"] plot_signal(x_orig, offset=yticks[0]) @@ -684,7 +683,6 @@ def plot_signal(x, offset): text.set(rotation=45, size=8) axes[1].set(xlim=flim, ylim=(-60, 10), xlabel="Frequency (Hz)", ylabel="Magnitude (dB)") mne.viz.adjust_axes(axes) -mne.viz.tight_layout() plt.show() # %% @@ -793,7 +791,6 @@ def plot_signal(x, offset): ) mne.viz.adjust_axes(axes) -mne.viz.tight_layout() plt.show() # %% @@ -832,7 +829,7 @@ def plot_signal(x, offset): def baseline_plot(x): - all_axes = plt.subplots(3, 2)[1] + all_axes = plt.subplots(3, 2, layout="constrained")[1] for ri, (axes, freq) in enumerate(zip(all_axes, [0.1, 0.3, 0.5])): for ci, ax in enumerate(axes): if ci == 0: @@ -849,7 +846,6 @@ def baseline_plot(x): ax.set(xticks=tticks, ylim=ylim, xlim=xlim, xlabel=xlabel) ax.set_ylabel("%0.1f Hz" % freq, rotation=0, horizontalalignment="right") mne.viz.adjust_axes(axes) - mne.viz.tight_layout() plt.suptitle(title) plt.show() diff --git a/tutorials/preprocessing/30_filtering_resampling.py b/tutorials/preprocessing/30_filtering_resampling.py index 32854096194..53b1f550fcc 100644 --- a/tutorials/preprocessing/30_filtering_resampling.py +++ b/tutorials/preprocessing/30_filtering_resampling.py @@ -156,7 +156,6 @@ def add_arrows(axes): raw_notch = raw.copy().notch_filter(freqs=freqs, picks=meg_picks) for title, data in zip(["Un", "Notch "], [raw, raw_notch]): fig = data.compute_psd(fmax=250).plot(average=True, picks="data", exclude="bads") - fig.subplots_adjust(top=0.85) fig.suptitle("{}filtered".format(title), size="xx-large", weight="bold") add_arrows(fig.axes[:2]) @@ -176,7 +175,6 @@ def add_arrows(axes): ) for title, data in zip(["Un", "spectrum_fit "], [raw, raw_notch_fit]): fig = data.compute_psd(fmax=250).plot(average=True, picks="data", exclude="bads") - fig.subplots_adjust(top=0.85) fig.suptitle("{}filtered".format(title), size="xx-large", weight="bold") add_arrows(fig.axes[:2]) @@ -212,7 +210,6 @@ def add_arrows(axes): for data, title in zip([raw, raw_downsampled], ["Original", "Downsampled"]): fig = data.compute_psd().plot(average=True, picks="data", exclude="bads") - fig.subplots_adjust(top=0.9) fig.suptitle(title) plt.setp(fig.axes, xlim=(0, 300)) diff --git a/tutorials/preprocessing/50_artifact_correction_ssp.py b/tutorials/preprocessing/50_artifact_correction_ssp.py index a1ea7135d8e..55d18b276a6 100644 --- a/tutorials/preprocessing/50_artifact_correction_ssp.py +++ b/tutorials/preprocessing/50_artifact_correction_ssp.py @@ -498,7 +498,9 @@ evoked_eeg = epochs.average().pick("eeg") evoked_eeg.del_proj().add_proj(ecg_projs).add_proj(eog_projs) -fig, axes = plt.subplots(1, 3, figsize=(8, 3), sharex=True, sharey=True) +fig, axes = plt.subplots( + 1, 3, figsize=(8, 3), sharex=True, sharey=True, layout="constrained" +) for pi, proj in enumerate((False, True, "reconstruct")): ax = axes[pi] evoked_eeg.plot(proj=proj, axes=ax, spatial_colors=True) @@ -512,7 +514,6 @@ ax.yaxis.set_tick_params(labelbottom=True) for text in list(ax.texts): text.remove() -mne.viz.tight_layout() # %% # Note that here the bias in the EEG and magnetometer channels is reduced by diff --git a/tutorials/preprocessing/60_maxwell_filtering_sss.py b/tutorials/preprocessing/60_maxwell_filtering_sss.py index 191eabf2b45..a3659b1f765 100644 --- a/tutorials/preprocessing/60_maxwell_filtering_sss.py +++ b/tutorials/preprocessing/60_maxwell_filtering_sss.py @@ -163,7 +163,7 @@ ) # First, plot the "raw" scores. -fig, ax = plt.subplots(1, 2, figsize=(12, 8)) +fig, ax = plt.subplots(1, 2, figsize=(12, 8), layout="constrained") fig.suptitle( f"Automated noisy channel detection: {ch_type}", fontsize=16, fontweight="bold" ) @@ -188,9 +188,6 @@ ] ax[1].set_title("Scores > Limit", fontweight="bold") -# The figure title should not overlap with the subplots. -fig.tight_layout(rect=[0, 0.03, 1, 0.95]) - # %% # # .. note:: You can use the very same code as above to produce figures for diff --git a/tutorials/preprocessing/70_fnirs_processing.py b/tutorials/preprocessing/70_fnirs_processing.py index 1dd30c628ab..886d99fc618 100644 --- a/tutorials/preprocessing/70_fnirs_processing.py +++ b/tutorials/preprocessing/70_fnirs_processing.py @@ -110,7 +110,7 @@ # coupling index. sci = mne.preprocessing.nirs.scalp_coupling_index(raw_od) -fig, ax = plt.subplots() +fig, ax = plt.subplots(layout="constrained") ax.hist(sci) ax.set(xlabel="Scalp Coupling Index", ylabel="Count", xlim=[0, 1]) @@ -157,7 +157,6 @@ for when, _raw in dict(Before=raw_haemo_unfiltered, After=raw_haemo).items(): fig = _raw.compute_psd().plot(average=True, picks="data", exclude="bads") fig.suptitle(f"{when} filtering", weight="bold", size="x-large") - fig.subplots_adjust(top=0.88) # %% # Extract epochs @@ -172,7 +171,6 @@ events, event_dict = mne.events_from_annotations(raw_haemo) fig = mne.viz.plot_events(events, event_id=event_dict, sfreq=raw_haemo.info["sfreq"]) -fig.subplots_adjust(right=0.7) # make room for the legend # %% @@ -238,7 +236,7 @@ # pairs that we selected. All the channels in this data are located over the # motor cortex, and all channels show a similar pattern in the data. -fig, axes = plt.subplots(nrows=2, ncols=2, figsize=(15, 6)) +fig, axes = plt.subplots(nrows=2, ncols=2, figsize=(15, 6), layout="constrained") clims = dict(hbo=[-20, 20], hbr=[-20, 20]) epochs["Control"].average().plot_image(axes=axes[:, 0], clim=clims) epochs["Tapping"].average().plot_image(axes=axes[:, 1], clim=clims) @@ -308,7 +306,11 @@ # And we can plot the comparison at a single time point for two conditions. fig, axes = plt.subplots( - nrows=2, ncols=4, figsize=(9, 5), gridspec_kw=dict(width_ratios=[1, 1, 1, 0.1]) + nrows=2, + ncols=4, + figsize=(9, 5), + gridspec_kw=dict(width_ratios=[1, 1, 1, 0.1]), + layout="constrained", ) vlim = (-8, 8) ts = 9.0 @@ -341,13 +343,12 @@ for column, condition in enumerate(["Tapping Left", "Tapping Right", "Left-Right"]): for row, chroma in enumerate(["HbO", "HbR"]): axes[row, column].set_title("{}: {}".format(chroma, condition)) -fig.tight_layout() # %% # Lastly, we can also look at the individual waveforms to see what is # driving the topographic plot above. -fig, axes = plt.subplots(nrows=1, ncols=1, figsize=(6, 4)) +fig, axes = plt.subplots(nrows=1, ncols=1, figsize=(6, 4), layout="constrained") mne.viz.plot_evoked_topo( epochs["Left"].average(picks="hbo"), color="b", axes=axes, legend=False ) diff --git a/tutorials/preprocessing/80_opm_processing.py b/tutorials/preprocessing/80_opm_processing.py index 7c76499fd36..a8d30c12abd 100644 --- a/tutorials/preprocessing/80_opm_processing.py +++ b/tutorials/preprocessing/80_opm_processing.py @@ -57,7 +57,7 @@ data_ds, time_ds = raw[picks[::5], :stop] data_ds, time_ds = data_ds[:, ::step] * amp_scale, time_ds[::step] -fig, ax = plt.subplots(constrained_layout=True) +fig, ax = plt.subplots(layout="constrained") plot_kwargs = dict(lw=1, alpha=0.5) ax.plot(time_ds, data_ds.T - np.mean(data_ds, axis=1), **plot_kwargs) ax.grid(True) @@ -111,7 +111,7 @@ data_ds, _ = raw[picks[::5], :stop] data_ds = data_ds[:, ::step] * amp_scale -fig, ax = plt.subplots(constrained_layout=True) +fig, ax = plt.subplots(layout="constrained") ax.plot(time_ds, data_ds.T - np.mean(data_ds, axis=1), **plot_kwargs) ax.grid(True, ls=":") ax.set(title="After reference regression", **set_kwargs) @@ -139,7 +139,7 @@ data_ds, _ = raw[picks[::5], :stop] data_ds = data_ds[:, ::step] * amp_scale -fig, ax = plt.subplots(constrained_layout=True) +fig, ax = plt.subplots(layout="constrained") ax.plot(time_ds, data_ds.T - np.mean(data_ds, axis=1), **plot_kwargs) ax.grid(True, ls=":") ax.set(title="After HFC", **set_kwargs) @@ -168,7 +168,7 @@ shielding = 10 * np.log10(psd_pre[:] / psd_post_reg[:]) -fig, ax = plt.subplots(constrained_layout=True) +fig, ax = plt.subplots(layout="constrained") ax.plot(psd_post_reg.freqs, shielding.T, **plot_kwargs) ax.grid(True, ls=":") ax.set(xticks=psd_post_reg.freqs) @@ -182,7 +182,7 @@ shielding = 10 * np.log10(psd_pre[:] / psd_post_hfc[:]) -fig, ax = plt.subplots(constrained_layout=True) +fig, ax = plt.subplots(layout="constrained") ax.plot(psd_post_hfc.freqs, shielding.T, **plot_kwargs) ax.grid(True, ls=":") ax.set(xticks=psd_post_hfc.freqs) @@ -215,7 +215,7 @@ # plot data_ds, _ = raw[picks[::5], :stop] data_ds = data_ds[:, ::step] * amp_scale -fig, ax = plt.subplots(constrained_layout=True) +fig, ax = plt.subplots(layout="constrained") plot_kwargs = dict(lw=1, alpha=0.5) ax.plot(time_ds, data_ds.T - np.mean(data_ds, axis=1), **plot_kwargs) ax.grid(True) diff --git a/tutorials/raw/20_event_arrays.py b/tutorials/raw/20_event_arrays.py index 6fedcfe0ade..e13b1f361a7 100644 --- a/tutorials/raw/20_event_arrays.py +++ b/tutorials/raw/20_event_arrays.py @@ -158,7 +158,6 @@ fig = mne.viz.plot_events( events, sfreq=raw.info["sfreq"], first_samp=raw.first_samp, event_id=event_dict ) -fig.subplots_adjust(right=0.7) # make room for legend # %% # Plotting events and raw data together diff --git a/tutorials/simulation/80_dics.py b/tutorials/simulation/80_dics.py index b8efcad9319..951671df1e4 100644 --- a/tutorials/simulation/80_dics.py +++ b/tutorials/simulation/80_dics.py @@ -99,7 +99,7 @@ def coh_signal_gen(): signal1 = coh_signal_gen() signal2 = coh_signal_gen() -fig, axes = plt.subplots(2, 2, figsize=(8, 4)) +fig, axes = plt.subplots(2, 2, figsize=(8, 4), layout="constrained") # Plot the timeseries ax = axes[0][0] @@ -133,7 +133,6 @@ def coh_signal_gen(): ylabel="Coherence", title="Coherence between the timeseries", ) -fig.tight_layout() # %% # Now we put the signals at two locations on the cortex. We construct a diff --git a/tutorials/stats-sensor-space/10_background_stats.py b/tutorials/stats-sensor-space/10_background_stats.py index 066ab249121..412715b3042 100644 --- a/tutorials/stats-sensor-space/10_background_stats.py +++ b/tutorials/stats-sensor-space/10_background_stats.py @@ -76,7 +76,7 @@ # %% # The data averaged over all subjects looks like this: -fig, ax = plt.subplots() +fig, ax = plt.subplots(layout="constrained") ax.imshow(X.mean(0), cmap="inferno") ax.set(xticks=[], yticks=[], title="Data averaged over subjects") @@ -121,7 +121,7 @@ def plot_t_p(t, p, title, mcc, axes=None): if axes is None: - fig = plt.figure(figsize=(6, 3)) + fig = plt.figure(figsize=(6, 3), layout="constrained") axes = [fig.add_subplot(121, projection="3d"), fig.add_subplot(122)] show = True else: @@ -150,7 +150,7 @@ def plot_t_p(t, p, title, mcc, axes=None): xticks=[], yticks=[], zticks=[], xlim=[0, width - 1], ylim=[0, width - 1] ) axes[0].view_init(30, 15) - cbar = plt.colorbar( + cbar = axes[0].figure.colorbar( ax=axes[0], shrink=0.75, orientation="horizontal", @@ -172,7 +172,7 @@ def plot_t_p(t, p, title, mcc, axes=None): use_p, cmap="inferno", vmin=p_lims[0], vmax=p_lims[1], interpolation="nearest" ) axes[1].set(xticks=[], yticks=[]) - cbar = plt.colorbar( + cbar = axes[1].figure.colorbar( ax=axes[1], shrink=0.75, orientation="horizontal", @@ -188,8 +188,6 @@ def plot_t_p(t, p, title, mcc, axes=None): text = fig.suptitle(title) if mcc: text.set_weight("bold") - plt.subplots_adjust(0, 0.05, 1, 0.9, wspace=0, hspace=0) - mne.viz.utils.plt_show() plot_t_p(ts[-1], ps[-1], titles[-1], mccs[-1]) @@ -286,7 +284,7 @@ def plot_t_p(t, p, title, mcc, axes=None): N = np.arange(1, 80) alpha = 0.05 p_type_I = 1 - (1 - alpha) ** N -fig, ax = plt.subplots(figsize=(4, 3)) +fig, ax = plt.subplots(figsize=(4, 3), layout="constrained") ax.scatter(N, p_type_I, 3) ax.set( xlim=N[[0, -1]], @@ -295,7 +293,6 @@ def plot_t_p(t, p, title, mcc, axes=None): ylabel="Probability of at least\none type I error", ) ax.grid(True) -fig.tight_layout() fig.show() # %% @@ -612,7 +609,7 @@ def plot_t_p(t, p, title, mcc, axes=None): # and the bottom shows p-values for various statistical tests, with the ones # with proper control over FWER or FDR with bold titles. -fig = plt.figure(facecolor="w", figsize=(14, 3)) +fig = plt.figure(facecolor="w", figsize=(14, 3), layout="constrained") assert len(ts) == len(titles) == len(ps) for ii in range(len(ts)): ax = [ @@ -620,8 +617,6 @@ def plot_t_p(t, p, title, mcc, axes=None): fig.add_subplot(2, 10, 11 + ii), ] plot_t_p(ts[ii], ps[ii], titles[ii], mccs[ii], ax) -fig.tight_layout(pad=0, w_pad=0.05, h_pad=0.1) -plt.show() # %% # The first three columns show the parametric and non-parametric statistics diff --git a/tutorials/stats-sensor-space/40_cluster_1samp_time_freq.py b/tutorials/stats-sensor-space/40_cluster_1samp_time_freq.py index a43fdfd46aa..cf49f48ddf4 100644 --- a/tutorials/stats-sensor-space/40_cluster_1samp_time_freq.py +++ b/tutorials/stats-sensor-space/40_cluster_1samp_time_freq.py @@ -235,8 +235,7 @@ evoked_data = evoked.data times = 1e3 * evoked.times -plt.figure() -plt.subplots_adjust(0.12, 0.08, 0.96, 0.94, 0.2, 0.43) +fig, (ax, ax2) = plt.subplots(2, layout="constrained") T_obs_plot = np.nan * np.ones_like(T_obs) for c, p_val in zip(clusters, cluster_p_values): @@ -252,8 +251,7 @@ vmax = np.max(np.abs(T_obs)) vmin = -vmax -plt.subplot(2, 1, 1) -plt.imshow( +ax.imshow( T_obs[ch_idx], cmap=plt.cm.gray, extent=[times[0], times[-1], freqs[0], freqs[-1]], @@ -262,7 +260,7 @@ vmin=vmin, vmax=vmax, ) -plt.imshow( +ax.imshow( T_obs_plot[ch_idx], cmap=plt.cm.RdBu_r, extent=[times[0], times[-1], freqs[0], freqs[-1]], @@ -271,11 +269,8 @@ vmin=vmin, vmax=vmax, ) -plt.colorbar() -plt.xlabel("Time (ms)") -plt.ylabel("Frequency (Hz)") -plt.title(f"Induced power ({tfr_epochs.ch_names[ch_idx]})") +fig.colorbar(ax.images[0]) +ax.set(xlabel="Time (ms)", ylabel="Frequency (Hz)") +ax.set(title=f"Induced power ({tfr_epochs.ch_names[ch_idx]})") -ax2 = plt.subplot(2, 1, 2) evoked.plot(axes=[ax2], time_unit="s") -plt.show() diff --git a/tutorials/stats-sensor-space/50_cluster_between_time_freq.py b/tutorials/stats-sensor-space/50_cluster_between_time_freq.py index 6ef0eaf3de3..69bdbbc5d91 100644 --- a/tutorials/stats-sensor-space/50_cluster_between_time_freq.py +++ b/tutorials/stats-sensor-space/50_cluster_between_time_freq.py @@ -147,8 +147,7 @@ times = 1e3 * epochs_condition_1.times # change unit to ms -fig, (ax, ax2) = plt.subplots(2, 1, figsize=(6, 4)) -fig.subplots_adjust(0.12, 0.08, 0.96, 0.94, 0.2, 0.43) +fig, (ax, ax2) = plt.subplots(2, 1, figsize=(6, 4), layout="constrained") # Compute the difference in evoked to determine which was greater since # we used a 1-way ANOVA which tested for a difference in population means diff --git a/tutorials/stats-sensor-space/70_cluster_rmANOVA_time_freq.py b/tutorials/stats-sensor-space/70_cluster_rmANOVA_time_freq.py index 1dfcfc79f86..a57112bedc4 100644 --- a/tutorials/stats-sensor-space/70_cluster_rmANOVA_time_freq.py +++ b/tutorials/stats-sensor-space/70_cluster_rmANOVA_time_freq.py @@ -172,7 +172,7 @@ effect_labels = ["modality", "location", "modality by location"] -fig, axes = plt.subplots(3, 1, figsize=(6, 6)) +fig, axes = plt.subplots(3, 1, figsize=(6, 6), layout="constrained") # let's visualize our effects by computing f-images for effect, sig, effect_label, ax in zip(fvals, pvals, effect_labels, axes): @@ -198,8 +198,6 @@ ax.set_ylabel("Frequency (Hz)") ax.set_title(f'Time-locked response for "{effect_label}" ({ch_name})') -fig.tight_layout() - # %% # Account for multiple comparisons using FDR versus permutation clustering test # ----------------------------------------------------------------------------- @@ -250,7 +248,7 @@ def stat_fun(*args): F_obs_plot = F_obs.copy() F_obs_plot[~clusters[np.squeeze(good_clusters)]] = np.nan -fig, ax = plt.subplots(figsize=(6, 4)) +fig, ax = plt.subplots(figsize=(6, 4), layout="constrained") for f_image, cmap in zip([F_obs, F_obs_plot], ["gray", "autumn"]): c = ax.imshow( f_image, @@ -267,7 +265,6 @@ def stat_fun(*args): f'Time-locked response for "modality by location" ({ch_name})\n' "cluster-level corrected (p <= 0.05)" ) -fig.tight_layout() # %% # Now using FDR: @@ -276,7 +273,7 @@ def stat_fun(*args): F_obs_plot2 = F_obs.copy() F_obs_plot2[~mask.reshape(F_obs_plot.shape)] = np.nan -fig, ax = plt.subplots(figsize=(6, 4)) +fig, ax = plt.subplots(figsize=(6, 4), layout="constrained") for f_image, cmap in zip([F_obs, F_obs_plot2], ["gray", "autumn"]): c = ax.imshow( f_image, @@ -293,7 +290,6 @@ def stat_fun(*args): f'Time-locked response for "modality by location" ({ch_name})\n' "FDR corrected (p <= 0.05)" ) -fig.tight_layout() # %% # Both cluster-level and FDR correction help get rid of potential diff --git a/tutorials/stats-sensor-space/75_cluster_ftest_spatiotemporal.py b/tutorials/stats-sensor-space/75_cluster_ftest_spatiotemporal.py index db6505fbafe..7a3234c5346 100644 --- a/tutorials/stats-sensor-space/75_cluster_ftest_spatiotemporal.py +++ b/tutorials/stats-sensor-space/75_cluster_ftest_spatiotemporal.py @@ -199,7 +199,7 @@ mask[ch_inds, :] = True # initialize figure - fig, ax_topo = plt.subplots(1, 1, figsize=(10, 3)) + fig, ax_topo = plt.subplots(1, 1, figsize=(10, 3), layout="constrained") # plot average test statistic and mark significant sensors f_evoked = mne.EvokedArray(f_map[:, np.newaxis], epochs.info, tmin=0) @@ -251,10 +251,7 @@ (ymin, ymax), sig_times[0], sig_times[-1], color="orange", alpha=0.3 ) - # clean up viz - mne.viz.tight_layout(fig=fig) - fig.subplots_adjust(bottom=0.05) - plt.show() +plt.show() # %% # Permutation statistic for time-frequencies @@ -352,7 +349,7 @@ sig_times = epochs.times[time_inds] # initialize figure - fig, ax_topo = plt.subplots(1, 1, figsize=(10, 3)) + fig, ax_topo = plt.subplots(1, 1, figsize=(10, 3), layout="constrained") # create spatial mask mask = np.zeros((f_map.shape[0], 1), dtype=bool) @@ -414,9 +411,7 @@ ax_colorbar2.set_ylabel("F-stat") # clean up viz - mne.viz.tight_layout(fig=fig) - fig.subplots_adjust(bottom=0.05) - plt.show() +plt.show() # %% diff --git a/tutorials/time-freq/20_sensors_time_frequency.py b/tutorials/time-freq/20_sensors_time_frequency.py index 776a230ecad..07a31e99db5 100644 --- a/tutorials/time-freq/20_sensors_time_frequency.py +++ b/tutorials/time-freq/20_sensors_time_frequency.py @@ -209,7 +209,7 @@ power.plot_topo(baseline=(-0.5, 0), mode="logratio", title="Average power") power.plot([82], baseline=(-0.5, 0), mode="logratio", title=power.ch_names[82]) -fig, axes = plt.subplots(1, 2, figsize=(7, 4), constrained_layout=True) +fig, axes = plt.subplots(1, 2, figsize=(7, 4), layout="constrained") topomap_kw = dict( ch_type="grad", tmin=0.5, tmax=1.5, baseline=(-0.5, 0), mode="logratio", show=False ) From 3b6a33954c1aeb2ee02db3e9840b1df00d8c3be3 Mon Sep 17 00:00:00 2001 From: Mathieu Scheltienne Date: Tue, 10 Oct 2023 17:39:24 +0200 Subject: [PATCH 004/405] Fix spelling in warning on cHPI (#12095) --- mne/_fiff/write.py | 3 +-- mne/chpi.py | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/mne/_fiff/write.py b/mne/_fiff/write.py index b8ed1d2b1d8..01c991767dd 100644 --- a/mne/_fiff/write.py +++ b/mne/_fiff/write.py @@ -133,9 +133,8 @@ def write_string(fid, kind, data): except UnicodeEncodeError: str_data = str(data).encode("latin1", errors="xmlcharrefreplace") data_size = len(str_data) # therefore compute size here - my_dtype = ">a" # py2/3 compatible on writing -- don't ask me why if data_size > 0: - _write(fid, str_data, kind, data_size, FIFF.FIFFT_STRING, my_dtype) + _write(fid, str_data, kind, data_size, FIFF.FIFFT_STRING, ">S") def write_name_list(fid, kind, data): diff --git a/mne/chpi.py b/mne/chpi.py index 96ce72ee195..801ee1d2e73 100644 --- a/mne/chpi.py +++ b/mne/chpi.py @@ -629,8 +629,8 @@ def _setup_hpi_amplitude_fitting( for key in ("sss_info", "max_st"): if len(ent["max_info"]["sss_info"]) > 0: warn( - "Fitting cHPI amplutudes after Maxwell filtering may not to work, " - "consider fitting on the original data" + "Fitting cHPI amplitudes after Maxwell filtering may not work, " + "consider fitting on the original data." ) break From e1545e6214f09ffe0a84fed0bef9a17bc1a6386f Mon Sep 17 00:00:00 2001 From: Daniel McCloy Date: Wed, 11 Oct 2023 10:19:59 -0500 Subject: [PATCH 005/405] Fix redirects (#12099) --- doc/_templates/sidebar-quicklinks.html | 8 +- doc/conf.py | 262 ++++++++++++++----------- 2 files changed, 147 insertions(+), 123 deletions(-) diff --git a/doc/_templates/sidebar-quicklinks.html b/doc/_templates/sidebar-quicklinks.html index ff37b0e3e45..64826a79f65 100644 --- a/doc/_templates/sidebar-quicklinks.html +++ b/doc/_templates/sidebar-quicklinks.html @@ -3,10 +3,10 @@
Version {{ release }}
diff --git a/doc/conf.py b/doc/conf.py index b8086500640..f9128227cd6 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -9,6 +9,7 @@ import gc from importlib.metadata import metadata import os +from pathlib import Path import subprocess import sys import time @@ -60,8 +61,8 @@ # (Sphinx looks at variable changes and rewrites all files if some change) copyright = ( f'2012–{td.year}, MNE Developers. Last updated \n' # noqa: E501 - '' -) # noqa: E501 + '' # noqa: E501 +) if os.getenv("MNE_FULL_DATE", "false").lower() != "true": copyright = f"2012–{td.year}, MNE Developers. Last updated locally." @@ -1636,87 +1637,70 @@ def reset_warnings(gallery_conf, fname): custom_redirects = { # Custom redirects (one HTML path to another, relative to outdir) # can be added here as fr->to key->value mappings - "install/contributing.html": "development/contributing.html", - "overview/roadmap.html": "development/roadmap.html", - "whats_new.html": "development/whats_new.html", - f"{tu}/evoked/plot_eeg_erp.html": f"{tu}/evoked/30_eeg_erp.html", - f"{tu}/evoked/plot_whitened.html": f"{tu}/evoked/40_whitened.html", - f"{tu}/misc/plot_modifying_data_inplace.html": f"{tu}/intro/15_inplace.html", - f"{tu}/misc/plot_report.html": f"{tu}/intro/70_report.html", - f"{tu}/misc/plot_seeg.html": f"{tu}/clinical/20_seeg.html", - f"{tu}/misc/plot_ecog.html": f"{tu}/clinical/30_ecog.html", - f"{tu}/{ml}/plot_receptive_field.html": f"{tu}/{ml}/30_strf.html", - f"{tu}/{ml}/plot_sensors_decoding.html": f"{tu}/{ml}/50_decoding.html", - f"{tu}/{sm}/plot_background_freesurfer.html": f"{tu}/{fw}/10_background_freesurfer.html", # noqa E501 - f"{tu}/{sm}/plot_source_alignment.html": f"{tu}/{fw}/20_source_alignment.html", - f"{tu}/{sm}/plot_forward.html": f"{tu}/{fw}/30_forward.html", - f"{tu}/{sm}/plot_eeg_no_mri.html": f"{tu}/{fw}/35_eeg_no_mri.html", - f"{tu}/{sm}/plot_background_freesurfer_mne.html": f"{tu}/{fw}/50_background_freesurfer_mne.html", # noqa E501 - f"{tu}/{sm}/plot_fix_bem_in_blender.html": f"{tu}/{fw}/80_fix_bem_in_blender.html", - f"{tu}/{sm}/plot_compute_covariance.html": f"{tu}/{fw}/90_compute_covariance.html", - f"{tu}/{sm}/plot_object_source_estimate.html": f"{tu}/{nv}/10_stc_class.html", - f"{tu}/{sm}/plot_dipole_fit.html": f"{tu}/{nv}/20_dipole_fit.html", - f"{tu}/{sm}/plot_mne_dspm_source_localization.html": f"{tu}/{nv}/30_mne_dspm_loreta.html", # noqa E501 - f"{tu}/{sm}/plot_dipole_orientations.html": f"{tu}/{nv}/35_dipole_orientations.html", # noqa E501 - f"{tu}/{sm}/plot_mne_solutions.html": f"{tu}/{nv}/40_mne_fixed_free.html", - f"{tu}/{sm}/plot_beamformer_lcmv.html": f"{tu}/{nv}/50_beamformer_lcmv.html", - f"{tu}/{sm}/plot_visualize_stc.html": f"{tu}/{nv}/60_visualize_stc.html", - f"{tu}/{sm}/plot_eeg_mri_coords.html": f"{tu}/{nv}/70_eeg_mri_coords.html", - f"{tu}/{sd}/plot_brainstorm_phantom_elekta.html": f"{tu}/{nv}/80_brainstorm_phantom_elekta.html", # noqa E501 - f"{tu}/{sd}/plot_brainstorm_phantom_ctf.html": f"{tu}/{nv}/85_brainstorm_phantom_ctf.html", # noqa E501 - f"{tu}/{sd}/plot_phantom_4DBTi.html": f"{tu}/{nv}/90_phantom_4DBTi.html", - f"{tu}/{sd}/plot_brainstorm_auditory.html": f"{tu}/io/60_ctf_bst_auditory.html", - f"{tu}/{sd}/plot_sleep.html": f"{tu}/clinical/60_sleep.html", - f"{tu}/{di}/plot_background_filtering.html": f"{tu}/{pr}/25_background_filtering.html", # noqa E501 - f"{tu}/{di}/plot_background_statistics.html": f"{tu}/{sn}/10_background_stats.html", - f"{tu}/{sn}/plot_stats_cluster_erp.html": f"{tu}/{sn}/20_erp_stats.html", - f"{tu}/{sn}/plot_stats_cluster_1samp_test_time_frequency.html": f"{tu}/{sn}/40_cluster_1samp_time_freq.html", # noqa E501 - f"{tu}/{sn}/plot_stats_cluster_time_frequency.html": f"{tu}/{sn}/50_cluster_between_time_freq.html", # noqa E501 - f"{tu}/{sn}/plot_stats_spatio_temporal_cluster_sensors.html": f"{tu}/{sn}/75_cluster_ftest_spatiotemporal.html", # noqa E501 - f"{tu}/{sr}/plot_stats_cluster_spatio_temporal.html": f"{tu}/{sr}/20_cluster_1samp_spatiotemporal.html", # noqa E501 - f"{tu}/{sr}/plot_stats_cluster_spatio_temporal_2samp.html": f"{tu}/{sr}/30_cluster_ftest_spatiotemporal.html", # noqa E501 - f"{tu}/{sr}/plot_stats_cluster_spatio_temporal_repeated_measures_anova.html": f"{tu}/{sr}/60_cluster_rmANOVA_spatiotemporal.html", # noqa E501 - f"{tu}/{sr}/plot_stats_cluster_time_frequency_repeated_measures_anova.html": f"{tu}/{sn}/70_cluster_rmANOVA_time_freq.html", # noqa E501 - f"{tu}/{tf}/plot_sensors_time_frequency.html": f"{tu}/{tf}/20_sensors_time_frequency.html", # noqa E501 - f"{tu}/{tf}/plot_ssvep.html": f"{tu}/{tf}/50_ssvep.html", - f"{tu}/{si}/plot_creating_data_structures.html": f"{tu}/{si}/10_array_objs.html", - f"{tu}/{si}/plot_point_spread.html": f"{tu}/{si}/70_point_spread.html", - f"{tu}/{si}/plot_dics.html": f"{tu}/{si}/80_dics.html", - f"{tu}/{tf}/plot_eyetracking.html": f"{tu}/{pr}/90_eyetracking_data.html", - f"{ex}/{co}/mne_inverse_label_connectivity.html": f"{mne_conn}/{ex}/mne_inverse_label_connectivity.html", # noqa E501 - f"{ex}/{co}/cwt_sensor_connectivity.html": f"{mne_conn}/{ex}/cwt_sensor_connectivity.html", # noqa E501 - f"{ex}/{co}/mixed_source_space_connectivity.html": f"{mne_conn}/{ex}/mixed_source_space_connectivity.html", # noqa E501 - f"{ex}/{co}/mne_inverse_coherence_epochs.html": f"{mne_conn}/{ex}/mne_inverse_coherence_epochs.html", # noqa E501 - f"{ex}/{co}/mne_inverse_connectivity_spectrum.html": f"{mne_conn}/{ex}/mne_inverse_connectivity_spectrum.html", # noqa E501 - f"{ex}/{co}/mne_inverse_envelope_correlation_volume.html": f"{mne_conn}/{ex}/mne_inverse_envelope_correlation_volume.html", # noqa E501 - f"{ex}/{co}/mne_inverse_envelope_correlation.html": f"{mne_conn}/{ex}/mne_inverse_envelope_correlation.html", # noqa E501 - f"{ex}/{co}/mne_inverse_psi_visual.html": f"{mne_conn}/{ex}/mne_inverse_psi_visual.html", # noqa E501 - f"{ex}/{co}/sensor_connectivity.html": f"{mne_conn}/{ex}/sensor_connectivity.html", - f"{ex}/{vi}/publication_figure.html": f"{tu}/{vi}/10_publication_figure.html", - f"{ex}/{vi}/sensor_noise_level.html": f"{tu}/{pr}/50_artifact_correction_ssp.html", + "install/contributing": "development/contributing", + "overview/cite": "documentation/cite", + "overview/get_help": "help/index", + "overview/roadmap": "development/roadmap", + "whats_new": "development/whats_new", + f"{tu}/evoked/plot_eeg_erp": f"{tu}/evoked/30_eeg_erp", + f"{tu}/evoked/plot_whitened": f"{tu}/evoked/40_whitened", + f"{tu}/misc/plot_modifying_data_inplace": f"{tu}/intro/15_inplace", + f"{tu}/misc/plot_report": f"{tu}/intro/70_report", + f"{tu}/misc/plot_seeg": f"{tu}/clinical/20_seeg", + f"{tu}/misc/plot_ecog": f"{tu}/clinical/30_ecog", + f"{tu}/{ml}/plot_receptive_field": f"{tu}/{ml}/30_strf", + f"{tu}/{ml}/plot_sensors_decoding": f"{tu}/{ml}/50_decoding", + f"{tu}/{sm}/plot_background_freesurfer": f"{tu}/{fw}/10_background_freesurfer", + f"{tu}/{sm}/plot_source_alignment": f"{tu}/{fw}/20_source_alignment", + f"{tu}/{sm}/plot_forward": f"{tu}/{fw}/30_forward", + f"{tu}/{sm}/plot_eeg_no_mri": f"{tu}/{fw}/35_eeg_no_mri", + f"{tu}/{sm}/plot_background_freesurfer_mne": f"{tu}/{fw}/50_background_freesurfer_mne", # noqa E501 + f"{tu}/{sm}/plot_fix_bem_in_blender": f"{tu}/{fw}/80_fix_bem_in_blender", + f"{tu}/{sm}/plot_compute_covariance": f"{tu}/{fw}/90_compute_covariance", + f"{tu}/{sm}/plot_object_source_estimate": f"{tu}/{nv}/10_stc_class", + f"{tu}/{sm}/plot_dipole_fit": f"{tu}/{nv}/20_dipole_fit", + f"{tu}/{sm}/plot_mne_dspm_source_localization": f"{tu}/{nv}/30_mne_dspm_loreta", + f"{tu}/{sm}/plot_dipole_orientations": f"{tu}/{nv}/35_dipole_orientations", + f"{tu}/{sm}/plot_mne_solutions": f"{tu}/{nv}/40_mne_fixed_free", + f"{tu}/{sm}/plot_beamformer_lcmv": f"{tu}/{nv}/50_beamformer_lcmv", + f"{tu}/{sm}/plot_visualize_stc": f"{tu}/{nv}/60_visualize_stc", + f"{tu}/{sm}/plot_eeg_mri_coords": f"{tu}/{nv}/70_eeg_mri_coords", + f"{tu}/{sd}/plot_brainstorm_phantom_elekta": f"{tu}/{nv}/80_brainstorm_phantom_elekta", # noqa E501 + f"{tu}/{sd}/plot_brainstorm_phantom_ctf": f"{tu}/{nv}/85_brainstorm_phantom_ctf", + f"{tu}/{sd}/plot_phantom_4DBTi": f"{tu}/{nv}/90_phantom_4DBTi", + f"{tu}/{sd}/plot_brainstorm_auditory": f"{tu}/io/60_ctf_bst_auditory", + f"{tu}/{sd}/plot_sleep": f"{tu}/clinical/60_sleep", + f"{tu}/{di}/plot_background_filtering": f"{tu}/{pr}/25_background_filtering", + f"{tu}/{di}/plot_background_statistics": f"{tu}/{sn}/10_background_stats", + f"{tu}/{sn}/plot_stats_cluster_erp": f"{tu}/{sn}/20_erp_stats", + f"{tu}/{sn}/plot_stats_cluster_1samp_test_time_frequency": f"{tu}/{sn}/40_cluster_1samp_time_freq", # noqa E501 + f"{tu}/{sn}/plot_stats_cluster_time_frequency": f"{tu}/{sn}/50_cluster_between_time_freq", # noqa E501 + f"{tu}/{sn}/plot_stats_spatio_temporal_cluster_sensors": f"{tu}/{sn}/75_cluster_ftest_spatiotemporal", # noqa E501 + f"{tu}/{sr}/plot_stats_cluster_spatio_temporal": f"{tu}/{sr}/20_cluster_1samp_spatiotemporal", # noqa E501 + f"{tu}/{sr}/plot_stats_cluster_spatio_temporal_2samp": f"{tu}/{sr}/30_cluster_ftest_spatiotemporal", # noqa E501 + f"{tu}/{sr}/plot_stats_cluster_spatio_temporal_repeated_measures_anova": f"{tu}/{sr}/60_cluster_rmANOVA_spatiotemporal", # noqa E501 + f"{tu}/{sr}/plot_stats_cluster_time_frequency_repeated_measures_anova": f"{tu}/{sn}/70_cluster_rmANOVA_time_freq", # noqa E501 + f"{tu}/{tf}/plot_sensors_time_frequency": f"{tu}/{tf}/20_sensors_time_frequency", + f"{tu}/{tf}/plot_ssvep": f"{tu}/{tf}/50_ssvep", + f"{tu}/{si}/plot_creating_data_structures": f"{tu}/{si}/10_array_objs", + f"{tu}/{si}/plot_point_spread": f"{tu}/{si}/70_point_spread", + f"{tu}/{si}/plot_dics": f"{tu}/{si}/80_dics", + f"{tu}/{tf}/plot_eyetracking": f"{tu}/{pr}/90_eyetracking_data", + f"{ex}/{co}/mne_inverse_label_connectivity": f"{mne_conn}/{ex}/mne_inverse_label_connectivity", # noqa E501 + f"{ex}/{co}/cwt_sensor_connectivity": f"{mne_conn}/{ex}/cwt_sensor_connectivity", + f"{ex}/{co}/mixed_source_space_connectivity": f"{mne_conn}/{ex}/mixed_source_space_connectivity", # noqa E501 + f"{ex}/{co}/mne_inverse_coherence_epochs": f"{mne_conn}/{ex}/mne_inverse_coherence_epochs", # noqa E501 + f"{ex}/{co}/mne_inverse_connectivity_spectrum": f"{mne_conn}/{ex}/mne_inverse_connectivity_spectrum", # noqa E501 + f"{ex}/{co}/mne_inverse_envelope_correlation_volume": f"{mne_conn}/{ex}/mne_inverse_envelope_correlation_volume", # noqa E501 + f"{ex}/{co}/mne_inverse_envelope_correlation": f"{mne_conn}/{ex}/mne_inverse_envelope_correlation", # noqa E501 + f"{ex}/{co}/mne_inverse_psi_visual": f"{mne_conn}/{ex}/mne_inverse_psi_visual", + f"{ex}/{co}/sensor_connectivity": f"{mne_conn}/{ex}/sensor_connectivity", + f"{ex}/{vi}/publication_figure": f"{tu}/{vi}/10_publication_figure", + f"{ex}/{vi}/sensor_noise_level": f"{tu}/{pr}/50_artifact_correction_ssp", } - -def check_existing_redirect(path): - """Make sure existing HTML files are redirects, before overwriting.""" - if os.path.isfile(path): - with open(path, "r") as fid: - for _ in range(8): - next(fid) - line = fid.readline() - assert "Page Redirection" in line, line - - -def make_redirects(app, exception): - """Make HTML redirects.""" - # https://www.sphinx-doc.org/en/master/extdev/appapi.html - # Adapted from sphinxcontrib/redirects (BSD-2-Clause) - if not ( - isinstance(app.builder, sphinx.builders.html.StandaloneHTMLBuilder) - and exception is None - ): - return - TEMPLATE = """\ +# Adapted from sphinxcontrib/redirects (BSD-2-Clause) +REDIRECT_TEMPLATE = """\ @@ -1730,66 +1714,104 @@ def make_redirects(app, exception): If you are not redirected automatically, follow this link. -""" # noqa: E501 - sphinx_gallery_conf = app.config["sphinx_gallery_conf"] - for src_dir, out_dir in zip( - sphinx_gallery_conf["examples_dirs"], sphinx_gallery_conf["gallery_dirs"] - ): - root = os.path.abspath(os.path.join(app.srcdir, src_dir)) +""" + + +def check_existing_redirect(path): + """Make sure existing HTML files are redirects, before overwriting.""" + if path.is_file(): + with open(path, "r") as fid: + for _ in range(8): + next(fid) + line = fid.readline() + if "Page Redirection" not in line: + raise RuntimeError( + "Attempted overwrite of HTML file with a redirect, where the " + "original file was not already a redirect." + ) + + +def _check_valid_builder(app, exception): + valid_builder = isinstance(app.builder, sphinx.builders.html.StandaloneHTMLBuilder) + return valid_builder and exception is None + + +def make_gallery_redirects(app, exception): + """Make HTML redirects for our sphinx gallery pages.""" + if not _check_valid_builder(app, exception): + return + sg_conf = app.config["sphinx_gallery_conf"] + for src_dir, out_dir in zip(sg_conf["examples_dirs"], sg_conf["gallery_dirs"]): + root = (Path(app.srcdir) / src_dir).resolve() fnames = [ - os.path.join(os.path.relpath(dirpath, root), fname) - for dirpath, _, fnames in os.walk(root) - for fname in fnames - if fname in needed_plot_redirects + pyfile.relative_to(root) + for pyfile in root.rglob(r"**/*.py") + if pyfile.name in needed_plot_redirects ] # plot_ redirects for fname in fnames: - dirname = os.path.join(app.outdir, out_dir, os.path.dirname(fname)) - to_fname = os.path.splitext(os.path.basename(fname))[0] + ".html" + dirname = Path(app.outdir) / out_dir / fname.parent + to_fname = fname.with_suffix(".html").name fr_fname = f"plot_{to_fname}" - to_path = os.path.join(dirname, to_fname) - fr_path = os.path.join(dirname, fr_fname) - assert os.path.isfile(to_path), (fname, to_path) + to_path = dirname / to_fname + fr_path = dirname / fr_fname + assert to_path.is_file(), (fname, to_path) with open(fr_path, "w") as fid: - fid.write(TEMPLATE.format(to=to_fname)) + fid.write(REDIRECT_TEMPLATE.format(to=to_fname)) sphinx_logger.info( f"Added {len(fnames):3d} HTML plot_* redirects for {out_dir}" ) - # API redirects + + +def make_api_redirects(app, exception): + """Make HTML redirects for our API pages.""" + if not _check_valid_builder(app, exception): + return + for page in api_redirects: fname = f"{page}.html" - fr_path = os.path.join(app.outdir, fname) - to_path = os.path.join(app.outdir, "api", fname) + fr_path = Path(app.outdir) / fname + to_path = Path(app.outdir) / "api" / fname # allow overwrite if existing file is just a redirect check_existing_redirect(fr_path) with open(fr_path, "w") as fid: - fid.write(TEMPLATE.format(to=to_path)) + fid.write(REDIRECT_TEMPLATE.format(to=to_path)) sphinx_logger.info(f"Added {len(api_redirects):3d} HTML API redirects") - # custom redirects - for fr, to in custom_redirects.items(): - if not to.startswith("http"): - assert os.path.isfile(os.path.join(app.outdir, to)), to - # handle links to sibling folders - path_parts = to.split("/") - if tu in path_parts: - path_parts = [".."] + path_parts[(path_parts.index(tu) + 1) :] - to = os.path.join(*path_parts) - assert to.endswith("html"), to - fr_path = os.path.join(app.outdir, fr) - assert fr_path.endswith("html"), fr_path - # allow overwrite if existing file is just a redirect + + +def make_custom_redirects(app, exception): + """Make HTML redirects for miscellaneous pages.""" + if not _check_valid_builder(app, exception): + return + + for _fr, _to in custom_redirects.items(): + fr = f"{_fr}.html" + to = f"{_to}.html" + fr_path = Path(app.outdir) / fr check_existing_redirect(fr_path) - # handle folders that no longer exist - if fr_path.split("/")[-2] in ( + if to.startswith("http"): + to_path = to + else: + to_path = Path(app.outdir) / to + assert to_path.is_file(), to_path + # recreate folders that no longer exist + defunct_gallery_folders = ( "misc", "discussions", "source-modeling", "sample-datasets", "connectivity", + ) + parts = fr_path.relative_to(Path(app.outdir)).parts + if ( + len(parts) > 1 # whats_new violates this + and parts[1] in defunct_gallery_folders + and not fr_path.parent.exists() ): - os.makedirs(os.path.dirname(fr_path), exist_ok=True) + os.makedirs(fr_path.parent, exist_ok=True) + # write the redirect with open(fr_path, "w") as fid: - fid.write(TEMPLATE.format(to=to)) + fid.write(REDIRECT_TEMPLATE.format(to=to_path)) sphinx_logger.info(f"Added {len(custom_redirects):3d} HTML custom redirects") @@ -1818,5 +1840,7 @@ def setup(app): app.connect("autodoc-process-docstring", append_attr_meth_examples) report_scraper.app = app app.connect("builder-inited", report_scraper.copyfiles) - app.connect("build-finished", make_redirects) + app.connect("build-finished", make_gallery_redirects) + app.connect("build-finished", make_api_redirects) + app.connect("build-finished", make_custom_redirects) app.connect("build-finished", make_version) From d6d2f8c6a2ed4a0b27357da9ddf8e0cd14931b59 Mon Sep 17 00:00:00 2001 From: Clemens Brunner Date: Wed, 11 Oct 2023 19:33:45 +0200 Subject: [PATCH 006/405] Sort imports (#12097) --- examples/datasets/hf_sef_data.py | 3 +- examples/datasets/limo_data.py | 7 +- examples/datasets/opm_data.py | 1 + examples/datasets/spm_faces_dataset_sgskip.py | 4 +- examples/decoding/decoding_csp_eeg.py | 9 +- examples/decoding/decoding_csp_timefreq.py | 13 +- examples/decoding/decoding_rsa_sgskip.py | 12 +- .../decoding_spatio_temporal_source.py | 9 +- examples/decoding/decoding_spoc_CMC.py | 9 +- ...decoding_time_generalization_conditions.py | 3 +- .../decoding_unsupervised_spatial_filter.py | 5 +- examples/decoding/decoding_xdawn_eeg.py | 12 +- examples/decoding/ems_filtering.py | 6 +- examples/decoding/linear_model_patterns.py | 13 +- examples/decoding/receptive_field_mtrf.py | 10 +- examples/decoding/ssd_spatial_filters.py | 1 + examples/forward/forward_sensitivity_maps.py | 5 +- .../compute_mne_inverse_epochs_in_label.py | 5 +- .../inverse/compute_mne_inverse_volume.py | 4 +- examples/inverse/custom_inverse_solver.py | 6 +- examples/inverse/dics_epochs.py | 5 +- examples/inverse/dics_source_power.py | 3 +- examples/inverse/evoked_ers_source_power.py | 5 +- examples/inverse/gamma_map_inverse.py | 4 +- examples/inverse/label_activation_from_stc.py | 3 +- examples/inverse/label_from_stc.py | 4 +- examples/inverse/label_source_activations.py | 4 +- examples/inverse/mixed_norm_inverse.py | 8 +- .../inverse/mixed_source_space_inverse.py | 3 +- examples/inverse/mne_cov_power.py | 2 +- examples/inverse/morph_volume_stc.py | 5 +- examples/inverse/multi_dipole_model.py | 9 +- .../inverse/multidict_reweighted_tfmxne.py | 2 +- examples/inverse/psf_ctf_label_leakage.py | 10 +- examples/inverse/psf_ctf_vertices.py | 2 +- examples/inverse/psf_ctf_vertices_lcmv.py | 2 +- examples/inverse/psf_volume.py | 2 +- examples/inverse/rap_music.py | 5 +- examples/inverse/resolution_metrics.py | 3 +- examples/inverse/snr_estimate.py | 2 +- examples/inverse/source_space_snr.py | 7 +- .../time_frequency_mixed_norm_inverse.py | 8 +- examples/inverse/trap_music.py | 5 +- examples/inverse/vector_mne_solution.py | 3 +- examples/io/elekta_epochs.py | 3 +- examples/io/read_neo_format.py | 1 + examples/preprocessing/css.py | 4 +- .../preprocessing/define_target_events.py | 5 +- examples/preprocessing/eeg_bridging.py | 2 +- examples/preprocessing/eeg_csd.py | 2 +- .../preprocessing/eog_artifact_histogram.py | 2 +- examples/preprocessing/eog_regression.py | 3 +- examples/preprocessing/find_ref_artifacts.py | 3 +- .../preprocessing/fnirs_artifact_removal.py | 2 +- examples/preprocessing/ica_comparison.py | 3 +- examples/preprocessing/muscle_detection.py | 2 +- examples/preprocessing/otp.py | 2 +- examples/preprocessing/shift_evoked.py | 1 + examples/preprocessing/xdawn_denoising.py | 2 +- examples/simulation/plot_stc_metrics.py | 13 +- examples/simulation/simulate_evoked_data.py | 4 +- examples/simulation/simulate_raw_data.py | 12 +- examples/stats/cluster_stats_evoked.py | 2 +- examples/stats/fdr_stats_evoked.py | 2 +- examples/stats/sensor_permutation_test.py | 2 +- examples/stats/sensor_regression.py | 5 +- examples/time_frequency/compute_csd.py | 2 +- .../compute_source_psd_epochs.py | 2 +- .../source_label_time_frequency.py | 2 +- .../time_frequency/source_power_spectrum.py | 2 +- examples/time_frequency/temporal_whitening.py | 4 +- .../time_frequency/time_frequency_erds.py | 7 +- .../time_frequency_simulated.py | 8 +- examples/visualization/3d_to_2d.py | 8 +- .../visualization/channel_epochs_image.py | 2 +- examples/visualization/evoked_arrowmap.py | 3 +- examples/visualization/evoked_topomap.py | 4 +- examples/visualization/evoked_whitening.py | 3 +- examples/visualization/montage_sgskip.py | 2 +- examples/visualization/roi_erpimage_by_rt.py | 2 +- .../ssp_projs_sensitivity_map.py | 1 - .../visualization/topo_compare_conditions.py | 4 +- examples/visualization/topo_customized.py | 4 +- mne/__init__.pyi | 50 +-- mne/_fiff/_digitization.py | 7 +- mne/_fiff/compensator.py | 2 +- mne/_fiff/ctf_comp.py | 7 +- mne/_fiff/matrix.py | 6 +- mne/_fiff/meas_info.py | 136 ++++---- mne/_fiff/open.py | 8 +- mne/_fiff/pick.py | 12 +- mne/_fiff/proc_history.py | 20 +- mne/_fiff/proj.py | 40 +-- mne/_fiff/reference.py | 35 +-- mne/_fiff/tag.py | 16 +- mne/_fiff/tests/test_compensator.py | 6 +- mne/_fiff/tests/test_constants.py | 9 +- mne/_fiff/tests/test_meas_info.py | 82 ++--- mne/_fiff/tests/test_pick.py | 46 +-- mne/_fiff/tests/test_proc_history.py | 2 +- mne/_fiff/tests/test_reference.py | 26 +- mne/_fiff/tests/test_what.py | 2 +- mne/_fiff/tree.py | 7 +- mne/_fiff/utils.py | 3 +- mne/_fiff/what.py | 12 +- mne/_fiff/write.py | 8 +- mne/_freesurfer.py | 21 +- mne/_ola.py | 2 +- mne/annotations.py | 85 ++--- mne/baseline.py | 2 +- mne/beamformer/__init__.pyi | 20 +- mne/beamformer/_compute_beamformer.py | 18 +- mne/beamformer/_dics.py | 28 +- mne/beamformer/_lcmv.py | 20 +- mne/beamformer/_rap_music.py | 6 +- mne/beamformer/resolution_matrix.py | 4 +- mne/beamformer/tests/test_dics.py | 20 +- mne/beamformer/tests/test_external.py | 6 +- mne/beamformer/tests/test_lcmv.py | 37 ++- mne/beamformer/tests/test_rap_music.py | 5 +- .../tests/test_resolution_matrix.py | 3 +- mne/bem.py | 64 ++-- mne/channels/__init__.pyi | 34 +- mne/channels/_dig_montage_utils.py | 6 +- mne/channels/_standard_montage_utils.py | 11 +- mne/channels/channels.py | 70 ++--- mne/channels/interpolation.py | 7 +- mne/channels/layout.py | 12 +- mne/channels/montage.py | 74 +++-- mne/channels/tests/test_channels.py | 50 +-- mne/channels/tests/test_interpolation.py | 13 +- mne/channels/tests/test_layout.py | 18 +- mne/channels/tests/test_montage.py | 79 +++-- mne/channels/tests/test_standard_montage.py | 14 +- mne/channels/tests/test_unify_bads.py | 1 + mne/chpi.py | 52 +-- mne/commands/mne_anonymize.py | 3 +- mne/commands/mne_browse_raw.py | 3 +- mne/commands/mne_compare_fiff.py | 1 + mne/commands/mne_compute_proj_ecg.py | 1 + mne/commands/mne_compute_proj_eog.py | 1 + mne/commands/mne_coreg.py | 2 +- mne/commands/mne_freeview_bem_surfaces.py | 4 +- mne/commands/mne_make_scalp_surfaces.py | 2 +- mne/commands/mne_maxfilter.py | 3 +- mne/commands/mne_prepare_bem_model.py | 5 +- mne/commands/mne_report.py | 4 +- mne/commands/mne_setup_forward_model.py | 5 +- mne/commands/mne_setup_source_space.py | 2 +- mne/commands/mne_show_fiff.py | 1 + mne/commands/mne_show_info.py | 1 + mne/commands/mne_sys_info.py | 1 + mne/commands/mne_watershed_bem.py | 2 +- mne/commands/tests/test_commands.py | 34 +- mne/commands/utils.py | 4 +- mne/conftest.py | 34 +- mne/coreg.py | 53 ++-- mne/cov.py | 102 +++--- mne/cuda.py | 12 +- mne/datasets/__init__.pyi | 30 +- mne/datasets/_fetch.py | 15 +- mne/datasets/_fsaverage/base.py | 4 +- mne/datasets/_infant/base.py | 2 +- mne/datasets/_phantom/base.py | 2 +- mne/datasets/brainstorm/bst_auditory.py | 4 +- mne/datasets/brainstorm/bst_phantom_ctf.py | 4 +- mne/datasets/brainstorm/bst_phantom_elekta.py | 4 +- mne/datasets/brainstorm/bst_raw.py | 9 +- mne/datasets/brainstorm/bst_resting.py | 4 +- mne/datasets/eegbci/eegbci.py | 6 +- mne/datasets/epilepsy_ecog/_data.py | 2 +- mne/datasets/erp_core/erp_core.py | 2 +- mne/datasets/eyelink/eyelink.py | 2 +- mne/datasets/fieldtrip_cmc/fieldtrip_cmc.py | 2 +- mne/datasets/fnirs_motor/fnirs_motor.py | 2 +- mne/datasets/hf_sef/hf_sef.py | 7 +- mne/datasets/kiloword/kiloword.py | 2 +- mne/datasets/limo/limo.py | 8 +- mne/datasets/misc/_misc.py | 2 +- mne/datasets/mtrf/mtrf.py | 3 +- mne/datasets/multimodal/multimodal.py | 2 +- mne/datasets/opm/opm.py | 2 +- mne/datasets/phantom_4dbti/phantom_4dbti.py | 2 +- mne/datasets/refmeg_noise/refmeg_noise.py | 2 +- mne/datasets/sample/sample.py | 2 +- mne/datasets/sleep_physionet/_utils.py | 4 +- mne/datasets/sleep_physionet/age.py | 9 +- mne/datasets/sleep_physionet/temazepam.py | 3 +- .../sleep_physionet/tests/test_physionet.py | 14 +- mne/datasets/somato/somato.py | 2 +- mne/datasets/spm_face/spm_data.py | 7 +- mne/datasets/ssvep/ssvep.py | 2 +- mne/datasets/testing/_testing.py | 6 +- mne/datasets/tests/test_datasets.py | 14 +- .../ucl_opm_auditory/ucl_opm_auditory.py | 3 +- mne/datasets/utils.py | 30 +- .../visual_92_categories.py | 2 +- mne/decoding/base.py | 12 +- mne/decoding/csp.py | 8 +- mne/decoding/ems.py | 6 +- mne/decoding/receptive_field.py | 4 +- mne/decoding/search_light.py | 7 +- mne/decoding/ssd.py | 12 +- mne/decoding/tests/test_base.py | 33 +- mne/decoding/tests/test_csp.py | 6 +- mne/decoding/tests/test_ems.py | 6 +- mne/decoding/tests/test_receptive_field.py | 13 +- mne/decoding/tests/test_search_light.py | 18 +- mne/decoding/tests/test_ssd.py | 8 +- mne/decoding/tests/test_time_frequency.py | 2 +- mne/decoding/tests/test_transformer.py | 13 +- mne/decoding/time_delaying_ridge.py | 4 +- mne/decoding/time_frequency.py | 7 +- mne/decoding/transformer.py | 15 +- mne/dipole.py | 59 ++-- mne/epochs.py | 145 +++++---- mne/event.py | 28 +- mne/evoked.py | 99 +++--- mne/export/__init__.pyi | 2 +- mne/export/_edf.py | 1 + mne/export/_eeglab.py | 2 +- mne/export/_egimff.py | 8 +- mne/export/_export.py | 2 +- mne/export/tests/test_export.py | 22 +- mne/filter.py | 23 +- mne/fixes.py | 11 +- mne/forward/__init__.pyi | 60 ++-- mne/forward/_compute_forward.py | 12 +- mne/forward/_field_interpolation.py | 20 +- mne/forward/_lead_dots.py | 3 +- mne/forward/_make_forward.py | 46 ++- mne/forward/forward.py | 75 +++-- mne/forward/tests/test_field_interpolation.py | 16 +- mne/forward/tests/test_forward.py | 34 +- mne/forward/tests/test_make_forward.py | 60 ++-- mne/gui/__init__.pyi | 2 +- mne/gui/_coreg.py | 69 ++-- mne/gui/_gui.py | 6 +- mne/gui/tests/test_coreg.py | 12 +- mne/gui/tests/test_gui_api.py | 5 +- mne/inverse_sparse/__init__.pyi | 2 +- mne/inverse_sparse/_gamma_map.py | 4 +- mne/inverse_sparse/mxne_debiasing.py | 3 +- mne/inverse_sparse/mxne_inverse.py | 31 +- mne/inverse_sparse/mxne_optim.py | 12 +- mne/inverse_sparse/tests/test_gamma_map.py | 16 +- mne/inverse_sparse/tests/test_mxne_inverse.py | 26 +- mne/inverse_sparse/tests/test_mxne_optim.py | 20 +- mne/io/__init__.pyi | 50 +-- mne/io/_fiff_wrap.py | 10 +- mne/io/_read_raw.py | 28 +- mne/io/array/array.py | 2 +- mne/io/array/tests/test_array.py | 12 +- mne/io/artemis123/artemis123.py | 22 +- mne/io/artemis123/tests/test_artemis123.py | 12 +- mne/io/artemis123/utils.py | 6 +- mne/io/base.py | 110 +++---- mne/io/besa/besa.py | 3 +- mne/io/besa/tests/test_besa.py | 6 +- mne/io/boxy/boxy.py | 4 +- mne/io/boxy/tests/test_boxy.py | 4 +- mne/io/brainvision/brainvision.py | 6 +- mne/io/brainvision/tests/test_brainvision.py | 14 +- mne/io/bti/bti.py | 28 +- mne/io/bti/tests/test_bti.py | 30 +- mne/io/cnt/_utils.py | 6 +- mne/io/cnt/cnt.py | 18 +- mne/io/cnt/tests/test_cnt.py | 6 +- mne/io/ctf/ctf.py | 24 +- mne/io/ctf/eeg.py | 10 +- mne/io/ctf/hc.py | 5 +- mne/io/ctf/info.py | 22 +- mne/io/ctf/markers.py | 5 +- mne/io/ctf/tests/test_ctf.py | 20 +- mne/io/ctf/trans.py | 10 +- mne/io/curry/curry.py | 22 +- mne/io/curry/tests/test_curry.py | 19 +- mne/io/edf/edf.py | 13 +- mne/io/edf/tests/test_edf.py | 17 +- mne/io/edf/tests/test_gdf.py | 8 +- mne/io/eeglab/eeglab.py | 23 +- mne/io/eeglab/tests/test_eeglab.py | 14 +- mne/io/egi/egi.py | 10 +- mne/io/egi/egimff.py | 28 +- mne/io/egi/events.py | 2 +- mne/io/egi/general.py | 2 +- mne/io/egi/tests/test_egi.py | 10 +- mne/io/eximia/eximia.py | 6 +- mne/io/eximia/tests/test_eximia.py | 2 +- mne/io/eyelink/_utils.py | 2 +- mne/io/eyelink/eyelink.py | 4 +- mne/io/eyelink/tests/test_eyelink.py | 9 +- mne/io/fieldtrip/fieldtrip.py | 12 +- mne/io/fieldtrip/tests/helpers.py | 3 +- mne/io/fieldtrip/tests/test_fieldtrip.py | 12 +- mne/io/fieldtrip/utils.py | 2 +- mne/io/fiff/raw.py | 32 +- mne/io/fiff/tests/test_raw_fiff.py | 42 +-- mne/io/fil/fil.py | 17 +- mne/io/fil/sensors.py | 1 + mne/io/fil/tests/test_fil.py | 11 +- mne/io/hitachi/hitachi.py | 8 +- mne/io/hitachi/tests/test_hitachi.py | 9 +- mne/io/kit/coreg.py | 17 +- mne/io/kit/kit.py | 30 +- mne/io/kit/tests/test_coreg.py | 2 +- mne/io/kit/tests/test_kit.py | 20 +- mne/io/nedf/nedf.py | 6 +- mne/io/nedf/tests/test_nedf.py | 2 +- mne/io/nicolet/nicolet.py | 15 +- mne/io/nicolet/tests/test_nicolet.py | 4 +- mne/io/nihon/nihon.py | 6 +- mne/io/nihon/tests/test_nihon.py | 8 +- mne/io/nirx/nirx.py | 30 +- mne/io/nirx/tests/test_nirx.py | 14 +- mne/io/nsx/nsx.py | 8 +- mne/io/nsx/tests/test_nsx.py | 11 +- mne/io/persyst/persyst.py | 4 +- mne/io/persyst/tests/test_persyst.py | 2 +- mne/io/pick.py | 2 +- mne/io/snirf/_snirf.py | 17 +- mne/io/snirf/tests/test_snirf.py | 16 +- mne/io/tests/test_apply_function.py | 2 +- mne/io/tests/test_raw.py | 32 +- mne/io/tests/test_read_raw.py | 3 +- mne/label.py | 24 +- mne/minimum_norm/__init__.pyi | 30 +- mne/minimum_norm/_eloreta.py | 6 +- mne/minimum_norm/inverse.py | 64 ++-- mne/minimum_norm/resolution_matrix.py | 12 +- mne/minimum_norm/spatial_resolution.py | 2 +- mne/minimum_norm/tests/test_inverse.py | 63 ++-- .../tests/test_resolution_matrix.py | 7 +- .../tests/test_resolution_metrics.py | 4 +- mne/minimum_norm/tests/test_snr.py | 4 +- mne/minimum_norm/tests/test_time_frequency.py | 16 +- mne/minimum_norm/time_frequency.py | 24 +- mne/morph.py | 39 ++- mne/morph_map.py | 14 +- mne/parallel.py | 8 +- mne/preprocessing/__init__.pyi | 58 ++-- mne/preprocessing/_annotate_amplitude.py | 8 +- mne/preprocessing/_csd.py | 10 +- mne/preprocessing/_css.py | 4 +- mne/preprocessing/_fine_cal.py | 23 +- mne/preprocessing/_peak_finder.py | 2 +- mne/preprocessing/_regress.py | 16 +- mne/preprocessing/artifact_detection.py | 18 +- mne/preprocessing/ecg.py | 10 +- mne/preprocessing/eog.py | 8 +- .../eyetracking/_pupillometry.py | 4 +- .../eyetracking/tests/test_calibration.py | 4 +- .../eyetracking/tests/test_pupillometry.py | 2 +- mne/preprocessing/hfc.py | 2 +- mne/preprocessing/ica.py | 125 ++++---- mne/preprocessing/ieeg/_projection.py | 11 +- mne/preprocessing/ieeg/_volume.py | 6 +- .../ieeg/tests/test_projection.py | 5 +- mne/preprocessing/ieeg/tests/test_volume.py | 2 +- mne/preprocessing/infomax_.py | 2 +- mne/preprocessing/interpolate.py | 6 +- mne/preprocessing/maxfilter.py | 2 +- mne/preprocessing/maxwell.py | 55 ++-- mne/preprocessing/nirs/_beer_lambert_law.py | 4 +- mne/preprocessing/nirs/_optical_density.py | 6 +- mne/preprocessing/nirs/nirs.py | 4 +- .../nirs/tests/test_beer_lambert_law.py | 8 +- mne/preprocessing/nirs/tests/test_nirs.py | 27 +- .../nirs/tests/test_optical_density.py | 6 +- .../nirs/tests/test_scalp_coupling_index.py | 6 +- ...temporal_derivative_distribution_repair.py | 4 +- mne/preprocessing/otp.py | 2 +- mne/preprocessing/realign.py | 2 +- mne/preprocessing/ssp.py | 6 +- mne/preprocessing/stim.py | 9 +- .../tests/test_annotate_amplitude.py | 3 +- mne/preprocessing/tests/test_annotate_nan.py | 3 +- .../tests/test_artifact_detection.py | 8 +- mne/preprocessing/tests/test_csd.py | 14 +- mne/preprocessing/tests/test_css.py | 2 +- mne/preprocessing/tests/test_ctps.py | 4 +- mne/preprocessing/tests/test_ecg.py | 4 +- .../tests/test_eeglab_infomax.py | 9 +- mne/preprocessing/tests/test_fine_cal.py | 12 +- mne/preprocessing/tests/test_hfc.py | 5 +- mne/preprocessing/tests/test_ica.py | 40 +-- mne/preprocessing/tests/test_infomax.py | 7 +- mne/preprocessing/tests/test_interpolate.py | 2 +- mne/preprocessing/tests/test_maxwell.py | 39 ++- mne/preprocessing/tests/test_otp.py | 5 +- mne/preprocessing/tests/test_peak_finder.py | 4 +- mne/preprocessing/tests/test_realign.py | 4 +- mne/preprocessing/tests/test_regress.py | 4 +- mne/preprocessing/tests/test_ssp.py | 10 +- mne/preprocessing/tests/test_stim.py | 6 +- mne/preprocessing/tests/test_xdawn.py | 13 +- mne/preprocessing/xdawn.py | 8 +- mne/proj.py | 30 +- mne/rank.py | 28 +- mne/report/__init__.pyi | 2 +- .../bootstrap-icons/gen_css_for_mne.py | 4 +- mne/report/report.py | 97 +++--- mne/report/tests/test_report.py | 21 +- mne/simulation/__init__.pyi | 8 +- mne/simulation/evoked.py | 4 +- mne/simulation/metrics/metrics.py | 2 +- mne/simulation/metrics/tests/test_metrics.py | 11 +- mne/simulation/raw.py | 38 +-- mne/simulation/source.py | 14 +- mne/simulation/tests/test_evoked.py | 20 +- mne/simulation/tests/test_metrics.py | 2 +- mne/simulation/tests/test_raw.py | 50 +-- mne/simulation/tests/test_source.py | 13 +- mne/source_estimate.py | 60 ++-- mne/source_space/__init__.pyi | 6 +- mne/source_space/_source_space.py | 107 ++++--- mne/source_space/tests/test_source_space.py | 39 +-- mne/stats/__init__.pyi | 26 +- mne/stats/_adjacency.py | 2 +- mne/stats/cluster_level.py | 24 +- mne/stats/parametric.py | 2 +- mne/stats/permutations.py | 3 +- mne/stats/regression.py | 8 +- mne/stats/tests/test_adjacency.py | 2 +- mne/stats/tests/test_cluster_level.py | 25 +- mne/stats/tests/test_multi_comp.py | 6 +- mne/stats/tests/test_parametric.py | 6 +- mne/stats/tests/test_permutations.py | 6 +- mne/stats/tests/test_regression.py | 5 +- mne/surface.py | 47 ++- mne/tests/test_annotations.py | 44 +-- mne/tests/test_bem.py | 34 +- mne/tests/test_chpi.py | 44 +-- mne/tests/test_coreg.py | 44 +-- mne/tests/test_cov.py | 49 ++- mne/tests/test_defaults.py | 1 + mne/tests/test_dipole.py | 54 ++-- mne/tests/test_docstring_parameters.py | 2 +- mne/tests/test_epochs.py | 60 ++-- mne/tests/test_event.py | 30 +- mne/tests/test_evoked.py | 22 +- mne/tests/test_filter.py | 34 +- mne/tests/test_freesurfer.py | 14 +- mne/tests/test_import_nesting.py | 10 +- mne/tests/test_label.py | 40 ++- mne/tests/test_line_endings.py | 3 +- mne/tests/test_morph.py | 33 +- mne/tests/test_morph_map.py | 6 +- mne/tests/test_ola.py | 2 +- mne/tests/test_parallel.py | 2 +- mne/tests/test_proj.py | 30 +- mne/tests/test_rank.py | 16 +- mne/tests/test_source_estimate.py | 75 +++-- mne/tests/test_surface.py | 28 +- mne/tests/test_transforms.py | 60 ++-- mne/time_frequency/__init__.pyi | 2 +- mne/time_frequency/_stockwell.py | 2 +- mne/time_frequency/ar.py | 4 +- mne/time_frequency/csd.py | 26 +- mne/time_frequency/multitaper.py | 3 +- mne/time_frequency/psd.py | 2 +- mne/time_frequency/spectrum.py | 10 +- mne/time_frequency/tests/test_ar.py | 5 +- mne/time_frequency/tests/test_csd.py | 27 +- mne/time_frequency/tests/test_psd.py | 8 +- mne/time_frequency/tests/test_spectrum.py | 9 +- mne/time_frequency/tests/test_stft.py | 6 +- mne/time_frequency/tests/test_stockwell.py | 18 +- mne/time_frequency/tests/test_tfr.py | 32 +- mne/time_frequency/tfr.py | 75 +++-- mne/transforms.py | 37 ++- mne/utils/__init__.pyi | 296 +++++++++--------- mne/utils/_bunch.py | 1 - mne/utils/_logging.py | 12 +- mne/utils/_testing.py | 14 +- mne/utils/check.py | 27 +- mne/utils/config.py | 5 +- mne/utils/dataframe.py | 3 +- mne/utils/docs.py | 1 - mne/utils/misc.py | 14 +- mne/utils/mixin.py | 17 +- mne/utils/numerics.py | 18 +- mne/utils/progressbar.py | 8 +- mne/utils/spectrum.py | 1 + mne/utils/tests/test_bunch.py | 3 +- mne/utils/tests/test_check.py | 28 +- mne/utils/tests/test_config.py | 11 +- mne/utils/tests/test_docs.py | 11 +- mne/utils/tests/test_linalg.py | 4 +- mne/utils/tests/test_logging.py | 18 +- mne/utils/tests/test_misc.py | 4 +- mne/utils/tests/test_mixin.py | 2 +- mne/utils/tests/test_numerics.py | 45 ++- mne/utils/tests/test_progressbar.py | 4 +- mne/viz/_3d.py | 113 +++---- mne/viz/_3d_overlay.py | 3 +- mne/viz/__init__.pyi | 102 +++--- mne/viz/_brain/_brain.py | 111 ++++--- mne/viz/_brain/_linkviewer.py | 1 + mne/viz/_brain/_scraper.py | 3 +- mne/viz/_brain/surface.py | 5 +- mne/viz/_brain/tests/test_brain.py | 35 +-- mne/viz/_brain/tests/test_notebook.py | 15 +- mne/viz/_dipole.py | 10 +- mne/viz/_figure.py | 24 +- mne/viz/_mpl_figure.py | 11 +- mne/viz/_proj.py | 9 +- mne/viz/backends/_abstract.py | 4 +- mne/viz/backends/_notebook.py | 96 +++--- mne/viz/backends/_pyvista.py | 45 ++- mne/viz/backends/_qt.py | 142 ++++----- mne/viz/backends/_utils.py | 12 +- mne/viz/backends/renderer.py | 30 +- mne/viz/backends/tests/_utils.py | 3 +- mne/viz/backends/tests/test_abstract.py | 4 +- mne/viz/backends/tests/test_renderer.py | 6 +- mne/viz/backends/tests/test_utils.py | 10 +- mne/viz/circle.py | 8 +- mne/viz/conftest.py | 12 +- mne/viz/epochs.py | 32 +- mne/viz/evoked.py | 96 +++--- mne/viz/evoked_field.py | 34 +- mne/viz/ica.py | 48 +-- mne/viz/misc.py | 42 +-- mne/viz/montage.py | 4 +- mne/viz/raw.py | 18 +- mne/viz/tests/test_3d.py | 60 ++-- mne/viz/tests/test_3d_mpl.py | 8 +- mne/viz/tests/test_circle.py | 2 +- mne/viz/tests/test_epochs.py | 2 +- mne/viz/tests/test_evoked.py | 16 +- mne/viz/tests/test_figure.py | 1 + mne/viz/tests/test_ica.py | 12 +- mne/viz/tests/test_misc.py | 24 +- mne/viz/tests/test_montage.py | 5 +- mne/viz/tests/test_proj.py | 2 +- mne/viz/tests/test_raw.py | 8 +- mne/viz/tests/test_scraper.py | 2 + mne/viz/tests/test_topo.py | 12 +- mne/viz/tests/test_topomap.py | 45 ++- mne/viz/tests/test_utils.py | 24 +- mne/viz/topo.py | 19 +- mne/viz/topomap.py | 92 +++--- mne/viz/ui_events.py | 11 +- mne/viz/utils.py | 70 +++-- pyproject.toml | 2 +- tutorials/clinical/30_ecog.py | 2 +- tutorials/clinical/60_sleep.py | 13 +- tutorials/epochs/30_epochs_metadata.py | 1 + tutorials/epochs/40_autogenerate_metadata.py | 2 + .../epochs/60_make_fixed_length_epochs.py | 5 +- tutorials/evoked/30_eeg_erp.py | 3 +- tutorials/forward/20_source_alignment.py | 2 +- tutorials/forward/35_eeg_no_mri.py | 4 +- .../forward/50_background_freesurfer_mne.py | 8 +- tutorials/forward/80_fix_bem_in_blender.py | 1 + tutorials/intro/10_overview.py | 1 + tutorials/intro/20_events_from_raw.py | 1 + tutorials/intro/50_configure_mne.py | 1 + tutorials/intro/70_report.py | 6 +- tutorials/inverse/20_dipole_fit.py | 9 +- tutorials/inverse/30_mne_dspm_loreta.py | 4 +- tutorials/inverse/35_dipole_orientations.py | 5 +- tutorials/inverse/40_mne_fixed_free.py | 2 +- tutorials/inverse/50_beamformer_lcmv.py | 5 +- tutorials/inverse/60_visualize_stc.py | 6 +- tutorials/inverse/70_eeg_mri_coords.py | 2 +- .../inverse/80_brainstorm_phantom_elekta.py | 2 +- .../inverse/85_brainstorm_phantom_ctf.py | 2 +- tutorials/inverse/90_phantom_4DBTi.py | 4 +- tutorials/io/30_reading_fnirs_data.py | 1 + tutorials/io/60_ctf_bst_auditory.py | 4 +- tutorials/io/70_reading_eyetracking_data.py | 2 +- tutorials/machine-learning/30_strf.py | 9 +- tutorials/machine-learning/50_decoding.py | 13 +- .../10_preprocessing_overview.py | 2 + .../preprocessing/15_handling_bad_channels.py | 2 + .../preprocessing/20_rejecting_bad_data.py | 1 + .../preprocessing/25_background_filtering.py | 5 +- .../preprocessing/30_filtering_resampling.py | 4 +- .../35_artifact_correction_regression.py | 1 + .../40_artifact_correction_ica.py | 1 + .../preprocessing/45_projectors_background.py | 4 +- .../50_artifact_correction_ssp.py | 8 +- .../preprocessing/55_setting_eeg_reference.py | 1 + tutorials/preprocessing/59_head_positions.py | 1 + .../preprocessing/60_maxwell_filtering_sss.py | 6 +- .../preprocessing/70_fnirs_processing.py | 6 +- tutorials/raw/10_raw_overview.py | 4 +- tutorials/raw/20_event_arrays.py | 2 + tutorials/raw/30_annotate_raw.py | 1 + tutorials/simulation/70_point_spread.py | 5 +- tutorials/simulation/80_dics.py | 8 +- .../stats-sensor-space/10_background_stats.py | 8 +- tutorials/stats-sensor-space/20_erp_stats.py | 2 +- .../40_cluster_1samp_time_freq.py | 6 +- .../50_cluster_between_time_freq.py | 6 +- .../70_cluster_rmANOVA_time_freq.py | 6 +- .../75_cluster_ftest_spatiotemporal.py | 10 +- .../20_cluster_1samp_spatiotemporal.py | 4 +- .../30_cluster_ftest_spatiotemporal.py | 2 +- .../60_cluster_rmANOVA_spatiotemporal.py | 11 +- tutorials/time-freq/50_ssvep.py | 3 +- .../visualization/10_publication_figure.py | 4 +- tutorials/visualization/20_ui_events.py | 5 +- 605 files changed, 4706 insertions(+), 4686 deletions(-) diff --git a/examples/datasets/hf_sef_data.py b/examples/datasets/hf_sef_data.py index 36ea2cbc2bb..f9240698231 100644 --- a/examples/datasets/hf_sef_data.py +++ b/examples/datasets/hf_sef_data.py @@ -14,8 +14,9 @@ # %% -import mne import os + +import mne from mne.datasets import hf_sef fname_evoked = os.path.join(hf_sef.data_path(), "MEG/subject_b/hf_sef_15min-ave.fif") diff --git a/examples/datasets/limo_data.py b/examples/datasets/limo_data.py index 4285411dd6c..62fb0322295 100644 --- a/examples/datasets/limo_data.py +++ b/examples/datasets/limo_data.py @@ -38,14 +38,13 @@ # %% -import numpy as np import matplotlib.pyplot as plt +import numpy as np +from mne import combine_evoked from mne.datasets.limo import load_data from mne.stats import linear_regression -from mne.viz import plot_events, plot_compare_evokeds -from mne import combine_evoked - +from mne.viz import plot_compare_evokeds, plot_events print(__doc__) diff --git a/examples/datasets/opm_data.py b/examples/datasets/opm_data.py index 184ea216866..60c4a08f8fe 100644 --- a/examples/datasets/opm_data.py +++ b/examples/datasets/opm_data.py @@ -13,6 +13,7 @@ # sphinx_gallery_thumbnail_number = 4 import numpy as np + import mne data_path = mne.datasets.opm.data_path() diff --git a/examples/datasets/spm_faces_dataset_sgskip.py b/examples/datasets/spm_faces_dataset_sgskip.py index 0bda341aa4f..cdc7dc9aab0 100644 --- a/examples/datasets/spm_faces_dataset_sgskip.py +++ b/examples/datasets/spm_faces_dataset_sgskip.py @@ -27,10 +27,10 @@ import matplotlib.pyplot as plt import mne +from mne import combine_evoked, io from mne.datasets import spm_face +from mne.minimum_norm import apply_inverse, make_inverse_operator from mne.preprocessing import ICA, create_eog_epochs -from mne import io, combine_evoked -from mne.minimum_norm import make_inverse_operator, apply_inverse print(__doc__) diff --git a/examples/decoding/decoding_csp_eeg.py b/examples/decoding/decoding_csp_eeg.py index 1ee2f0ce87d..896489252f2 100644 --- a/examples/decoding/decoding_csp_eeg.py +++ b/examples/decoding/decoding_csp_eeg.py @@ -20,18 +20,17 @@ # %% -import numpy as np import matplotlib.pyplot as plt - -from sklearn.pipeline import Pipeline +import numpy as np from sklearn.discriminant_analysis import LinearDiscriminantAnalysis from sklearn.model_selection import ShuffleSplit, cross_val_score +from sklearn.pipeline import Pipeline -from mne import Epochs, pick_types, events_from_annotations +from mne import Epochs, events_from_annotations, pick_types from mne.channels import make_standard_montage -from mne.io import concatenate_raws, read_raw_edf from mne.datasets import eegbci from mne.decoding import CSP +from mne.io import concatenate_raws, read_raw_edf print(__doc__) diff --git a/examples/decoding/decoding_csp_timefreq.py b/examples/decoding/decoding_csp_timefreq.py index e6d3daa725d..c1f88588326 100644 --- a/examples/decoding/decoding_csp_timefreq.py +++ b/examples/decoding/decoding_csp_timefreq.py @@ -21,20 +21,19 @@ # %% -import numpy as np import matplotlib.pyplot as plt +import numpy as np +from sklearn.discriminant_analysis import LinearDiscriminantAnalysis +from sklearn.model_selection import StratifiedKFold, cross_val_score +from sklearn.pipeline import make_pipeline +from sklearn.preprocessing import LabelEncoder from mne import Epochs, create_info, events_from_annotations -from mne.io import concatenate_raws, read_raw_edf from mne.datasets import eegbci from mne.decoding import CSP +from mne.io import concatenate_raws, read_raw_edf from mne.time_frequency import AverageTFR -from sklearn.discriminant_analysis import LinearDiscriminantAnalysis -from sklearn.model_selection import StratifiedKFold, cross_val_score -from sklearn.pipeline import make_pipeline -from sklearn.preprocessing import LabelEncoder - # %% # Set parameters and read data event_id = dict(hands=2, feet=3) # motor imagery: hands vs feet diff --git a/examples/decoding/decoding_rsa_sgskip.py b/examples/decoding/decoding_rsa_sgskip.py index 3cc8467deb3..0b6b8e7a340 100644 --- a/examples/decoding/decoding_rsa_sgskip.py +++ b/examples/decoding/decoding_rsa_sgskip.py @@ -29,21 +29,19 @@ # %% +import matplotlib.pyplot as plt import numpy as np from pandas import read_csv -import matplotlib.pyplot as plt - +from sklearn.linear_model import LogisticRegression +from sklearn.manifold import MDS +from sklearn.metrics import roc_auc_score from sklearn.model_selection import StratifiedKFold from sklearn.pipeline import make_pipeline from sklearn.preprocessing import StandardScaler -from sklearn.linear_model import LogisticRegression -from sklearn.metrics import roc_auc_score -from sklearn.manifold import MDS import mne -from mne.io import read_raw_fif, concatenate_raws from mne.datasets import visual_92_categories - +from mne.io import concatenate_raws, read_raw_fif print(__doc__) diff --git a/examples/decoding/decoding_spatio_temporal_source.py b/examples/decoding/decoding_spatio_temporal_source.py index ad96720f640..3efbde4c047 100644 --- a/examples/decoding/decoding_spatio_temporal_source.py +++ b/examples/decoding/decoding_spatio_temporal_source.py @@ -21,17 +21,16 @@ # %% -import numpy as np import matplotlib.pyplot as plt - -from sklearn.pipeline import make_pipeline -from sklearn.preprocessing import StandardScaler +import numpy as np from sklearn.feature_selection import SelectKBest, f_classif from sklearn.linear_model import LogisticRegression +from sklearn.pipeline import make_pipeline +from sklearn.preprocessing import StandardScaler import mne +from mne.decoding import LinearModel, SlidingEstimator, cross_val_multiscore, get_coef from mne.minimum_norm import apply_inverse_epochs, read_inverse_operator -from mne.decoding import cross_val_multiscore, LinearModel, SlidingEstimator, get_coef print(__doc__) diff --git a/examples/decoding/decoding_spoc_CMC.py b/examples/decoding/decoding_spoc_CMC.py index 4e689d338d5..ba3c23a08dc 100644 --- a/examples/decoding/decoding_spoc_CMC.py +++ b/examples/decoding/decoding_spoc_CMC.py @@ -24,15 +24,14 @@ # %% import matplotlib.pyplot as plt +from sklearn.linear_model import Ridge +from sklearn.model_selection import KFold, cross_val_predict +from sklearn.pipeline import make_pipeline import mne from mne import Epochs -from mne.decoding import SPoC from mne.datasets.fieldtrip_cmc import data_path - -from sklearn.pipeline import make_pipeline -from sklearn.linear_model import Ridge -from sklearn.model_selection import KFold, cross_val_predict +from mne.decoding import SPoC # Define parameters fname = data_path() / "SubjectCMC.ds" diff --git a/examples/decoding/decoding_time_generalization_conditions.py b/examples/decoding/decoding_time_generalization_conditions.py index a018ebbe75b..beb69831b8b 100644 --- a/examples/decoding/decoding_time_generalization_conditions.py +++ b/examples/decoding/decoding_time_generalization_conditions.py @@ -20,10 +20,9 @@ # %% import matplotlib.pyplot as plt - +from sklearn.linear_model import LogisticRegression from sklearn.pipeline import make_pipeline from sklearn.preprocessing import StandardScaler -from sklearn.linear_model import LogisticRegression import mne from mne.datasets import sample diff --git a/examples/decoding/decoding_unsupervised_spatial_filter.py b/examples/decoding/decoding_unsupervised_spatial_filter.py index d215203ac3c..07c18813ab8 100644 --- a/examples/decoding/decoding_unsupervised_spatial_filter.py +++ b/examples/decoding/decoding_unsupervised_spatial_filter.py @@ -17,15 +17,14 @@ # %% -import numpy as np import matplotlib.pyplot as plt +import numpy as np +from sklearn.decomposition import PCA, FastICA import mne from mne.datasets import sample from mne.decoding import UnsupervisedSpatialFilter -from sklearn.decomposition import PCA, FastICA - print(__doc__) # Preprocess data diff --git a/examples/decoding/decoding_xdawn_eeg.py b/examples/decoding/decoding_xdawn_eeg.py index e7fac8c52e6..221a16c380c 100644 --- a/examples/decoding/decoding_xdawn_eeg.py +++ b/examples/decoding/decoding_xdawn_eeg.py @@ -16,20 +16,18 @@ # %% -import numpy as np import matplotlib.pyplot as plt - -from sklearn.model_selection import StratifiedKFold -from sklearn.pipeline import make_pipeline +import numpy as np from sklearn.linear_model import LogisticRegression from sklearn.metrics import classification_report, confusion_matrix +from sklearn.model_selection import StratifiedKFold +from sklearn.pipeline import make_pipeline from sklearn.preprocessing import MinMaxScaler -from mne import io, pick_types, read_events, Epochs, EvokedArray, create_info +from mne import Epochs, EvokedArray, create_info, io, pick_types, read_events from mne.datasets import sample -from mne.preprocessing import Xdawn from mne.decoding import Vectorizer - +from mne.preprocessing import Xdawn print(__doc__) diff --git a/examples/decoding/ems_filtering.py b/examples/decoding/ems_filtering.py index e3512a3591e..ff6296bf1f1 100644 --- a/examples/decoding/ems_filtering.py +++ b/examples/decoding/ems_filtering.py @@ -25,14 +25,14 @@ # %% -import numpy as np import matplotlib.pyplot as plt +import numpy as np +from sklearn.model_selection import StratifiedKFold import mne -from mne import io, EvokedArray +from mne import EvokedArray, io from mne.datasets import sample from mne.decoding import EMS, compute_ems -from sklearn.model_selection import StratifiedKFold print(__doc__) diff --git a/examples/decoding/linear_model_patterns.py b/examples/decoding/linear_model_patterns.py index 05b4c0591a6..4b23e5d1e56 100644 --- a/examples/decoding/linear_model_patterns.py +++ b/examples/decoding/linear_model_patterns.py @@ -22,17 +22,16 @@ # %% -import mne -from mne import io, EvokedArray -from mne.datasets import sample -from mne.decoding import Vectorizer, get_coef - -from sklearn.preprocessing import StandardScaler from sklearn.linear_model import LogisticRegression from sklearn.pipeline import make_pipeline +from sklearn.preprocessing import StandardScaler + +import mne +from mne import EvokedArray, io +from mne.datasets import sample # import a linear classifier from mne.decoding -from mne.decoding import LinearModel +from mne.decoding import LinearModel, Vectorizer, get_coef print(__doc__) diff --git a/examples/decoding/receptive_field_mtrf.py b/examples/decoding/receptive_field_mtrf.py index e927cd3cf25..799920611bf 100644 --- a/examples/decoding/receptive_field_mtrf.py +++ b/examples/decoding/receptive_field_mtrf.py @@ -28,16 +28,16 @@ # %% # sphinx_gallery_thumbnail_number = 3 -import numpy as np -import matplotlib.pyplot as plt -from scipy.io import loadmat from os.path import join -import mne -from mne.decoding import ReceptiveField +import matplotlib.pyplot as plt +import numpy as np +from scipy.io import loadmat from sklearn.model_selection import KFold from sklearn.preprocessing import scale +import mne +from mne.decoding import ReceptiveField # %% # Load the data from the publication diff --git a/examples/decoding/ssd_spatial_filters.py b/examples/decoding/ssd_spatial_filters.py index 035f2b0cf2d..a2bdcabf9a1 100644 --- a/examples/decoding/ssd_spatial_filters.py +++ b/examples/decoding/ssd_spatial_filters.py @@ -21,6 +21,7 @@ import matplotlib.pyplot as plt + import mne from mne import Epochs from mne.datasets.fieldtrip_cmc import data_path diff --git a/examples/forward/forward_sensitivity_maps.py b/examples/forward/forward_sensitivity_maps.py index dca41bb9b12..db501375b86 100644 --- a/examples/forward/forward_sensitivity_maps.py +++ b/examples/forward/forward_sensitivity_maps.py @@ -18,12 +18,13 @@ # %% +import matplotlib.pyplot as plt import numpy as np + import mne from mne.datasets import sample -from mne.source_space import compute_distance_to_sensors from mne.source_estimate import SourceEstimate -import matplotlib.pyplot as plt +from mne.source_space import compute_distance_to_sensors print(__doc__) diff --git a/examples/inverse/compute_mne_inverse_epochs_in_label.py b/examples/inverse/compute_mne_inverse_epochs_in_label.py index e779444f6cf..b7938868058 100644 --- a/examples/inverse/compute_mne_inverse_epochs_in_label.py +++ b/examples/inverse/compute_mne_inverse_epochs_in_label.py @@ -14,13 +14,12 @@ # %% -import numpy as np import matplotlib.pyplot as plt +import numpy as np import mne from mne.datasets import sample -from mne.minimum_norm import apply_inverse_epochs, read_inverse_operator -from mne.minimum_norm import apply_inverse +from mne.minimum_norm import apply_inverse, apply_inverse_epochs, read_inverse_operator print(__doc__) diff --git a/examples/inverse/compute_mne_inverse_volume.py b/examples/inverse/compute_mne_inverse_volume.py index 7b5193a081b..501bc30199a 100644 --- a/examples/inverse/compute_mne_inverse_volume.py +++ b/examples/inverse/compute_mne_inverse_volume.py @@ -14,11 +14,11 @@ # %% -from nilearn.plotting import plot_stat_map from nilearn.image import index_img +from nilearn.plotting import plot_stat_map -from mne.datasets import sample from mne import read_evokeds +from mne.datasets import sample from mne.minimum_norm import apply_inverse, read_inverse_operator print(__doc__) diff --git a/examples/inverse/custom_inverse_solver.py b/examples/inverse/custom_inverse_solver.py index 8e2a495c1dd..6798d0c80a3 100644 --- a/examples/inverse/custom_inverse_solver.py +++ b/examples/inverse/custom_inverse_solver.py @@ -23,11 +23,11 @@ import numpy as np from scipy import linalg + import mne from mne.datasets import sample from mne.viz import plot_sparse_source_estimates - data_path = sample.data_path() meg_path = data_path / "MEG" / "sample" fwd_fname = meg_path / "sample_audvis-meg-eeg-oct-6-fwd.fif" @@ -95,10 +95,10 @@ def apply_solver(solver, evoked, forward, noise_cov, loose=0.2, depth=0.8): """ # Import the necessary private functions from mne.inverse_sparse.mxne_inverse import ( + _make_sparse_stc, _prepare_gain, - is_fixed_orient, _reapply_source_weighting, - _make_sparse_stc, + is_fixed_orient, ) all_ch_names = evoked.ch_names diff --git a/examples/inverse/dics_epochs.py b/examples/inverse/dics_epochs.py index 5ea93986fda..6370275e3b2 100644 --- a/examples/inverse/dics_epochs.py +++ b/examples/inverse/dics_epochs.py @@ -17,10 +17,11 @@ # License: BSD-3-Clause import numpy as np + import mne +from mne.beamformer import apply_dics_tfr_epochs, make_dics from mne.datasets import somato -from mne.time_frequency import tfr_morlet, csd_tfr -from mne.beamformer import make_dics, apply_dics_tfr_epochs +from mne.time_frequency import csd_tfr, tfr_morlet print(__doc__) diff --git a/examples/inverse/dics_source_power.py b/examples/inverse/dics_source_power.py index 68925202b17..a140b32e7e4 100644 --- a/examples/inverse/dics_source_power.py +++ b/examples/inverse/dics_source_power.py @@ -21,10 +21,11 @@ # %% import numpy as np + import mne +from mne.beamformer import apply_dics_csd, make_dics from mne.datasets import somato from mne.time_frequency import csd_morlet -from mne.beamformer import make_dics, apply_dics_csd print(__doc__) diff --git a/examples/inverse/evoked_ers_source_power.py b/examples/inverse/evoked_ers_source_power.py index 272b0518293..0ffcf90816d 100644 --- a/examples/inverse/evoked_ers_source_power.py +++ b/examples/inverse/evoked_ers_source_power.py @@ -18,12 +18,13 @@ # %% import numpy as np + import mne +from mne.beamformer import apply_dics_csd, apply_lcmv_cov, make_dics, make_lcmv from mne.cov import compute_covariance from mne.datasets import somato +from mne.minimum_norm import apply_inverse_cov, make_inverse_operator from mne.time_frequency import csd_morlet -from mne.beamformer import make_dics, apply_dics_csd, make_lcmv, apply_lcmv_cov -from mne.minimum_norm import make_inverse_operator, apply_inverse_cov print(__doc__) diff --git a/examples/inverse/gamma_map_inverse.py b/examples/inverse/gamma_map_inverse.py index da38204d3b2..2a11f32fd41 100644 --- a/examples/inverse/gamma_map_inverse.py +++ b/examples/inverse/gamma_map_inverse.py @@ -20,9 +20,9 @@ from mne.datasets import sample from mne.inverse_sparse import gamma_map, make_stc_from_dipoles from mne.viz import ( - plot_sparse_source_estimates, - plot_dipole_locations, plot_dipole_amplitudes, + plot_dipole_locations, + plot_sparse_source_estimates, ) print(__doc__) diff --git a/examples/inverse/label_activation_from_stc.py b/examples/inverse/label_activation_from_stc.py index 358de19bff2..a154b5b1aec 100644 --- a/examples/inverse/label_activation_from_stc.py +++ b/examples/inverse/label_activation_from_stc.py @@ -17,9 +17,10 @@ # %% +import matplotlib.pyplot as plt + import mne from mne.datasets import sample -import matplotlib.pyplot as plt print(__doc__) diff --git a/examples/inverse/label_from_stc.py b/examples/inverse/label_from_stc.py index 39469e8c68b..31d057b92e6 100644 --- a/examples/inverse/label_from_stc.py +++ b/examples/inverse/label_from_stc.py @@ -18,12 +18,12 @@ # %% -import numpy as np import matplotlib.pyplot as plt +import numpy as np import mne -from mne.minimum_norm import read_inverse_operator, apply_inverse from mne.datasets import sample +from mne.minimum_norm import apply_inverse, read_inverse_operator print(__doc__) diff --git a/examples/inverse/label_source_activations.py b/examples/inverse/label_source_activations.py index 035533b4b9a..060e2506cf9 100644 --- a/examples/inverse/label_source_activations.py +++ b/examples/inverse/label_source_activations.py @@ -18,12 +18,12 @@ # %% -import matplotlib.pyplot as plt import matplotlib.patheffects as path_effects +import matplotlib.pyplot as plt import mne from mne.datasets import sample -from mne.minimum_norm import read_inverse_operator, apply_inverse +from mne.minimum_norm import apply_inverse, read_inverse_operator print(__doc__) diff --git a/examples/inverse/mixed_norm_inverse.py b/examples/inverse/mixed_norm_inverse.py index f64a9f55665..031483ae137 100644 --- a/examples/inverse/mixed_norm_inverse.py +++ b/examples/inverse/mixed_norm_inverse.py @@ -23,12 +23,12 @@ import mne from mne.datasets import sample -from mne.inverse_sparse import mixed_norm, make_stc_from_dipoles -from mne.minimum_norm import make_inverse_operator, apply_inverse +from mne.inverse_sparse import make_stc_from_dipoles, mixed_norm +from mne.minimum_norm import apply_inverse, make_inverse_operator from mne.viz import ( - plot_sparse_source_estimates, - plot_dipole_locations, plot_dipole_amplitudes, + plot_dipole_locations, + plot_sparse_source_estimates, ) print(__doc__) diff --git a/examples/inverse/mixed_source_space_inverse.py b/examples/inverse/mixed_source_space_inverse.py index f069b5e89ac..ddf3e35db45 100644 --- a/examples/inverse/mixed_source_space_inverse.py +++ b/examples/inverse/mixed_source_space_inverse.py @@ -15,11 +15,10 @@ # %% import matplotlib.pyplot as plt - from nilearn import plotting import mne -from mne.minimum_norm import make_inverse_operator, apply_inverse +from mne.minimum_norm import apply_inverse, make_inverse_operator # Set dir data_path = mne.datasets.sample.data_path() diff --git a/examples/inverse/mne_cov_power.py b/examples/inverse/mne_cov_power.py index 592664a72ef..79f3dd08a4e 100644 --- a/examples/inverse/mne_cov_power.py +++ b/examples/inverse/mne_cov_power.py @@ -27,7 +27,7 @@ import mne from mne.datasets import sample -from mne.minimum_norm import make_inverse_operator, apply_inverse_cov +from mne.minimum_norm import apply_inverse_cov, make_inverse_operator data_path = sample.data_path() subjects_dir = data_path / "subjects" diff --git a/examples/inverse/morph_volume_stc.py b/examples/inverse/morph_volume_stc.py index adf20db7905..c5fc2b5130c 100644 --- a/examples/inverse/morph_volume_stc.py +++ b/examples/inverse/morph_volume_stc.py @@ -28,10 +28,11 @@ import os import nibabel as nib +from nilearn.plotting import plot_glass_brain + import mne -from mne.datasets import sample, fetch_fsaverage +from mne.datasets import fetch_fsaverage, sample from mne.minimum_norm import apply_inverse, read_inverse_operator -from nilearn.plotting import plot_glass_brain print(__doc__) diff --git a/examples/inverse/multi_dipole_model.py b/examples/inverse/multi_dipole_model.py index 6a66f89f624..b05ead57498 100644 --- a/examples/inverse/multi_dipole_model.py +++ b/examples/inverse/multi_dipole_model.py @@ -30,13 +30,14 @@ ############################################################################### # Importing everything and setting up the data paths for the MNE-Sample # dataset. -import mne -from mne.datasets import sample -from mne.channels import read_vectorview_selection -from mne.minimum_norm import make_inverse_operator, apply_inverse, apply_inverse_epochs import matplotlib.pyplot as plt import numpy as np +import mne +from mne.channels import read_vectorview_selection +from mne.datasets import sample +from mne.minimum_norm import apply_inverse, apply_inverse_epochs, make_inverse_operator + data_path = sample.data_path() meg_path = data_path / "MEG" / "sample" raw_fname = meg_path / "sample_audvis_raw.fif" diff --git a/examples/inverse/multidict_reweighted_tfmxne.py b/examples/inverse/multidict_reweighted_tfmxne.py index dcaaf5575e9..ce45efc8707 100644 --- a/examples/inverse/multidict_reweighted_tfmxne.py +++ b/examples/inverse/multidict_reweighted_tfmxne.py @@ -28,7 +28,7 @@ import mne from mne.datasets import somato -from mne.inverse_sparse import tf_mixed_norm, make_stc_from_dipoles +from mne.inverse_sparse import make_stc_from_dipoles, tf_mixed_norm from mne.viz import plot_sparse_source_estimates print(__doc__) diff --git a/examples/inverse/psf_ctf_label_leakage.py b/examples/inverse/psf_ctf_label_leakage.py index d74663d369a..1ca6d5de8a1 100644 --- a/examples/inverse/psf_ctf_label_leakage.py +++ b/examples/inverse/psf_ctf_label_leakage.py @@ -20,20 +20,18 @@ # %% -import numpy as np import matplotlib.pyplot as plt +import numpy as np +from mne_connectivity.viz import plot_connectivity_circle import mne from mne.datasets import sample from mne.minimum_norm import ( - read_inverse_operator, - make_inverse_resolution_matrix, get_point_spread, + make_inverse_resolution_matrix, + read_inverse_operator, ) - from mne.viz import circular_layout -from mne_connectivity.viz import plot_connectivity_circle - print(__doc__) diff --git a/examples/inverse/psf_ctf_vertices.py b/examples/inverse/psf_ctf_vertices.py index 0ec01a865dc..efbfe6ff470 100644 --- a/examples/inverse/psf_ctf_vertices.py +++ b/examples/inverse/psf_ctf_vertices.py @@ -17,9 +17,9 @@ import mne from mne.datasets import sample from mne.minimum_norm import ( - make_inverse_resolution_matrix, get_cross_talk, get_point_spread, + make_inverse_resolution_matrix, ) print(__doc__) diff --git a/examples/inverse/psf_ctf_vertices_lcmv.py b/examples/inverse/psf_ctf_vertices_lcmv.py index 7f3d2a4207e..cd5527c88c9 100644 --- a/examples/inverse/psf_ctf_vertices_lcmv.py +++ b/examples/inverse/psf_ctf_vertices_lcmv.py @@ -16,8 +16,8 @@ # %% import mne -from mne.datasets import sample from mne.beamformer import make_lcmv, make_lcmv_resolution_matrix +from mne.datasets import sample from mne.minimum_norm import get_cross_talk print(__doc__) diff --git a/examples/inverse/psf_volume.py b/examples/inverse/psf_volume.py index f2e465c1b20..1074527d6af 100644 --- a/examples/inverse/psf_volume.py +++ b/examples/inverse/psf_volume.py @@ -19,7 +19,7 @@ import mne from mne.datasets import sample -from mne.minimum_norm import make_inverse_resolution_matrix, get_point_spread +from mne.minimum_norm import get_point_spread, make_inverse_resolution_matrix print(__doc__) diff --git a/examples/inverse/rap_music.py b/examples/inverse/rap_music.py index f87d733ef37..4c294d06d7b 100644 --- a/examples/inverse/rap_music.py +++ b/examples/inverse/rap_music.py @@ -16,10 +16,9 @@ # %% import mne - -from mne.datasets import sample from mne.beamformer import rap_music -from mne.viz import plot_dipole_locations, plot_dipole_amplitudes +from mne.datasets import sample +from mne.viz import plot_dipole_amplitudes, plot_dipole_locations print(__doc__) diff --git a/examples/inverse/resolution_metrics.py b/examples/inverse/resolution_metrics.py index e3e98827bea..594a37a5161 100644 --- a/examples/inverse/resolution_metrics.py +++ b/examples/inverse/resolution_metrics.py @@ -19,8 +19,7 @@ import mne from mne.datasets import sample -from mne.minimum_norm import make_inverse_resolution_matrix -from mne.minimum_norm import resolution_metrics +from mne.minimum_norm import make_inverse_resolution_matrix, resolution_metrics print(__doc__) diff --git a/examples/inverse/snr_estimate.py b/examples/inverse/snr_estimate.py index 4a88a9d13c4..6422c0a1d05 100644 --- a/examples/inverse/snr_estimate.py +++ b/examples/inverse/snr_estimate.py @@ -14,9 +14,9 @@ # %% +from mne import read_evokeds from mne.datasets.sample import data_path from mne.minimum_norm import read_inverse_operator -from mne import read_evokeds from mne.viz import plot_snr_estimate print(__doc__) diff --git a/examples/inverse/source_space_snr.py b/examples/inverse/source_space_snr.py index c7077d091e5..690f16f7eb8 100644 --- a/examples/inverse/source_space_snr.py +++ b/examples/inverse/source_space_snr.py @@ -17,11 +17,12 @@ # sphinx_gallery_thumbnail_number = 2 +import matplotlib.pyplot as plt +import numpy as np + import mne from mne.datasets import sample -from mne.minimum_norm import make_inverse_operator, apply_inverse -import numpy as np -import matplotlib.pyplot as plt +from mne.minimum_norm import apply_inverse, make_inverse_operator print(__doc__) diff --git a/examples/inverse/time_frequency_mixed_norm_inverse.py b/examples/inverse/time_frequency_mixed_norm_inverse.py index dd2b2db2b3a..5d1e680776a 100644 --- a/examples/inverse/time_frequency_mixed_norm_inverse.py +++ b/examples/inverse/time_frequency_mixed_norm_inverse.py @@ -30,12 +30,12 @@ import mne from mne.datasets import sample -from mne.minimum_norm import make_inverse_operator, apply_inverse -from mne.inverse_sparse import tf_mixed_norm, make_stc_from_dipoles +from mne.inverse_sparse import make_stc_from_dipoles, tf_mixed_norm +from mne.minimum_norm import apply_inverse, make_inverse_operator from mne.viz import ( - plot_sparse_source_estimates, - plot_dipole_locations, plot_dipole_amplitudes, + plot_dipole_locations, + plot_sparse_source_estimates, ) print(__doc__) diff --git a/examples/inverse/trap_music.py b/examples/inverse/trap_music.py index 6a85d557188..5262b4b9515 100644 --- a/examples/inverse/trap_music.py +++ b/examples/inverse/trap_music.py @@ -16,10 +16,9 @@ # %% import mne - -from mne.datasets import sample from mne.beamformer import trap_music -from mne.viz import plot_dipole_locations, plot_dipole_amplitudes +from mne.datasets import sample +from mne.viz import plot_dipole_amplitudes, plot_dipole_locations print(__doc__) diff --git a/examples/inverse/vector_mne_solution.py b/examples/inverse/vector_mne_solution.py index 2733f40acd1..0511a2c7821 100644 --- a/examples/inverse/vector_mne_solution.py +++ b/examples/inverse/vector_mne_solution.py @@ -26,9 +26,10 @@ # %% import numpy as np + import mne from mne.datasets import sample -from mne.minimum_norm import read_inverse_operator, apply_inverse +from mne.minimum_norm import apply_inverse, read_inverse_operator print(__doc__) diff --git a/examples/io/elekta_epochs.py b/examples/io/elekta_epochs.py index f632b0fbae3..0b8a3a0f162 100644 --- a/examples/io/elekta_epochs.py +++ b/examples/io/elekta_epochs.py @@ -15,8 +15,9 @@ # %% -import mne import os + +import mne from mne.datasets import multimodal fname_raw = os.path.join(multimodal.data_path(), "multimodal_raw.fif") diff --git a/examples/io/read_neo_format.py b/examples/io/read_neo_format.py index 7847e23dcfa..2146764d522 100644 --- a/examples/io/read_neo_format.py +++ b/examples/io/read_neo_format.py @@ -14,6 +14,7 @@ # %% import neo + import mne # %% diff --git a/examples/preprocessing/css.py b/examples/preprocessing/css.py index 73e86c1b389..ab7309d98d3 100644 --- a/examples/preprocessing/css.py +++ b/examples/preprocessing/css.py @@ -17,12 +17,12 @@ """ # Author: John G Samuelsson -import numpy as np import matplotlib.pyplot as plt +import numpy as np import mne from mne.datasets import sample -from mne.simulation import simulate_sparse_stc, simulate_evoked +from mne.simulation import simulate_evoked, simulate_sparse_stc ############################################################################### # Load sample subject data diff --git a/examples/preprocessing/define_target_events.py b/examples/preprocessing/define_target_events.py index 51e0fdbb960..86ed28c7505 100644 --- a/examples/preprocessing/define_target_events.py +++ b/examples/preprocessing/define_target_events.py @@ -21,11 +21,12 @@ # %% +import matplotlib.pyplot as plt + import mne from mne import io -from mne.event import define_target_events from mne.datasets import sample -import matplotlib.pyplot as plt +from mne.event import define_target_events print(__doc__) diff --git a/examples/preprocessing/eeg_bridging.py b/examples/preprocessing/eeg_bridging.py index 30cdde8502b..7eadb7239d2 100644 --- a/examples/preprocessing/eeg_bridging.py +++ b/examples/preprocessing/eeg_bridging.py @@ -35,8 +35,8 @@ # sphinx_gallery_thumbnail_number = 2 -import numpy as np import matplotlib.pyplot as plt +import numpy as np from matplotlib.colors import LinearSegmentedColormap import mne diff --git a/examples/preprocessing/eeg_csd.py b/examples/preprocessing/eeg_csd.py index 892f856e75e..98d968d4c94 100644 --- a/examples/preprocessing/eeg_csd.py +++ b/examples/preprocessing/eeg_csd.py @@ -20,8 +20,8 @@ # sphinx_gallery_thumbnail_number = 6 -import numpy as np import matplotlib.pyplot as plt +import numpy as np import mne from mne.datasets import sample diff --git a/examples/preprocessing/eog_artifact_histogram.py b/examples/preprocessing/eog_artifact_histogram.py index 2d51370b571..0f9de66fda7 100644 --- a/examples/preprocessing/eog_artifact_histogram.py +++ b/examples/preprocessing/eog_artifact_histogram.py @@ -15,8 +15,8 @@ # %% -import numpy as np import matplotlib.pyplot as plt +import numpy as np import mne from mne import io diff --git a/examples/preprocessing/eog_regression.py b/examples/preprocessing/eog_regression.py index 2123974dde4..621195d5818 100644 --- a/examples/preprocessing/eog_regression.py +++ b/examples/preprocessing/eog_regression.py @@ -22,10 +22,11 @@ # We begin as always by importing the necessary Python modules and loading some # data, in this case the :ref:`MNE sample dataset `. +from matplotlib import pyplot as plt + import mne from mne.datasets import sample from mne.preprocessing import EOGRegression -from matplotlib import pyplot as plt print(__doc__) diff --git a/examples/preprocessing/find_ref_artifacts.py b/examples/preprocessing/find_ref_artifacts.py index f64e85372a4..ca6a2833298 100644 --- a/examples/preprocessing/find_ref_artifacts.py +++ b/examples/preprocessing/find_ref_artifacts.py @@ -32,11 +32,12 @@ # %% +import numpy as np + import mne from mne import io from mne.datasets import refmeg_noise from mne.preprocessing import ICA -import numpy as np print(__doc__) diff --git a/examples/preprocessing/fnirs_artifact_removal.py b/examples/preprocessing/fnirs_artifact_removal.py index d669d6ce09c..3d842ce92a3 100644 --- a/examples/preprocessing/fnirs_artifact_removal.py +++ b/examples/preprocessing/fnirs_artifact_removal.py @@ -16,8 +16,8 @@ # %% import os -import mne +import mne from mne.preprocessing.nirs import ( optical_density, temporal_derivative_distribution_repair, diff --git a/examples/preprocessing/ica_comparison.py b/examples/preprocessing/ica_comparison.py index 6aa601dd5fa..51d1bc7974e 100644 --- a/examples/preprocessing/ica_comparison.py +++ b/examples/preprocessing/ica_comparison.py @@ -18,9 +18,8 @@ from time import time import mne -from mne.preprocessing import ICA from mne.datasets import sample - +from mne.preprocessing import ICA print(__doc__) diff --git a/examples/preprocessing/muscle_detection.py b/examples/preprocessing/muscle_detection.py index 37bd021d853..011e6c23e30 100644 --- a/examples/preprocessing/muscle_detection.py +++ b/examples/preprocessing/muscle_detection.py @@ -32,11 +32,11 @@ import matplotlib.pyplot as plt import numpy as np + from mne.datasets.brainstorm import bst_auditory from mne.io import read_raw_ctf from mne.preprocessing import annotate_muscle_zscore - # Load data data_path = bst_auditory.data_path() raw_fname = data_path / "MEG" / "bst_auditory" / "S01_AEF_20131218_01.ds" diff --git a/examples/preprocessing/otp.py b/examples/preprocessing/otp.py index a05eaf5c6ce..7e5e28561fb 100644 --- a/examples/preprocessing/otp.py +++ b/examples/preprocessing/otp.py @@ -14,9 +14,9 @@ # %% -import mne import numpy as np +import mne from mne import find_events, fit_dipole from mne.datasets.brainstorm import bst_phantom_elekta from mne.io import read_raw_fif diff --git a/examples/preprocessing/shift_evoked.py b/examples/preprocessing/shift_evoked.py index c16becc679c..27c4bc45f02 100644 --- a/examples/preprocessing/shift_evoked.py +++ b/examples/preprocessing/shift_evoked.py @@ -13,6 +13,7 @@ # %% import matplotlib.pyplot as plt + import mne from mne.datasets import sample diff --git a/examples/preprocessing/xdawn_denoising.py b/examples/preprocessing/xdawn_denoising.py index b6eed43d142..67082d60947 100644 --- a/examples/preprocessing/xdawn_denoising.py +++ b/examples/preprocessing/xdawn_denoising.py @@ -25,7 +25,7 @@ # %% -from mne import io, compute_raw_covariance, read_events, pick_types, Epochs +from mne import Epochs, compute_raw_covariance, io, pick_types, read_events from mne.datasets import sample from mne.preprocessing import Xdawn from mne.viz import plot_epochs_image diff --git a/examples/simulation/plot_stc_metrics.py b/examples/simulation/plot_stc_metrics.py index 105c66d7e12..f7dfa657569 100644 --- a/examples/simulation/plot_stc_metrics.py +++ b/examples/simulation/plot_stc_metrics.py @@ -13,20 +13,21 @@ # # License: BSD (3-clause) -import numpy as np -import matplotlib.pyplot as plt from functools import partial +import matplotlib.pyplot as plt +import numpy as np + import mne from mne.datasets import sample -from mne.minimum_norm import make_inverse_operator, apply_inverse +from mne.minimum_norm import apply_inverse, make_inverse_operator from mne.simulation.metrics import ( - region_localization_error, + cosine_score, f1_score, + peak_position_error, precision_score, recall_score, - cosine_score, - peak_position_error, + region_localization_error, spatial_deviation_error, ) diff --git a/examples/simulation/simulate_evoked_data.py b/examples/simulation/simulate_evoked_data.py index 0a8d69a66ed..8f09c12e40c 100644 --- a/examples/simulation/simulate_evoked_data.py +++ b/examples/simulation/simulate_evoked_data.py @@ -14,14 +14,14 @@ # %% -import numpy as np import matplotlib.pyplot as plt +import numpy as np import mne from mne.datasets import sample +from mne.simulation import simulate_evoked, simulate_sparse_stc from mne.time_frequency import fit_iir_model_raw from mne.viz import plot_sparse_source_estimates -from mne.simulation import simulate_sparse_stc, simulate_evoked print(__doc__) diff --git a/examples/simulation/simulate_raw_data.py b/examples/simulation/simulate_raw_data.py index 902429717c2..4cf88a9930b 100644 --- a/examples/simulation/simulate_raw_data.py +++ b/examples/simulation/simulate_raw_data.py @@ -16,18 +16,18 @@ # %% -import numpy as np import matplotlib.pyplot as plt +import numpy as np import mne -from mne import find_events, Epochs, compute_covariance, make_ad_hoc_cov +from mne import Epochs, compute_covariance, find_events, make_ad_hoc_cov from mne.datasets import sample from mne.simulation import ( - simulate_sparse_stc, - simulate_raw, - add_noise, add_ecg, add_eog, + add_noise, + simulate_raw, + simulate_sparse_stc, ) print(__doc__) @@ -50,7 +50,7 @@ def data_fun(times): - """Generate time-staggered sinusoids at harmonics of 10Hz""" + """Generate time-staggered sinusoids at harmonics of 10Hz.""" global n n_samp = len(times) window = np.zeros(n_samp) diff --git a/examples/stats/cluster_stats_evoked.py b/examples/stats/cluster_stats_evoked.py index 1e21cdb7617..112ee80d9ab 100644 --- a/examples/stats/cluster_stats_evoked.py +++ b/examples/stats/cluster_stats_evoked.py @@ -20,8 +20,8 @@ import mne from mne import io -from mne.stats import permutation_cluster_test from mne.datasets import sample +from mne.stats import permutation_cluster_test print(__doc__) diff --git a/examples/stats/fdr_stats_evoked.py b/examples/stats/fdr_stats_evoked.py index 94239f887df..0b0b1b5f935 100644 --- a/examples/stats/fdr_stats_evoked.py +++ b/examples/stats/fdr_stats_evoked.py @@ -16,9 +16,9 @@ # %% +import matplotlib.pyplot as plt import numpy as np from scipy import stats -import matplotlib.pyplot as plt import mne from mne import io diff --git a/examples/stats/sensor_permutation_test.py b/examples/stats/sensor_permutation_test.py index 5edca4f605a..ee548b364f3 100644 --- a/examples/stats/sensor_permutation_test.py +++ b/examples/stats/sensor_permutation_test.py @@ -20,8 +20,8 @@ import mne from mne import io -from mne.stats import permutation_t_test from mne.datasets import sample +from mne.stats import permutation_t_test print(__doc__) diff --git a/examples/stats/sensor_regression.py b/examples/stats/sensor_regression.py index 2b17927b28b..4d5b02782f3 100644 --- a/examples/stats/sensor_regression.py +++ b/examples/stats/sensor_regression.py @@ -32,10 +32,11 @@ # %% import pandas as pd + import mne -from mne.stats import linear_regression, fdr_correction -from mne.viz import plot_compare_evokeds from mne.datasets import kiloword +from mne.stats import fdr_correction, linear_regression +from mne.viz import plot_compare_evokeds # Load the data path = kiloword.data_path() / "kword_metadata-epo.fif" diff --git a/examples/time_frequency/compute_csd.py b/examples/time_frequency/compute_csd.py index 0de5482be1e..b87f284ec63 100644 --- a/examples/time_frequency/compute_csd.py +++ b/examples/time_frequency/compute_csd.py @@ -22,7 +22,7 @@ # %% import mne from mne.datasets import sample -from mne.time_frequency import csd_fourier, csd_multitaper, csd_morlet +from mne.time_frequency import csd_fourier, csd_morlet, csd_multitaper print(__doc__) diff --git a/examples/time_frequency/compute_source_psd_epochs.py b/examples/time_frequency/compute_source_psd_epochs.py index 745fc69717e..e28e28bf5e9 100644 --- a/examples/time_frequency/compute_source_psd_epochs.py +++ b/examples/time_frequency/compute_source_psd_epochs.py @@ -19,7 +19,7 @@ import mne from mne.datasets import sample -from mne.minimum_norm import read_inverse_operator, compute_source_psd_epochs +from mne.minimum_norm import compute_source_psd_epochs, read_inverse_operator print(__doc__) diff --git a/examples/time_frequency/source_label_time_frequency.py b/examples/time_frequency/source_label_time_frequency.py index 2e7cc4d3592..f88d1ce2c50 100644 --- a/examples/time_frequency/source_label_time_frequency.py +++ b/examples/time_frequency/source_label_time_frequency.py @@ -19,8 +19,8 @@ # %% -import numpy as np import matplotlib.pyplot as plt +import numpy as np import mne from mne import io diff --git a/examples/time_frequency/source_power_spectrum.py b/examples/time_frequency/source_power_spectrum.py index a2aab813930..5eaec30cb78 100644 --- a/examples/time_frequency/source_power_spectrum.py +++ b/examples/time_frequency/source_power_spectrum.py @@ -19,7 +19,7 @@ import mne from mne import io from mne.datasets import sample -from mne.minimum_norm import read_inverse_operator, compute_source_psd +from mne.minimum_norm import compute_source_psd, read_inverse_operator print(__doc__) diff --git a/examples/time_frequency/temporal_whitening.py b/examples/time_frequency/temporal_whitening.py index de70216461b..38863baf227 100644 --- a/examples/time_frequency/temporal_whitening.py +++ b/examples/time_frequency/temporal_whitening.py @@ -15,13 +15,13 @@ # %% +import matplotlib.pyplot as plt import numpy as np from scipy import signal -import matplotlib.pyplot as plt import mne -from mne.time_frequency import fit_iir_model_raw from mne.datasets import sample +from mne.time_frequency import fit_iir_model_raw print(__doc__) diff --git a/examples/time_frequency/time_frequency_erds.py b/examples/time_frequency/time_frequency_erds.py index 5b4040b608e..8f7f9a9d5fd 100644 --- a/examples/time_frequency/time_frequency_erds.py +++ b/examples/time_frequency/time_frequency_erds.py @@ -34,16 +34,17 @@ # %% # As usual, we import everything we need. -import numpy as np import matplotlib.pyplot as plt -from matplotlib.colors import TwoSlopeNorm +import numpy as np import pandas as pd import seaborn as sns +from matplotlib.colors import TwoSlopeNorm + import mne from mne.datasets import eegbci from mne.io import concatenate_raws, read_raw_edf -from mne.time_frequency import tfr_multitaper from mne.stats import permutation_cluster_1samp_test as pcluster_test +from mne.time_frequency import tfr_multitaper # %% # First, we load and preprocess the data. We use runs 6, 10, and 14 from diff --git a/examples/time_frequency/time_frequency_simulated.py b/examples/time_frequency/time_frequency_simulated.py index c6f00a9da32..7b3a08faee5 100644 --- a/examples/time_frequency/time_frequency_simulated.py +++ b/examples/time_frequency/time_frequency_simulated.py @@ -23,15 +23,15 @@ import numpy as np from matplotlib import pyplot as plt -from mne import create_info, Epochs +from mne import Epochs, create_info from mne.baseline import rescale from mne.io import RawArray from mne.time_frequency import ( + AverageTFR, + tfr_array_morlet, + tfr_morlet, tfr_multitaper, tfr_stockwell, - tfr_morlet, - tfr_array_morlet, - AverageTFR, ) from mne.viz import centers_to_edges diff --git a/examples/visualization/3d_to_2d.py b/examples/visualization/3d_to_2d.py index 966e97f76ac..586f8bac734 100644 --- a/examples/visualization/3d_to_2d.py +++ b/examples/visualization/3d_to_2d.py @@ -30,8 +30,12 @@ import mne from mne.io.fiff.raw import read_raw_fif -from mne.viz import ClickableImage # noqa: F401 -from mne.viz import plot_alignment, set_3d_view, snapshot_brain_montage +from mne.viz import ( + ClickableImage, # noqa: F401 + plot_alignment, + set_3d_view, + snapshot_brain_montage, +) misc_path = mne.datasets.misc.data_path() subjects_dir = misc_path / "ecog" diff --git a/examples/visualization/channel_epochs_image.py b/examples/visualization/channel_epochs_image.py index 618330ec44d..86c8f626db2 100644 --- a/examples/visualization/channel_epochs_image.py +++ b/examples/visualization/channel_epochs_image.py @@ -20,8 +20,8 @@ # %% -import numpy as np import matplotlib.pyplot as plt +import numpy as np import mne from mne import io diff --git a/examples/visualization/evoked_arrowmap.py b/examples/visualization/evoked_arrowmap.py index 312b1166370..01bb7bfb405 100644 --- a/examples/visualization/evoked_arrowmap.py +++ b/examples/visualization/evoked_arrowmap.py @@ -24,10 +24,11 @@ # %% import numpy as np + import mne +from mne import read_evokeds from mne.datasets import sample from mne.datasets.brainstorm import bst_raw -from mne import read_evokeds from mne.viz import plot_arrowmap print(__doc__) diff --git a/examples/visualization/evoked_topomap.py b/examples/visualization/evoked_topomap.py index dfd6be7f0f3..0f64f60cfe6 100644 --- a/examples/visualization/evoked_topomap.py +++ b/examples/visualization/evoked_topomap.py @@ -20,11 +20,11 @@ # %% # sphinx_gallery_thumbnail_number = 5 -import numpy as np import matplotlib.pyplot as plt +import numpy as np -from mne.datasets import sample from mne import read_evokeds +from mne.datasets import sample print(__doc__) diff --git a/examples/visualization/evoked_whitening.py b/examples/visualization/evoked_whitening.py index 1d1575a83b6..ce460778b07 100644 --- a/examples/visualization/evoked_whitening.py +++ b/examples/visualization/evoked_whitening.py @@ -24,10 +24,9 @@ # %% import mne - from mne import io -from mne.datasets import sample from mne.cov import compute_covariance +from mne.datasets import sample print(__doc__) diff --git a/examples/visualization/montage_sgskip.py b/examples/visualization/montage_sgskip.py index 521e4e87a16..95e9912c47d 100644 --- a/examples/visualization/montage_sgskip.py +++ b/examples/visualization/montage_sgskip.py @@ -15,6 +15,7 @@ # %% import os.path as op + import numpy as np import mne @@ -22,7 +23,6 @@ from mne.datasets import fetch_fsaverage from mne.viz import set_3d_title, set_3d_view - # %% # Check all montages against a sphere diff --git a/examples/visualization/roi_erpimage_by_rt.py b/examples/visualization/roi_erpimage_by_rt.py index 02264d8263e..770fa2e13e3 100644 --- a/examples/visualization/roi_erpimage_by_rt.py +++ b/examples/visualization/roi_erpimage_by_rt.py @@ -22,8 +22,8 @@ # %% import mne -from mne.event import define_target_events from mne.channels import make_1020_channel_selections +from mne.event import define_target_events print(__doc__) diff --git a/examples/visualization/ssp_projs_sensitivity_map.py b/examples/visualization/ssp_projs_sensitivity_map.py index d51c498e423..02e8efa45ec 100644 --- a/examples/visualization/ssp_projs_sensitivity_map.py +++ b/examples/visualization/ssp_projs_sensitivity_map.py @@ -17,7 +17,6 @@ import matplotlib.pyplot as plt from mne import read_forward_solution, read_proj, sensitivity_map - from mne.datasets import sample print(__doc__) diff --git a/examples/visualization/topo_compare_conditions.py b/examples/visualization/topo_compare_conditions.py index 742565fc1fd..208f5f16125 100644 --- a/examples/visualization/topo_compare_conditions.py +++ b/examples/visualization/topo_compare_conditions.py @@ -20,10 +20,10 @@ import matplotlib.pyplot as plt -import mne -from mne.viz import plot_evoked_topo +import mne from mne.datasets import sample +from mne.viz import plot_evoked_topo print(__doc__) diff --git a/examples/visualization/topo_customized.py b/examples/visualization/topo_customized.py index 02c0435b25f..62052d65aa9 100644 --- a/examples/visualization/topo_customized.py +++ b/examples/visualization/topo_customized.py @@ -19,13 +19,13 @@ # %% -import numpy as np import matplotlib.pyplot as plt +import numpy as np import mne -from mne.viz import iter_topography from mne import io from mne.datasets import sample +from mne.viz import iter_topography print(__doc__) diff --git a/mne/__init__.pyi b/mne/__init__.pyi index 5492a3dfdb5..d4895473c16 100644 --- a/mne/__init__.pyi +++ b/mne/__init__.pyi @@ -221,13 +221,32 @@ from . import ( minimum_norm, preprocessing, report, - source_space, simulation, + source_space, stats, surface, time_frequency, viz, ) +from ._fiff.meas_info import Info, create_info +from ._fiff.pick import ( + channel_indices_by_type, + channel_type, + pick_channels, + pick_channels_cov, + pick_channels_forward, + pick_channels_regexp, + pick_info, + pick_types, + pick_types_forward, +) +from ._fiff.proj import Projection +from ._fiff.reference import ( + add_reference_channels, + set_bipolar_reference, + set_eeg_reference, +) +from ._fiff.what import what from ._freesurfer import ( get_volume_labels_from_aseg, head_to_mni, @@ -256,9 +275,9 @@ from .bem import ( ) from .channels import ( equalize_channels, - rename_channels, find_layout, read_vectorview_selection, + rename_channels, ) from .coreg import ( create_default_subject, @@ -300,8 +319,8 @@ from .event import ( from .evoked import Evoked, EvokedArray, combine_evoked, read_evokeds, write_evokeds from .forward import ( Forward, - apply_forward_raw, apply_forward, + apply_forward_raw, average_forward_solutions, convert_forward_solution, make_field_map, @@ -320,30 +339,11 @@ from .io import ( from .io.base import concatenate_raws, match_channel_orders from .io.eeglab import read_epochs_eeglab from .io.kit import read_epochs_kit -from ._fiff.meas_info import Info, create_info -from ._fiff.pick import ( - channel_indices_by_type, - channel_type, - pick_channels_cov, - pick_channels_forward, - pick_channels_regexp, - pick_channels, - pick_info, - pick_types_forward, - pick_types, -) -from ._fiff.proj import Projection -from ._fiff.reference import ( - add_reference_channels, - set_bipolar_reference, - set_eeg_reference, -) -from ._fiff.what import what from .label import ( BiHemiLabel, + Label, grow_labels, label_sign_flip, - Label, labels_to_stc, morph_labels, random_parcellation, @@ -355,13 +355,13 @@ from .label import ( write_labels_to_annot, ) from .misc import parse_config, read_reject_parameters -from .morph_map import read_morph_map from .morph import ( SourceMorph, compute_source_morph, grade_to_vertices, read_source_morph, ) +from .morph_map import read_morph_map from .proj import ( compute_proj_epochs, compute_proj_evoked, @@ -413,8 +413,8 @@ from .surface import ( ) from .transforms import Transform, read_trans, transform_surface_to, write_trans from .utils import ( - get_config_path, get_config, + get_config_path, grand_average, open_docs, set_cache_dir, diff --git a/mne/_fiff/_digitization.py b/mne/_fiff/_digitization.py index e70a9410af5..932429d9a11 100644 --- a/mne/_fiff/_digitization.py +++ b/mne/_fiff/_digitization.py @@ -11,11 +11,10 @@ import numpy as np -from ..utils import logger, warn, Bunch, _validate_type, _check_fname, verbose - +from ..utils import Bunch, _check_fname, _validate_type, logger, verbose, warn from .constants import FIFF, _coord_frame_named -from .tree import dir_tree_find from .tag import read_tag +from .tree import dir_tree_find from .write import start_and_end_file, write_dig_points _dig_kind_dict = { @@ -524,8 +523,8 @@ def _make_dig_points( def _call_make_dig_points(nasion, lpa, rpa, hpi, extra, convert=True): from ..transforms import ( - apply_trans, Transform, + apply_trans, get_ras_to_neuromag_trans, ) diff --git a/mne/_fiff/compensator.py b/mne/_fiff/compensator.py index 2a5334e7138..6b28c94d9ab 100644 --- a/mne/_fiff/compensator.py +++ b/mne/_fiff/compensator.py @@ -1,7 +1,7 @@ import numpy as np -from .constants import FIFF from ..utils import fill_doc +from .constants import FIFF def get_current_comp(info): diff --git a/mne/_fiff/ctf_comp.py b/mne/_fiff/ctf_comp.py index a16d1c79607..6fc2aa90c0b 100644 --- a/mne/_fiff/ctf_comp.py +++ b/mne/_fiff/ctf_comp.py @@ -8,13 +8,12 @@ import numpy as np +from ..utils import _pl, logger, verbose from .constants import FIFF +from .matrix import _read_named_matrix, write_named_matrix from .tag import read_tag from .tree import dir_tree_find -from .write import start_block, end_block, write_int -from .matrix import write_named_matrix, _read_named_matrix - -from ..utils import logger, verbose, _pl +from .write import end_block, start_block, write_int def _add_kind(one): diff --git a/mne/_fiff/matrix.py b/mne/_fiff/matrix.py index 3699278d2de..189f0dbf227 100644 --- a/mne/_fiff/matrix.py +++ b/mne/_fiff/matrix.py @@ -3,16 +3,16 @@ # # License: BSD-3-Clause +from ..utils import logger from .constants import FIFF from .tag import find_tag, has_tag from .write import ( - write_int, - start_block, end_block, + start_block, write_float_matrix, + write_int, write_name_list, ) -from ..utils import logger def _transpose_named_matrix(mat): diff --git a/mne/_fiff/meas_info.py b/mne/_fiff/meas_info.py index f1e67fa8306..7b74e8c0ead 100644 --- a/mne/_fiff/meas_info.py +++ b/mne/_fiff/meas_info.py @@ -5,97 +5,97 @@ # # License: BSD-3-Clause -from collections import Counter, OrderedDict -from collections.abc import Mapping import contextlib -from copy import deepcopy import datetime -from io import BytesIO import operator -from textwrap import shorten import string import uuid +from collections import Counter, OrderedDict +from collections.abc import Mapping +from copy import deepcopy +from io import BytesIO +from textwrap import shorten import numpy as np +from ..defaults import _handle_default +from ..html_templates import _get_html_template +from ..utils import ( + _check_fname, + _check_on_missing, + _check_option, + _dt_to_stamp, + _is_numeric, + _on_missing, + _pl, + _stamp_to_dt, + _validate_type, + check_fname, + fill_doc, + logger, + object_diff, + repr_html, + verbose, + warn, +) +from ._digitization import ( + DigPoint, + _dig_kind_ints, + _dig_kind_proper, + _dig_kind_rev, + _format_dig_points, + _get_data_as_dict_from_dig, + _read_dig_fif, + write_dig, +) +from .compensator import get_current_comp +from .constants import FIFF, _ch_unit_mul_named, _coord_frame_named +from .ctf_comp import _read_ctf_comp, write_ctf_comp +from .open import fiff_open from .pick import ( + _DATA_CH_TYPES_SPLIT, + _contains_ch_type, + _picks_to_idx, channel_type, get_channel_type_constants, pick_types, - _picks_to_idx, - _contains_ch_type, - _DATA_CH_TYPES_SPLIT, -) -from .constants import FIFF, _coord_frame_named, _ch_unit_mul_named -from .open import fiff_open -from .tree import dir_tree_find -from .tag import ( - read_tag, - find_tag, - _ch_coord_dict, - _update_ch_info_named, - _rename_list, - _int_item, - _float_item, ) +from .proc_history import _read_proc_history, _write_proc_history from .proj import ( - _read_proj, - _write_proj, - _uniquify_projs, + Projection, _normalize_proj, _proj_equal, - Projection, + _read_proj, + _uniquify_projs, + _write_proj, ) -from .ctf_comp import _read_ctf_comp, write_ctf_comp +from .tag import ( + _ch_coord_dict, + _float_item, + _int_item, + _rename_list, + _update_ch_info_named, + find_tag, + read_tag, +) +from .tree import dir_tree_find from .write import ( + DATE_NONE, + _safe_name_list, + end_block, start_and_end_file, start_block, - end_block, - write_string, + write_ch_info, + write_coord_trans, write_dig_points, write_float, - write_int, - write_coord_trans, - write_ch_info, - write_julian, write_float_matrix, write_id, - DATE_NONE, - _safe_name_list, + write_int, + write_julian, write_name_list_sanitized, + write_string, ) -from .proc_history import _read_proc_history, _write_proc_history -from ..html_templates import _get_html_template -from ..utils import ( - logger, - verbose, - warn, - object_diff, - _validate_type, - _stamp_to_dt, - _dt_to_stamp, - _pl, - _is_numeric, - _check_option, - _on_missing, - _check_on_missing, - fill_doc, - _check_fname, - check_fname, - repr_html, -) -from ._digitization import ( - _format_dig_points, - _dig_kind_proper, - DigPoint, - _dig_kind_rev, - _dig_kind_ints, - _read_dig_fif, -) -from ._digitization import write_dig, _get_data_as_dict_from_dig -from .compensator import get_current_comp -from ..defaults import _handle_default - b = bytes # alias @@ -636,8 +636,8 @@ def rename_channels(self, mapping, allow_duplicates=False, *, verbose=None): ----- .. versionadded:: 0.9.0 """ - from ..io import BaseRaw from ..channels.channels import rename_channels + from ..io import BaseRaw info = self if isinstance(self, Info) else self.info @@ -1638,7 +1638,7 @@ def normalize_proj(self): def __repr__(self): """Summarize info instead of printing all.""" from ..io.kit.constants import KIT_SYSNAMES - from ..transforms import _coord_frame_name, Transform + from ..transforms import Transform, _coord_frame_name MAX_WIDTH = 68 strs = [" # # License: BSD-3-Clause -from collections import OrderedDict import csv - import os.path as op -import numpy as np - +from collections import OrderedDict from functools import partial + +import numpy as np from defusedxml import ElementTree -from .montage import make_dig_montage from .._freesurfer import get_mni_fiducials from ..transforms import _sph_to_cart -from ..utils import warn, _pl +from ..utils import _pl, warn from . import __file__ as _CHANNELS_INIT_FILE +from .montage import make_dig_montage MONTAGE_PATH = op.join(op.dirname(_CHANNELS_INIT_FILE), "data", "montages") diff --git a/mne/channels/channels.py b/mne/channels/channels.py index 3ec53636a15..2fb73fa5654 100644 --- a/mne/channels/channels.py +++ b/mne/channels/channels.py @@ -12,13 +12,13 @@ import os.path as op -from pathlib import Path +import string import sys from collections import OrderedDict -from dataclasses import dataclass from copy import deepcopy +from dataclasses import dataclass from functools import partial -import string +from pathlib import Path from typing import Union import numpy as np @@ -27,45 +27,45 @@ from scipy.spatial import Delaunay from scipy.stats import zscore -from ..bem import _check_origin -from ..defaults import HEAD_SIZE_DEFAULT, _handle_default -from ..utils import ( - verbose, - logger, - warn, - _check_preload, - _validate_type, - fill_doc, - _check_option, - _get_stim_channel, - _check_fname, - _check_dict_keys, - _on_missing, - legacy, -) from .._fiff.constants import FIFF from .._fiff.meas_info import ( # noqa F401 Info, MontageMixin, - create_info, - _rename_comps, _merge_info, + _rename_comps, _unit2human, # TODO: pybv relies on this, should be made public + create_info, ) from .._fiff.pick import ( - channel_type, - pick_info, - pick_types, - _picks_by_type, _check_excludes_includes, + _pick_data_channels, + _picks_by_type, + _picks_to_idx, channel_indices_by_type, + channel_type, pick_channels, - _picks_to_idx, - _pick_data_channels, + pick_info, + pick_types, ) -from .._fiff.reference import set_eeg_reference, add_reference_channels -from .._fiff.tag import _rename_list from .._fiff.proj import setup_proj +from .._fiff.reference import add_reference_channels, set_eeg_reference +from .._fiff.tag import _rename_list +from ..bem import _check_origin +from ..defaults import HEAD_SIZE_DEFAULT, _handle_default +from ..utils import ( + _check_dict_keys, + _check_fname, + _check_option, + _check_preload, + _get_stim_channel, + _on_missing, + _validate_type, + fill_doc, + legacy, + logger, + verbose, + warn, +) def _get_meg_system(info): @@ -143,11 +143,11 @@ def equalize_channels(instances, copy=True, verbose=None): This function operates inplace. """ from ..cov import Covariance - from ..io import BaseRaw from ..epochs import BaseEpochs from ..evoked import Evoked from ..forward import Forward - from ..time_frequency import _BaseTFR, CrossSpectralDensity + from ..io import BaseRaw + from ..time_frequency import CrossSpectralDensity, _BaseTFR # Instances need to have a `ch_names` attribute and a `pick_channels` # method that supports `ordered=True`. @@ -239,9 +239,9 @@ def unify_bad_channels(insts): .. versionadded:: 1.6 """ - from ..io import BaseRaw from ..epochs import Epochs from ..evoked import Evoked + from ..io import BaseRaw from ..time_frequency.spectrum import BaseSpectrum # ensure input is list-like @@ -693,8 +693,8 @@ def add_channels(self, add_list, force_update_info=False): :obj:`numpy.memmap` instance, the memmap will be resized. """ # avoid circular imports - from ..io import BaseRaw from ..epochs import BaseEpochs + from ..io import BaseRaw _validate_type(add_list, (list, tuple), "Input") @@ -1585,8 +1585,8 @@ def _compute_ch_adjacency(info, ch_type): ch_names : list The list of channel names present in adjacency matrix. """ - from ..source_estimate import spatial_tris_adjacency from ..channels.layout import _find_topomap_coords, _pair_grad_sensors + from ..source_estimate import spatial_tris_adjacency combine_grads = ch_type == "grad" and any( [ @@ -1882,9 +1882,9 @@ def combine_channels( one virtual channel for each group in ``groups`` (and, if ``keep_stim`` is ``True``, also containing stimulus channels). """ - from ..io import BaseRaw, RawArray from ..epochs import BaseEpochs, EpochsArray from ..evoked import Evoked, EvokedArray + from ..io import BaseRaw, RawArray ch_axis = 1 if isinstance(inst, BaseEpochs) else 0 ch_idx = list(range(inst.info["nchan"])) diff --git a/mne/channels/interpolation.py b/mne/channels/interpolation.py index 8f5418f9b85..3b7eebac419 100644 --- a/mne/channels/interpolation.py +++ b/mne/channels/interpolation.py @@ -8,11 +8,10 @@ from scipy.linalg import pinv from scipy.spatial.distance import pdist, squareform -from ..utils import logger, warn, verbose from .._fiff.meas_info import _simplify_info -from .._fiff.pick import pick_types, pick_channels, pick_info +from .._fiff.pick import pick_channels, pick_info, pick_types from ..surface import _normalize_vectors -from ..utils import _check_option, _validate_type +from ..utils import _check_option, _validate_type, logger, verbose, warn def _calc_h(cosang, stiffness=4, n_legendre_terms=50): @@ -121,9 +120,9 @@ def _make_interpolation_matrix(pos_from, pos_to, alpha=1e-5): def _do_interp_dots(inst, interpolation, goods_idx, bads_idx): """Dot product of channel mapping matrix to channel data.""" - from ..io import BaseRaw from ..epochs import BaseEpochs from ..evoked import Evoked + from ..io import BaseRaw _validate_type(inst, (BaseRaw, BaseEpochs, Evoked), "inst") inst._data[..., bads_idx, :] = np.matmul( diff --git a/mne/channels/layout.py b/mne/channels/layout.py index 1604ca0d260..10b341d36b4 100644 --- a/mne/channels/layout.py +++ b/mne/channels/layout.py @@ -17,22 +17,22 @@ import numpy as np from scipy.spatial.distance import pdist, squareform -from ..transforms import _pol_to_cart, _cart_to_sph -from .._fiff.pick import pick_types, _picks_to_idx, _FNIRS_CH_TYPES_SPLIT from .._fiff.constants import FIFF from .._fiff.meas_info import Info +from .._fiff.pick import _FNIRS_CH_TYPES_SPLIT, _picks_to_idx, pick_types +from ..transforms import _cart_to_sph, _pol_to_cart from ..utils import ( - _clean_names, - warn, _check_ch_locs, - fill_doc, _check_fname, _check_option, _check_sphere, + _clean_names, + fill_doc, logger, + warn, ) -from .channels import _get_ch_info from ..viz.topomap import plot_layout +from .channels import _get_ch_info class Layout: diff --git a/mne/channels/montage.py b/mne/channels/montage.py index f23547b2ad5..b442c905908 100644 --- a/mne/channels/montage.py +++ b/mne/channels/montage.py @@ -11,60 +11,58 @@ # # License: Simplified BSD -from collections import OrderedDict -from dataclasses import dataclass -from copy import deepcopy import os.path as op import re +from collections import OrderedDict +from copy import deepcopy +from dataclasses import dataclass import numpy as np -from ..defaults import HEAD_SIZE_DEFAULT -from .._freesurfer import get_mni_fiducials -from ..viz import plot_montage -from ..transforms import ( - apply_trans, - get_ras_to_neuromag_trans, - _sph_to_cart, - _topo_to_sph, - _frame_to_str, - Transform, - _verbose_frames, - _fit_matched_points, - _quat_to_affine, - _ensure_trans, -) from .._fiff._digitization import ( + _coord_frame_const, _count_points_by_type, _ensure_fiducials_head, + _format_dig_points, + _get_data_as_dict_from_dig, _get_dig_eeg, + _get_fid_coords, _make_dig_points, - write_dig, _read_dig_fif, - _format_dig_points, - _get_fid_coords, - _coord_frame_const, - _get_data_as_dict_from_dig, + write_dig, ) +from .._fiff.constants import CHANNEL_LOC_ALIASES, FIFF from .._fiff.meas_info import create_info from .._fiff.open import fiff_open -from .._fiff.pick import pick_types, _picks_to_idx, channel_type -from .._fiff.constants import FIFF, CHANNEL_LOC_ALIASES +from .._fiff.pick import _picks_to_idx, channel_type, pick_types +from .._freesurfer import get_mni_fiducials +from ..defaults import HEAD_SIZE_DEFAULT +from ..transforms import ( + Transform, + _ensure_trans, + _fit_matched_points, + _frame_to_str, + _quat_to_affine, + _sph_to_cart, + _topo_to_sph, + _verbose_frames, + apply_trans, + get_ras_to_neuromag_trans, +) from ..utils import ( - warn, - copy_function_doc_to_method_doc, - _pl, - verbose, - _check_option, - _validate_type, _check_fname, + _check_option, _on_missing, + _pl, + _validate_type, + copy_function_doc_to_method_doc, fill_doc, + verbose, + warn, ) from ..utils.docs import docdict - -from ._dig_montage_utils import _read_dig_montage_egi -from ._dig_montage_utils import _parse_brainvision_dig_montage +from ..viz import plot_montage +from ._dig_montage_utils import _parse_brainvision_dig_montage, _read_dig_montage_egi @dataclass @@ -928,7 +926,7 @@ def read_dig_hpts(fname, unit="mm"): eeg F7 -6.1042 -68.2969 45.4939 ... """ - from ._standard_montage_utils import _str_names, _str + from ._standard_montage_utils import _str, _str_names fname = _check_fname(fname, overwrite="read", must_exist=True) _scale = _check_unit_and_get_scaling(unit) @@ -1592,12 +1590,12 @@ def read_custom_montage(fname, head_size=HEAD_SIZE_DEFAULT, coord_frame=None): :func:`make_dig_montage` that takes arrays as input. """ from ._standard_montage_utils import ( - _read_theta_phi_in_degrees, - _read_sfp, + _read_brainvision, _read_csd, _read_elc, _read_elp_besa, - _read_brainvision, + _read_sfp, + _read_theta_phi_in_degrees, _read_xyz, ) diff --git a/mne/channels/tests/test_channels.py b/mne/channels/tests/test_channels.py index 1ba186b4218..7e27f301048 100644 --- a/mne/channels/tests/test_channels.py +++ b/mne/channels/tests/test_channels.py @@ -3,53 +3,53 @@ # # License: BSD-3-Clause -from pathlib import Path +import hashlib from copy import deepcopy from functools import partial -import hashlib +from pathlib import Path -import pytest import numpy as np +import pytest +from numpy.testing import assert_allclose, assert_array_equal, assert_equal from scipy.io import savemat -from numpy.testing import assert_array_equal, assert_equal, assert_allclose +from mne import ( + Epochs, + EpochsArray, + EvokedArray, + create_info, + make_ad_hoc_cov, + pick_channels, + pick_types, + read_events, +) +from mne._fiff.constants import FIFF, _ch_unit_mul_named from mne.channels import ( - rename_channels, - read_ch_adjacency, combine_channels, + equalize_channels, find_ch_adjacency, + get_builtin_ch_adjacencies, make_1020_channel_selections, + read_ch_adjacency, read_custom_montage, - equalize_channels, - get_builtin_ch_adjacencies, + rename_channels, ) from mne.channels.channels import ( - _ch_neighbor_adjacency, - _compute_ch_adjacency, _BUILTIN_CHANNEL_ADJACENCIES, _BuiltinChannelAdjacency, + _ch_neighbor_adjacency, + _compute_ch_adjacency, ) +from mne.datasets import testing from mne.io import ( + RawArray, read_info, - read_raw_fif, - read_raw_ctf, read_raw_bti, + read_raw_ctf, read_raw_eeglab, + read_raw_fif, read_raw_kit, - RawArray, ) -from mne._fiff.constants import FIFF, _ch_unit_mul_named -from mne import ( - pick_types, - pick_channels, - EpochsArray, - EvokedArray, - make_ad_hoc_cov, - create_info, - read_events, - Epochs, -) -from mne.datasets import testing from mne.parallel import parallel_func from mne.utils import requires_good_network diff --git a/mne/channels/tests/test_interpolation.py b/mne/channels/tests/test_interpolation.py index 9e8032c915b..1eabb64de91 100644 --- a/mne/channels/tests/test_interpolation.py +++ b/mne/channels/tests/test_interpolation.py @@ -1,23 +1,22 @@ +from itertools import compress from pathlib import Path import numpy as np -from numpy.testing import assert_allclose, assert_array_equal import pytest -from itertools import compress +from numpy.testing import assert_allclose, assert_array_equal -from mne import io, pick_types, pick_channels, read_events, Epochs +from mne import Epochs, io, pick_channels, pick_types, read_events +from mne._fiff.proj import _has_eeg_average_ref_proj from mne.channels.interpolation import _make_interpolation_matrix from mne.datasets import testing +from mne.io import read_raw_nirx from mne.preprocessing.nirs import ( + beer_lambert_law, optical_density, scalp_coupling_index, - beer_lambert_law, ) -from mne.io import read_raw_nirx -from mne._fiff.proj import _has_eeg_average_ref_proj from mne.utils import _record_warnings - base_dir = Path(__file__).parent.parent.parent / "io" / "tests" / "data" raw_fname = base_dir / "test_raw.fif" event_name = base_dir / "test-eve.fif" diff --git a/mne/channels/tests/test_layout.py b/mne/channels/tests/test_layout.py index b4a59720775..97fc882a666 100644 --- a/mne/channels/tests/test_layout.py +++ b/mne/channels/tests/test_layout.py @@ -8,28 +8,28 @@ import copy from pathlib import Path +import matplotlib.pyplot as plt import numpy as np +import pytest from numpy.testing import ( + assert_allclose, assert_array_almost_equal, assert_array_equal, - assert_allclose, assert_equal, ) -import pytest -import matplotlib.pyplot as plt +from mne import pick_info, pick_types +from mne._fiff.constants import FIFF +from mne._fiff.meas_info import _empty_info from mne.channels import ( + find_layout, make_eeg_layout, make_grid_layout, read_layout, - find_layout, ) -from mne.defaults import HEAD_SIZE_DEFAULT from mne.channels.layout import _box_size, _find_topomap_coords, generate_2d_layout -from mne import pick_types, pick_info -from mne.io import read_raw_kit, read_info -from mne._fiff.meas_info import _empty_info -from mne._fiff.constants import FIFF +from mne.defaults import HEAD_SIZE_DEFAULT +from mne.io import read_info, read_raw_kit io_dir = Path(__file__).parent.parent.parent / "io" fif_fname = io_dir / "tests" / "data" / "test_raw.fif" diff --git a/mne/channels/tests/test_montage.py b/mne/channels/tests/test_montage.py index 4428158ea4e..bd61a6338fb 100644 --- a/mne/channels/tests/test_montage.py +++ b/mne/channels/tests/test_montage.py @@ -3,80 +3,77 @@ # # License: BSD-3-Clause +import shutil from contextlib import nullcontext +from functools import partial from itertools import chain from pathlib import Path -import shutil - -import pytest - -import numpy as np -from functools import partial from string import ascii_lowercase +import matplotlib.pyplot as plt +import numpy as np +import pytest from numpy.testing import ( + assert_allclose, assert_array_equal, assert_array_less, - assert_allclose, assert_equal, ) -import matplotlib.pyplot as plt from mne import ( __file__ as _mne_file, +) +from mne import ( create_info, - read_evokeds, pick_types, + read_evokeds, ) -from mne.coreg import get_mni_fiducials -from mne.utils._testing import assert_object_equal +from mne._fiff._digitization import ( + _count_points_by_type, + _format_dig_points, + _get_dig_eeg, + _get_fid_coords, +) +from mne._fiff.constants import FIFF +from mne.bem import _fit_sphere from mne.channels import ( - get_builtin_montages, DigMontage, + compute_dev_head_t, + compute_native_head_t, + get_builtin_montages, + make_dig_montage, + make_standard_montage, + read_custom_montage, + read_dig_captrak, read_dig_dat, read_dig_egi, - read_dig_captrak, read_dig_fif, - make_standard_montage, - read_custom_montage, - compute_dev_head_t, - make_dig_montage, + read_dig_hpts, + read_dig_localite, read_dig_polhemus_isotrak, - compute_native_head_t, read_polhemus_fastscan, - read_dig_localite, - read_dig_hpts, ) from mne.channels.montage import ( - transform_to_head, - _check_get_coord_frame, _BUILTIN_STANDARD_MONTAGES, + _check_get_coord_frame, + transform_to_head, ) -from mne.preprocessing import compute_current_source_density -from mne.utils import assert_dig_allclose, _record_warnings -from mne.bem import _fit_sphere -from mne._fiff.constants import FIFF -from mne._fiff._digitization import ( - _format_dig_points, - _get_fid_coords, - _get_dig_eeg, - _count_points_by_type, -) -from mne.transforms import _ensure_trans, apply_trans, invert_transform, _get_trans -from mne.viz._3d import _fiducial_coords - -from mne.io.kit import read_mrk +from mne.coreg import get_mni_fiducials +from mne.datasets import testing from mne.io import ( + RawArray, + read_fiducials, read_raw_brainvision, read_raw_egi, read_raw_fif, - read_fiducials, read_raw_nirx, ) - -from mne.io import RawArray -from mne.datasets import testing - +from mne.io.kit import read_mrk +from mne.preprocessing import compute_current_source_density +from mne.transforms import _ensure_trans, _get_trans, apply_trans, invert_transform +from mne.utils import _record_warnings, assert_dig_allclose +from mne.utils._testing import assert_object_equal +from mne.viz._3d import _fiducial_coords data_path = testing.data_path(download=False) fif_dig_montage_fname = data_path / "montage" / "eeganes07.fif" diff --git a/mne/channels/tests/test_standard_montage.py b/mne/channels/tests/test_standard_montage.py index 2e1b41c7e90..d154a38b6cc 100644 --- a/mne/channels/tests/test_standard_montage.py +++ b/mne/channels/tests/test_standard_montage.py @@ -4,20 +4,18 @@ # License: BSD-3-Clause -import pytest - import numpy as np - +import pytest from numpy.testing import assert_allclose, assert_array_almost_equal, assert_raises from mne import create_info -from mne.channels import make_standard_montage, compute_native_head_t -from mne.channels.montage import get_builtin_montages, HEAD_SIZE_DEFAULT -from mne.io import RawArray from mne._fiff._digitization import _get_dig_eeg, _get_fid_coords from mne._fiff.constants import FIFF -from mne.preprocessing.nirs import optical_density, beer_lambert_law -from mne.transforms import _get_trans, _angle_between_quats, rot_to_quat +from mne.channels import compute_native_head_t, make_standard_montage +from mne.channels.montage import HEAD_SIZE_DEFAULT, get_builtin_montages +from mne.io import RawArray +from mne.preprocessing.nirs import beer_lambert_law, optical_density +from mne.transforms import _angle_between_quats, _get_trans, rot_to_quat @pytest.mark.parametrize("kind", get_builtin_montages()) diff --git a/mne/channels/tests/test_unify_bads.py b/mne/channels/tests/test_unify_bads.py index ac04983802b..a502f5ffb92 100644 --- a/mne/channels/tests/test_unify_bads.py +++ b/mne/channels/tests/test_unify_bads.py @@ -1,4 +1,5 @@ import pytest + from mne.channels import unify_bad_channels diff --git a/mne/chpi.py b/mne/chpi.py index 801ee1d2e73..939b22ac60e 100644 --- a/mne/chpi.py +++ b/mne/chpi.py @@ -19,62 +19,62 @@ # License: BSD-3-Clause import copy -from functools import partial import itertools +from functools import partial import numpy as np from scipy.linalg import orth from scipy.optimize import fmin_cobyla from scipy.spatial.distance import cdist -from .event import find_events -from .io import BaseRaw -from .io.ctf.trans import _make_ctf_coord_trans_set -from .io.kit.kit import RawKIT as _RawKIT -from .io.kit.constants import KIT -from .channels.channels import _get_meg_system -from ._fiff.meas_info import _simplify_info, Info +from ._fiff.constants import FIFF +from ._fiff.meas_info import Info, _simplify_info from ._fiff.pick import ( - pick_types, + _picks_to_idx, pick_channels, pick_channels_regexp, pick_info, - _picks_to_idx, + pick_types, ) from ._fiff.proj import Projection, setup_proj -from ._fiff.constants import FIFF -from .forward import _magnetic_dipole_field_vec, _create_meg_coils, _concatenate_coils -from .cov import make_ad_hoc_cov, compute_whitener +from .channels.channels import _get_meg_system +from .cov import compute_whitener, make_ad_hoc_cov from .dipole import _make_guesses +from .event import find_events from .fixes import jit +from .forward import _concatenate_coils, _create_meg_coils, _magnetic_dipole_field_vec +from .io import BaseRaw +from .io.ctf.trans import _make_ctf_coord_trans_set +from .io.kit.constants import KIT +from .io.kit.kit import RawKIT as _RawKIT from .preprocessing.maxwell import ( - _sss_basis, + _get_mf_picks_fix_mags, _prep_mf_coils, _regularize_out, - _get_mf_picks_fix_mags, + _sss_basis, ) from .transforms import ( - apply_trans, - invert_transform, _angle_between_quats, - quat_to_rot, - rot_to_quat, _fit_matched_points, _quat_to_affine, als_ras_trans, + apply_trans, + invert_transform, + quat_to_rot, + rot_to_quat, ) from .utils import ( - verbose, - logger, - use_log_level, - _check_fname, - warn, - _validate_type, ProgressBar, + _check_fname, _check_option, - _pl, _on_missing, + _pl, + _validate_type, _verbose_safe_false, + logger, + use_log_level, + verbose, + warn, ) # Eventually we should add: diff --git a/mne/commands/mne_anonymize.py b/mne/commands/mne_anonymize.py index d4b54000b78..65bb0c9e4f5 100644 --- a/mne/commands/mne_anonymize.py +++ b/mne/commands/mne_anonymize.py @@ -15,9 +15,10 @@ """ +import os.path as op import sys + import mne -import os.path as op ANONYMIZE_FILE_PREFIX = "anon" diff --git a/mne/commands/mne_browse_raw.py b/mne/commands/mne_browse_raw.py index 9c338518e85..c3a3d144e33 100644 --- a/mne/commands/mne_browse_raw.py +++ b/mne/commands/mne_browse_raw.py @@ -16,12 +16,13 @@ # Authors : Eric Larson, PhD import sys + import mne def run(): """Run command.""" - from mne.commands.utils import get_optparser, _add_verbose_flag + from mne.commands.utils import _add_verbose_flag, get_optparser from mne.viz import _RAW_CLIP_DEF parser = get_optparser(__file__, usage="usage: %prog raw [options]") diff --git a/mne/commands/mne_compare_fiff.py b/mne/commands/mne_compare_fiff.py index fe05d636592..d38e95761b0 100644 --- a/mne/commands/mne_compare_fiff.py +++ b/mne/commands/mne_compare_fiff.py @@ -12,6 +12,7 @@ # Authors : Eric Larson, PhD import sys + import mne diff --git a/mne/commands/mne_compute_proj_ecg.py b/mne/commands/mne_compute_proj_ecg.py index bb366f9d3e2..1e8b6aab973 100644 --- a/mne/commands/mne_compute_proj_ecg.py +++ b/mne/commands/mne_compute_proj_ecg.py @@ -15,6 +15,7 @@ import os import sys + import mne diff --git a/mne/commands/mne_compute_proj_eog.py b/mne/commands/mne_compute_proj_eog.py index 42c93513122..4b3dcae75dd 100644 --- a/mne/commands/mne_compute_proj_eog.py +++ b/mne/commands/mne_compute_proj_eog.py @@ -25,6 +25,7 @@ import os import sys + import mne diff --git a/mne/commands/mne_coreg.py b/mne/commands/mne_coreg.py index dad18d278aa..add4a65845b 100644 --- a/mne/commands/mne_coreg.py +++ b/mne/commands/mne_coreg.py @@ -18,7 +18,7 @@ def run(): """Run command.""" - from mne.commands.utils import get_optparser, _add_verbose_flag + from mne.commands.utils import _add_verbose_flag, get_optparser parser = get_optparser(__file__) diff --git a/mne/commands/mne_freeview_bem_surfaces.py b/mne/commands/mne_freeview_bem_surfaces.py index 646049b6616..b428cd3f7d0 100644 --- a/mne/commands/mne_freeview_bem_surfaces.py +++ b/mne/commands/mne_freeview_bem_surfaces.py @@ -10,12 +10,12 @@ """ # Authors: Alexandre Gramfort -import sys import os import os.path as op +import sys import mne -from mne.utils import run_subprocess, get_subjects_dir +from mne.utils import get_subjects_dir, run_subprocess def freeview_bem_surfaces(subject, subjects_dir, method): diff --git a/mne/commands/mne_make_scalp_surfaces.py b/mne/commands/mne_make_scalp_surfaces.py index c5bf03e06a0..dffb97fd591 100644 --- a/mne/commands/mne_make_scalp_surfaces.py +++ b/mne/commands/mne_make_scalp_surfaces.py @@ -24,7 +24,7 @@ def run(): """Run command.""" - from mne.commands.utils import get_optparser, _add_verbose_flag + from mne.commands.utils import _add_verbose_flag, get_optparser parser = get_optparser(__file__) subjects_dir = mne.get_config("SUBJECTS_DIR") diff --git a/mne/commands/mne_maxfilter.py b/mne/commands/mne_maxfilter.py index 182a2c6254b..c5f83b9176f 100644 --- a/mne/commands/mne_maxfilter.py +++ b/mne/commands/mne_maxfilter.py @@ -14,8 +14,9 @@ # Authors : Martin Luessi -import sys import os +import sys + import mne diff --git a/mne/commands/mne_prepare_bem_model.py b/mne/commands/mne_prepare_bem_model.py index ae43ae9533a..cc8620d0ecd 100644 --- a/mne/commands/mne_prepare_bem_model.py +++ b/mne/commands/mne_prepare_bem_model.py @@ -9,14 +9,15 @@ """ -import sys import os +import sys + import mne def run(): """Run command.""" - from mne.commands.utils import get_optparser, _add_verbose_flag + from mne.commands.utils import _add_verbose_flag, get_optparser parser = get_optparser(__file__) diff --git a/mne/commands/mne_report.py b/mne/commands/mne_report.py index 79818d52bab..4b5dd54b30f 100644 --- a/mne/commands/mne_report.py +++ b/mne/commands/mne_report.py @@ -72,7 +72,7 @@ import mne from mne.report import Report -from mne.utils import verbose, logger +from mne.utils import logger, verbose @verbose @@ -83,7 +83,7 @@ def log_elapsed(t, verbose=None): def run(): """Run command.""" - from mne.commands.utils import get_optparser, _add_verbose_flag + from mne.commands.utils import _add_verbose_flag, get_optparser parser = get_optparser(__file__) diff --git a/mne/commands/mne_setup_forward_model.py b/mne/commands/mne_setup_forward_model.py index df7fc5fff4b..c06b725a2d6 100644 --- a/mne/commands/mne_setup_forward_model.py +++ b/mne/commands/mne_setup_forward_model.py @@ -9,15 +9,16 @@ """ -import sys import os +import sys + import mne from mne.utils import get_subjects_dir, warn def run(): """Run command.""" - from mne.commands.utils import get_optparser, _add_verbose_flag + from mne.commands.utils import _add_verbose_flag, get_optparser parser = get_optparser(__file__) diff --git a/mne/commands/mne_setup_source_space.py b/mne/commands/mne_setup_source_space.py index 0937a75bafe..963a27fca07 100644 --- a/mne/commands/mne_setup_source_space.py +++ b/mne/commands/mne_setup_source_space.py @@ -21,7 +21,7 @@ def run(): """Run command.""" - from mne.commands.utils import get_optparser, _add_verbose_flag + from mne.commands.utils import _add_verbose_flag, get_optparser parser = get_optparser(__file__) diff --git a/mne/commands/mne_show_fiff.py b/mne/commands/mne_show_fiff.py index a14d5c129ea..128e8fee55f 100644 --- a/mne/commands/mne_show_fiff.py +++ b/mne/commands/mne_show_fiff.py @@ -19,6 +19,7 @@ # Authors : Eric Larson, PhD import sys + import mne diff --git a/mne/commands/mne_show_info.py b/mne/commands/mne_show_info.py index dc39491fb6c..80f546f6d80 100644 --- a/mne/commands/mne_show_info.py +++ b/mne/commands/mne_show_info.py @@ -12,6 +12,7 @@ # Authors : Alexandre Gramfort, Ph.D. import sys + import mne diff --git a/mne/commands/mne_sys_info.py b/mne/commands/mne_sys_info.py index 075ff446681..62bbf9afbfe 100644 --- a/mne/commands/mne_sys_info.py +++ b/mne/commands/mne_sys_info.py @@ -12,6 +12,7 @@ # Authors : Eric Larson import sys + import mne diff --git a/mne/commands/mne_watershed_bem.py b/mne/commands/mne_watershed_bem.py index c182c7a0ded..279cd826adc 100644 --- a/mne/commands/mne_watershed_bem.py +++ b/mne/commands/mne_watershed_bem.py @@ -19,7 +19,7 @@ def run(): """Run command.""" - from mne.commands.utils import get_optparser, _add_verbose_flag + from mne.commands.utils import _add_verbose_flag, get_optparser parser = get_optparser(__file__) diff --git a/mne/commands/tests/test_commands.py b/mne/commands/tests/test_commands.py index c3bac034339..fecd0235a87 100644 --- a/mne/commands/tests/test_commands.py +++ b/mne/commands/tests/test_commands.py @@ -1,54 +1,54 @@ import glob import os -from os import path as op import shutil +from os import path as op from pathlib import Path import numpy as np import pytest -from numpy.testing import assert_equal, assert_allclose +from numpy.testing import assert_allclose, assert_equal import mne from mne import ( concatenate_raws, + read_bem_solution, read_bem_surfaces, - read_surface, read_source_spaces, - read_bem_solution, + read_surface, ) from mne.bem import ConductorModel, convert_flash_mris from mne.commands import ( + mne_anonymize, mne_browse_raw, mne_bti2fiff, mne_clean_eog_ecg, + mne_compare_fiff, mne_compute_proj_ecg, mne_compute_proj_eog, mne_coreg, + mne_flash_bem, mne_kit2fiff, mne_make_scalp_surfaces, mne_maxfilter, + mne_prepare_bem_model, mne_report, - mne_surf2bem, - mne_watershed_bem, - mne_compare_fiff, - mne_flash_bem, + mne_setup_forward_model, + mne_setup_source_space, mne_show_fiff, mne_show_info, - mne_what, - mne_setup_source_space, - mne_setup_forward_model, - mne_anonymize, - mne_prepare_bem_model, + mne_surf2bem, mne_sys_info, + mne_watershed_bem, + mne_what, ) from mne.datasets import testing -from mne.io import read_raw_fif, read_info +from mne.io import read_info, read_raw_fif from mne.utils import ( - requires_mne, - requires_freesurfer, ArgvSetter, - _stamp_to_dt, _record_warnings, + _stamp_to_dt, + requires_freesurfer, + requires_mne, ) base_dir = op.join(op.dirname(__file__), "..", "..", "io", "tests", "data") diff --git a/mne/commands/utils.py b/mne/commands/utils.py index 80d04ab1729..68ede4e807f 100644 --- a/mne/commands/utils.py +++ b/mne/commands/utils.py @@ -9,8 +9,8 @@ import importlib import os import os.path as op -from optparse import OptionParser import sys +from optparse import OptionParser import mne @@ -41,7 +41,7 @@ def load_module(name, path): Imported module. """ - from importlib.util import spec_from_file_location, module_from_spec + from importlib.util import module_from_spec, spec_from_file_location spec = spec_from_file_location(name, path) mod = module_from_spec(spec) diff --git a/mne/conftest.py b/mne/conftest.py index a0eeaf18dfb..908e7f53dba 100644 --- a/mne/conftest.py +++ b/mne/conftest.py @@ -2,37 +2,37 @@ # # License: BSD-3-Clause -from contextlib import contextmanager -import inspect -from textwrap import dedent import gc +import inspect import os import os.path as op -from pathlib import Path import shutil import sys import warnings -import pytest -from pytest import StashKey +from contextlib import contextmanager +from pathlib import Path +from textwrap import dedent from unittest import mock import numpy as np +import pytest +from pytest import StashKey import mne -from mne import read_events, pick_types, Epochs +from mne import Epochs, pick_types, read_events from mne.channels import read_layout from mne.coreg import create_default_subject from mne.datasets import testing -from mne.fixes import has_numba, _compare_version -from mne.io import read_raw_fif, read_raw_ctf, read_raw_nirx, read_raw_snirf +from mne.fixes import _compare_version, has_numba +from mne.io import read_raw_ctf, read_raw_fif, read_raw_nirx, read_raw_snirf from mne.stats import cluster_level from mne.utils import ( - _pl, - _assert_no_instances, - numerics, Bunch, + _assert_no_instances, _check_qt_version, + _pl, _TempDir, + numerics, ) # data from sample dataset @@ -599,12 +599,12 @@ def _use_backend(backend_name, interactive): def _check_skip_backend(name): + from mne.viz.backends._utils import _notebook_vtk_works from mne.viz.backends.tests._utils import ( - has_pyvista, has_imageio_ffmpeg, + has_pyvista, has_pyvistaqt, ) - from mne.viz.backends._utils import _notebook_vtk_works if not has_pyvista(): pytest.skip("Test skipped, requires pyvista.") @@ -629,8 +629,8 @@ def pixel_ratio(): # _check_qt_version will init an app for us, so no need for us to do it if not has_pyvista() or not _check_qt_version(): return 1.0 - from qtpy.QtWidgets import QMainWindow from qtpy.QtCore import Qt + from qtpy.QtWidgets import QMainWindow app = _init_mne_qtapp() app.processEvents() @@ -1005,10 +1005,10 @@ def numba_conditional(monkeypatch, request): def _nbclient(): try: import nbformat + import trame # noqa + from ipywidgets import Button # noqa from jupyter_client import AsyncKernelManager from nbclient import NotebookClient - from ipywidgets import Button # noqa - import trame # noqa except Exception as exc: return pytest.skip(f"Skipping Notebook test: {exc}") km = AsyncKernelManager(config=None) diff --git a/mne/coreg.py b/mne/coreg.py index cd9158b7cba..c50d3d00274 100644 --- a/mne/coreg.py +++ b/mne/coreg.py @@ -6,75 +6,74 @@ import configparser import fnmatch -from glob import glob, iglob import os import os.path as op -import stat -import sys import re import shutil +import stat +import sys from functools import reduce +from glob import glob, iglob import numpy as np from scipy.optimize import leastsq from scipy.spatial.distance import cdist -from ._fiff.meas_info import read_fiducials, write_fiducials, read_info -from ._fiff.constants import FIFF -from ._fiff.meas_info import Info from ._fiff._digitization import _get_data_as_dict_from_dig +from ._fiff.constants import FIFF +from ._fiff.meas_info import Info, read_fiducials, read_info, write_fiducials # keep get_mni_fiducials for backward compat (no burden to keep in this # namespace, too) from ._freesurfer import ( _read_mri_info, - get_mni_fiducials, estimate_head_mri_t, # noqa: F401 + get_mni_fiducials, ) -from .label import read_label, Label +from .bem import read_bem_surfaces, write_bem_surfaces +from .channels import make_dig_montage +from .label import Label, read_label from .source_space import ( add_source_space_distances, read_source_spaces, # noqa: F401 write_source_spaces, ) from .surface import ( - read_surface, - write_surface, + _DistanceQuery, _normalize_vectors, complete_surface_info, decimate_surface, - _DistanceQuery, + read_surface, + write_surface, ) -from .bem import read_bem_surfaces, write_bem_surfaces from .transforms import ( - rotation, - rotation3d, - scaling, - translation, Transform, + _angle_between_quats, + _fit_matched_points, + _quat_to_euler, _read_fs_xfm, _write_fs_xfm, - invert_transform, - combine_transforms, - _quat_to_euler, - _fit_matched_points, apply_trans, + combine_transforms, + invert_transform, rot_to_quat, - _angle_between_quats, + rotation, + rotation3d, + scaling, + translation, ) -from .channels import make_dig_montage from .utils import ( + _check_option, + _check_subject, + _import_nibabel, + _validate_type, + fill_doc, get_config, get_subjects_dir, logger, pformat, verbose, warn, - fill_doc, - _validate_type, - _check_subject, - _check_option, - _import_nibabel, ) from .viz._3d import _fiducial_coords diff --git a/mne/cov.py b/mne/cov.py index 4fa6fe44e6c..db4b2126a3d 100644 --- a/mne/cov.py +++ b/mne/cov.py @@ -4,78 +4,80 @@ # # License: BSD-3-Clause -from copy import deepcopy import itertools as itt +from copy import deepcopy from math import log import numpy as np from scipy.sparse import issparse -from .defaults import ( - _INTERPOLATION_DEFAULT, - _EXTRAPOLATE_DEFAULT, - _BORDER_DEFAULT, - DEFAULTS, +from . import viz +from ._fiff.constants import FIFF +from ._fiff.meas_info import _read_bad_channels, _write_bad_channels, create_info +from ._fiff.pick import ( + _DATA_CH_TYPES_SPLIT, + _pick_data_channels, + _picks_by_type, + _picks_to_idx, + pick_channels, + pick_channels_cov, + pick_info, + pick_types, ) -from .fixes import _safe_svd from ._fiff.proj import ( - make_projector as _make_projector, - _proj_equal, - activate_proj as _activate_proj, _check_projs, - _needs_eeg_average_ref_proj, _has_eeg_average_ref_proj, + _needs_eeg_average_ref_proj, + _proj_equal, _read_proj, _write_proj, ) -from ._fiff.pick import ( - pick_types, - pick_channels_cov, - pick_channels, - pick_info, - _picks_by_type, - _pick_data_channels, - _picks_to_idx, - _DATA_CH_TYPES_SPLIT, +from ._fiff.proj import ( + activate_proj as _activate_proj, +) +from ._fiff.proj import ( + make_projector as _make_projector, ) - -from ._fiff.constants import FIFF -from ._fiff.meas_info import _read_bad_channels, create_info, _write_bad_channels from ._fiff.tag import find_tag from ._fiff.tree import dir_tree_find -from .defaults import _handle_default +from .defaults import ( + _BORDER_DEFAULT, + _EXTRAPOLATE_DEFAULT, + _INTERPOLATION_DEFAULT, + DEFAULTS, + _handle_default, +) from .epochs import Epochs from .event import make_fixed_length_events from .evoked import EvokedArray +from .fixes import ( + BaseEstimator, + EmpiricalCovariance, + _logdet, + _safe_svd, + empirical_covariance, + log_likelihood, +) from .rank import compute_rank from .utils import ( - check_fname, - logger, - verbose, - check_version, - _time_mask, - warn, - copy_function_doc_to_method_doc, + _check_fname, + _check_on_missing, + _check_option, + _on_missing, _pl, - _undo_scaling_cov, _scaled_array, + _time_mask, + _undo_scaling_cov, _validate_type, - _check_option, + _verbose_safe_false, + check_fname, + check_version, + copy_function_doc_to_method_doc, eigh, fill_doc, - _on_missing, - _check_on_missing, - _check_fname, - _verbose_safe_false, -) -from . import viz - -from .fixes import ( - BaseEstimator, - EmpiricalCovariance, - _logdet, - empirical_covariance, - log_likelihood, + logger, + verbose, + warn, ) @@ -1330,8 +1332,8 @@ def _compute_covariance_auto( del sc elif method_ == "shrunk": - from sklearn.model_selection import GridSearchCV from sklearn.covariance import ShrunkCovariance + from sklearn.model_selection import GridSearchCV shrinkage = mp.pop("shrinkage") tuned_parameters = [{"shrinkage": shrinkage}] @@ -2428,13 +2430,13 @@ def _read_cov(fid, node, cov_kind, limited=False, verbose=None): def _write_cov(fid, cov): """Write a noise covariance matrix.""" from ._fiff.write import ( - start_block, end_block, - write_int, + start_block, write_double, write_float_matrix, - write_string, + write_int, write_name_list_sanitized, + write_string, ) start_block(fid, FIFF.FIFFB_MNE_COV) diff --git a/mne/cuda.py b/mne/cuda.py index 602d29a2f7d..410a81fd36e 100644 --- a/mne/cuda.py +++ b/mne/cuda.py @@ -6,14 +6,14 @@ from scipy.fft import irfft, rfft from .utils import ( - sizeof_fmt, - logger, - get_config, - warn, + _check_option, _explain_exception, - verbose, fill_doc, - _check_option, + get_config, + logger, + sizeof_fmt, + verbose, + warn, ) _cuda_capable = False diff --git a/mne/datasets/__init__.pyi b/mne/datasets/__init__.pyi index 96964148d3e..c028314c233 100644 --- a/mne/datasets/__init__.pyi +++ b/mne/datasets/__init__.pyi @@ -34,31 +34,31 @@ __all__ = [ "visual_92_categories", ] from . import ( - fieldtrip_cmc, + _fake, brainstorm, - visual_92_categories, - kiloword, eegbci, + epilepsy_ecog, + erp_core, + eyelink, + fieldtrip_cmc, + fnirs_motor, hf_sef, + kiloword, + limo, misc, mtrf, - sample, - somato, multimodal, - fnirs_motor, opm, - spm_face, - testing, - _fake, phantom_4dbti, - sleep_physionet, - limo, refmeg_noise, + sample, + sleep_physionet, + somato, + spm_face, ssvep, - erp_core, - epilepsy_ecog, - eyelink, + testing, ucl_opm_auditory, + visual_92_categories, ) from ._fetch import fetch_dataset from ._fsaverage.base import fetch_fsaverage @@ -66,7 +66,7 @@ from ._infant.base import fetch_infant_template from ._phantom.base import fetch_phantom from .utils import ( _download_all_example_data, - fetch_hcp_mmp_parcellation, fetch_aparc_sub_parcellation, + fetch_hcp_mmp_parcellation, has_dataset, ) diff --git a/mne/datasets/_fetch.py b/mne/datasets/_fetch.py index 6dbfe41c2bf..48fb609d81a 100644 --- a/mne/datasets/_fetch.py +++ b/mne/datasets/_fetch.py @@ -2,30 +2,29 @@ # # License: BSD Style. -import sys import os import os.path as op +import sys +import time from pathlib import Path from shutil import rmtree -import time from .. import __version__ as mne_version -from ..utils import logger, warn, _safe_input +from ..fixes import _compare_version +from ..utils import _safe_input, logger, warn from .config import ( - _bst_license_text, + MISC_VERSIONED, RELEASES, TESTING_VERSIONED, - MISC_VERSIONED, + _bst_license_text, ) from .utils import ( _dataset_version, _do_path_update, + _downloader_params, _get_path, _log_time_size, - _downloader_params, ) -from ..fixes import _compare_version - _FAKE_VERSION = None # used for monkeypatching while testing versioning diff --git a/mne/datasets/_fsaverage/base.py b/mne/datasets/_fsaverage/base.py index daa01dc64c2..934f76ca520 100644 --- a/mne/datasets/_fsaverage/base.py +++ b/mne/datasets/_fsaverage/base.py @@ -4,8 +4,8 @@ import os import os.path as op -from ..utils import _manifest_check_download, _get_path -from ...utils import verbose, get_subjects_dir, set_config +from ...utils import get_subjects_dir, set_config, verbose +from ..utils import _get_path, _manifest_check_download FSAVERAGE_MANIFEST_PATH = op.dirname(__file__) diff --git a/mne/datasets/_infant/base.py b/mne/datasets/_infant/base.py index 196faa7bfc2..e709b740691 100644 --- a/mne/datasets/_infant/base.py +++ b/mne/datasets/_infant/base.py @@ -4,8 +4,8 @@ import os import os.path as op +from ...utils import _check_option, _validate_type, get_subjects_dir, verbose from ..utils import _manifest_check_download -from ...utils import verbose, get_subjects_dir, _check_option, _validate_type _AGES = "2wk 1mo 2mo 3mo 4.5mo 6mo 7.5mo 9mo 10.5mo 12mo 15mo 18mo 2yr" # https://github.com/christian-oreilly/infant_template_paper/releases diff --git a/mne/datasets/_phantom/base.py b/mne/datasets/_phantom/base.py index 3d8af0e68ac..19ab1db89ee 100644 --- a/mne/datasets/_phantom/base.py +++ b/mne/datasets/_phantom/base.py @@ -4,8 +4,8 @@ import os import os.path as op +from ...utils import _check_option, _validate_type, get_subjects_dir, verbose from ..utils import _manifest_check_download -from ...utils import verbose, get_subjects_dir, _check_option, _validate_type PHANTOM_MANIFEST_PATH = op.dirname(__file__) diff --git a/mne/datasets/brainstorm/bst_auditory.py b/mne/datasets/brainstorm/bst_auditory.py index f6cbd7c4e43..6dbbe45407b 100644 --- a/mne/datasets/brainstorm/bst_auditory.py +++ b/mne/datasets/brainstorm/bst_auditory.py @@ -3,10 +3,10 @@ # License: BSD-3-Clause from ...utils import verbose from ..utils import ( - _get_version, - _version_doc, _data_path_doc_accept, _download_mne_dataset, + _get_version, + _version_doc, ) _description = """ diff --git a/mne/datasets/brainstorm/bst_phantom_ctf.py b/mne/datasets/brainstorm/bst_phantom_ctf.py index 9a64a438e89..c73e8798623 100644 --- a/mne/datasets/brainstorm/bst_phantom_ctf.py +++ b/mne/datasets/brainstorm/bst_phantom_ctf.py @@ -3,10 +3,10 @@ # License: BSD-3-Clause from ...utils import verbose from ..utils import ( - _get_version, - _version_doc, _data_path_doc_accept, _download_mne_dataset, + _get_version, + _version_doc, ) _description = """ diff --git a/mne/datasets/brainstorm/bst_phantom_elekta.py b/mne/datasets/brainstorm/bst_phantom_elekta.py index b5e13d385f3..b30770bbe34 100644 --- a/mne/datasets/brainstorm/bst_phantom_elekta.py +++ b/mne/datasets/brainstorm/bst_phantom_elekta.py @@ -3,10 +3,10 @@ # License: BSD-3-Clause from ...utils import verbose from ..utils import ( - _get_version, - _version_doc, _data_path_doc_accept, _download_mne_dataset, + _get_version, + _version_doc, ) _description = """ diff --git a/mne/datasets/brainstorm/bst_raw.py b/mne/datasets/brainstorm/bst_raw.py index 3aeef5e49d2..3c9ea2ae965 100644 --- a/mne/datasets/brainstorm/bst_raw.py +++ b/mne/datasets/brainstorm/bst_raw.py @@ -3,16 +3,15 @@ # License: BSD-3-Clause from functools import partial -from ...utils import verbose, get_config +from ...utils import get_config, verbose from ..utils import ( - has_dataset, - _get_version, - _version_doc, _data_path_doc_accept, _download_mne_dataset, + _get_version, + _version_doc, + has_dataset, ) - has_brainstorm_data = partial(has_dataset, name="bst_raw") _description = """ diff --git a/mne/datasets/brainstorm/bst_resting.py b/mne/datasets/brainstorm/bst_resting.py index cef6ab986c6..efe86c0ce69 100644 --- a/mne/datasets/brainstorm/bst_resting.py +++ b/mne/datasets/brainstorm/bst_resting.py @@ -3,10 +3,10 @@ # License: BSD-3-Clause from ...utils import verbose from ..utils import ( - _get_version, - _version_doc, _data_path_doc_accept, _download_mne_dataset, + _get_version, + _version_doc, ) _description = """ diff --git a/mne/datasets/eegbci/eegbci.py b/mne/datasets/eegbci/eegbci.py index 76db5ef99ac..00f7fff4767 100644 --- a/mne/datasets/eegbci/eegbci.py +++ b/mne/datasets/eegbci/eegbci.py @@ -5,12 +5,12 @@ import os import re +import time from os import path as op from pathlib import Path -import time -from ...utils import _url_to_local_path, verbose, logger -from ..utils import _do_path_update, _get_path, _log_time_size, _downloader_params +from ...utils import _url_to_local_path, logger, verbose +from ..utils import _do_path_update, _downloader_params, _get_path, _log_time_size # TODO: remove try/except when our min version is py 3.9 try: diff --git a/mne/datasets/epilepsy_ecog/_data.py b/mne/datasets/epilepsy_ecog/_data.py index b6cc93b92bd..8192a7bf369 100644 --- a/mne/datasets/epilepsy_ecog/_data.py +++ b/mne/datasets/epilepsy_ecog/_data.py @@ -3,7 +3,7 @@ # License: BSD Style. from ...utils import verbose -from ..utils import _data_path_doc, _get_version, _version_doc, _download_mne_dataset +from ..utils import _data_path_doc, _download_mne_dataset, _get_version, _version_doc @verbose diff --git a/mne/datasets/erp_core/erp_core.py b/mne/datasets/erp_core/erp_core.py index 8f3aa1e2663..843777412bc 100644 --- a/mne/datasets/erp_core/erp_core.py +++ b/mne/datasets/erp_core/erp_core.py @@ -1,5 +1,5 @@ from ...utils import verbose -from ..utils import _data_path_doc, _get_version, _version_doc, _download_mne_dataset +from ..utils import _data_path_doc, _download_mne_dataset, _get_version, _version_doc @verbose diff --git a/mne/datasets/eyelink/eyelink.py b/mne/datasets/eyelink/eyelink.py index f0a349c3c16..596f0f79e47 100644 --- a/mne/datasets/eyelink/eyelink.py +++ b/mne/datasets/eyelink/eyelink.py @@ -2,7 +2,7 @@ # License: BSD Style. from ...utils import verbose -from ..utils import _data_path_doc, _get_version, _version_doc, _download_mne_dataset +from ..utils import _data_path_doc, _download_mne_dataset, _get_version, _version_doc @verbose diff --git a/mne/datasets/fieldtrip_cmc/fieldtrip_cmc.py b/mne/datasets/fieldtrip_cmc/fieldtrip_cmc.py index cdce53d57a8..c825eafef05 100644 --- a/mne/datasets/fieldtrip_cmc/fieldtrip_cmc.py +++ b/mne/datasets/fieldtrip_cmc/fieldtrip_cmc.py @@ -3,7 +3,7 @@ # # License: BSD Style. from ...utils import verbose -from ..utils import _data_path_doc, _get_version, _version_doc, _download_mne_dataset +from ..utils import _data_path_doc, _download_mne_dataset, _get_version, _version_doc @verbose diff --git a/mne/datasets/fnirs_motor/fnirs_motor.py b/mne/datasets/fnirs_motor/fnirs_motor.py index 2c49a32c891..9ae0844dacd 100644 --- a/mne/datasets/fnirs_motor/fnirs_motor.py +++ b/mne/datasets/fnirs_motor/fnirs_motor.py @@ -2,7 +2,7 @@ # License: BSD Style. from ...utils import verbose -from ..utils import _data_path_doc, _get_version, _version_doc, _download_mne_dataset +from ..utils import _data_path_doc, _download_mne_dataset, _get_version, _version_doc @verbose diff --git a/mne/datasets/hf_sef/hf_sef.py b/mne/datasets/hf_sef/hf_sef.py index 66c25ad12be..dc1586cac9f 100644 --- a/mne/datasets/hf_sef/hf_sef.py +++ b/mne/datasets/hf_sef/hf_sef.py @@ -3,11 +3,12 @@ # License: BSD Style. -import os.path as op import os -from ...utils import verbose, _check_option -from ..utils import _get_path, _do_path_update, _download_mne_dataset +import os.path as op + +from ...utils import _check_option, verbose from ..config import MNE_DATASETS +from ..utils import _do_path_update, _download_mne_dataset, _get_path @verbose diff --git a/mne/datasets/kiloword/kiloword.py b/mne/datasets/kiloword/kiloword.py index c6f437ab36e..67014bcadb9 100644 --- a/mne/datasets/kiloword/kiloword.py +++ b/mne/datasets/kiloword/kiloword.py @@ -1,7 +1,7 @@ # License: BSD Style. from ...utils import verbose -from ..utils import _get_version, _version_doc, _download_mne_dataset +from ..utils import _download_mne_dataset, _get_version, _version_doc @verbose diff --git a/mne/datasets/limo/limo.py b/mne/datasets/limo/limo.py index 8e90ed01fc2..9c5b3861b88 100644 --- a/mne/datasets/limo/limo.py +++ b/mne/datasets/limo/limo.py @@ -3,17 +3,17 @@ # License: BSD-3-Clause import os.path as op -from pathlib import Path import time +from pathlib import Path import numpy as np from scipy.io import loadmat +from ..._fiff.meas_info import create_info from ...channels import make_standard_montage from ...epochs import EpochsArray -from ..._fiff.meas_info import create_info -from ...utils import _check_pandas_installed, verbose, logger -from ..utils import _get_path, _do_path_update, _log_time_size, _downloader_params +from ...utils import _check_pandas_installed, logger, verbose +from ..utils import _do_path_update, _downloader_params, _get_path, _log_time_size # root url for LIMO files root_url = "https://files.de-1.osf.io/v1/resources/52rea/providers/osfstorage/" diff --git a/mne/datasets/misc/_misc.py b/mne/datasets/misc/_misc.py index 443aa24787b..65f873af635 100644 --- a/mne/datasets/misc/_misc.py +++ b/mne/datasets/misc/_misc.py @@ -4,7 +4,7 @@ # License: BSD Style. from ...utils import verbose -from ..utils import has_dataset, _data_path_doc, _download_mne_dataset +from ..utils import _data_path_doc, _download_mne_dataset, has_dataset @verbose diff --git a/mne/datasets/mtrf/mtrf.py b/mne/datasets/mtrf/mtrf.py index 1ce4f741a4f..fdcb6bf9c3b 100644 --- a/mne/datasets/mtrf/mtrf.py +++ b/mne/datasets/mtrf/mtrf.py @@ -3,8 +3,7 @@ # License: BSD Style. from ...utils import verbose -from ..utils import _data_path_doc, _get_version, _version_doc, _download_mne_dataset - +from ..utils import _data_path_doc, _download_mne_dataset, _get_version, _version_doc data_name = "mtrf" diff --git a/mne/datasets/multimodal/multimodal.py b/mne/datasets/multimodal/multimodal.py index 84fbf662e5f..a1789e30623 100644 --- a/mne/datasets/multimodal/multimodal.py +++ b/mne/datasets/multimodal/multimodal.py @@ -4,7 +4,7 @@ # License: BSD Style. from ...utils import verbose -from ..utils import _data_path_doc, _get_version, _version_doc, _download_mne_dataset +from ..utils import _data_path_doc, _download_mne_dataset, _get_version, _version_doc @verbose diff --git a/mne/datasets/opm/opm.py b/mne/datasets/opm/opm.py index b2b24f2e3f8..b21ed9ffd1c 100644 --- a/mne/datasets/opm/opm.py +++ b/mne/datasets/opm/opm.py @@ -4,7 +4,7 @@ # License: BSD Style. from ...utils import verbose -from ..utils import _data_path_doc, _get_version, _version_doc, _download_mne_dataset +from ..utils import _data_path_doc, _download_mne_dataset, _get_version, _version_doc @verbose diff --git a/mne/datasets/phantom_4dbti/phantom_4dbti.py b/mne/datasets/phantom_4dbti/phantom_4dbti.py index 59c42416d5a..6963b72fb09 100644 --- a/mne/datasets/phantom_4dbti/phantom_4dbti.py +++ b/mne/datasets/phantom_4dbti/phantom_4dbti.py @@ -3,7 +3,7 @@ # License: BSD Style. from ...utils import verbose -from ..utils import _data_path_doc, _get_version, _version_doc, _download_mne_dataset +from ..utils import _data_path_doc, _download_mne_dataset, _get_version, _version_doc @verbose diff --git a/mne/datasets/refmeg_noise/refmeg_noise.py b/mne/datasets/refmeg_noise/refmeg_noise.py index e77f3eefaf0..5c27a469a2d 100644 --- a/mne/datasets/refmeg_noise/refmeg_noise.py +++ b/mne/datasets/refmeg_noise/refmeg_noise.py @@ -2,7 +2,7 @@ # License: BSD Style. from ...utils import verbose -from ..utils import _data_path_doc, _get_version, _version_doc, _download_mne_dataset +from ..utils import _data_path_doc, _download_mne_dataset, _get_version, _version_doc @verbose diff --git a/mne/datasets/sample/sample.py b/mne/datasets/sample/sample.py index f5ca6de24c4..06118fa8409 100644 --- a/mne/datasets/sample/sample.py +++ b/mne/datasets/sample/sample.py @@ -4,7 +4,7 @@ # License: BSD Style. from ...utils import verbose -from ..utils import _data_path_doc, _get_version, _version_doc, _download_mne_dataset +from ..utils import _data_path_doc, _download_mne_dataset, _get_version, _version_doc @verbose diff --git a/mne/datasets/sleep_physionet/_utils.py b/mne/datasets/sleep_physionet/_utils.py index bca3284d73b..ff89155ece7 100644 --- a/mne/datasets/sleep_physionet/_utils.py +++ b/mne/datasets/sleep_physionet/_utils.py @@ -8,8 +8,8 @@ import numpy as np -from ...utils import verbose, _TempDir, _check_pandas_installed, _on_missing -from ..utils import _get_path, _downloader_params +from ...utils import _check_pandas_installed, _on_missing, _TempDir, verbose +from ..utils import _downloader_params, _get_path AGE_SLEEP_RECORDS = op.join(op.dirname(__file__), "age_records.csv") TEMAZEPAM_SLEEP_RECORDS = op.join(op.dirname(__file__), "temazepam_records.csv") diff --git a/mne/datasets/sleep_physionet/age.py b/mne/datasets/sleep_physionet/age.py index b0d23a94b07..bdc98f1be9e 100644 --- a/mne/datasets/sleep_physionet/age.py +++ b/mne/datasets/sleep_physionet/age.py @@ -10,8 +10,13 @@ from ...utils import verbose from ..utils import _log_time_size -from ._utils import _fetch_one, _data_path, _on_missing, AGE_SLEEP_RECORDS -from ._utils import _check_subjects +from ._utils import ( + AGE_SLEEP_RECORDS, + _check_subjects, + _data_path, + _fetch_one, + _on_missing, +) data_path = _data_path # expose _data_path(..) as data_path(..) diff --git a/mne/datasets/sleep_physionet/temazepam.py b/mne/datasets/sleep_physionet/temazepam.py index 8a6efc19a9a..a20cde1e366 100644 --- a/mne/datasets/sleep_physionet/temazepam.py +++ b/mne/datasets/sleep_physionet/temazepam.py @@ -10,8 +10,7 @@ from ...utils import verbose from ..utils import _log_time_size -from ._utils import _fetch_one, _data_path, TEMAZEPAM_SLEEP_RECORDS -from ._utils import _check_subjects +from ._utils import TEMAZEPAM_SLEEP_RECORDS, _check_subjects, _data_path, _fetch_one data_path = _data_path # expose _data_path(..) as data_path(..) diff --git a/mne/datasets/sleep_physionet/tests/test_physionet.py b/mne/datasets/sleep_physionet/tests/test_physionet.py index bab7a17c32b..db3600290b1 100644 --- a/mne/datasets/sleep_physionet/tests/test_physionet.py +++ b/mne/datasets/sleep_physionet/tests/test_physionet.py @@ -4,15 +4,17 @@ # License: BSD Style. from pathlib import Path -import pytest +import pytest -from mne.utils import requires_good_network from mne.datasets.sleep_physionet import age, temazepam -from mne.datasets.sleep_physionet._utils import _update_sleep_temazepam_records -from mne.datasets.sleep_physionet._utils import _update_sleep_age_records -from mne.datasets.sleep_physionet._utils import AGE_SLEEP_RECORDS -from mne.datasets.sleep_physionet._utils import TEMAZEPAM_SLEEP_RECORDS +from mne.datasets.sleep_physionet._utils import ( + AGE_SLEEP_RECORDS, + TEMAZEPAM_SLEEP_RECORDS, + _update_sleep_age_records, + _update_sleep_temazepam_records, +) +from mne.utils import requires_good_network @pytest.fixture(scope="session") diff --git a/mne/datasets/somato/somato.py b/mne/datasets/somato/somato.py index 4dcdeffa4a5..507cc3d32a8 100644 --- a/mne/datasets/somato/somato.py +++ b/mne/datasets/somato/somato.py @@ -4,7 +4,7 @@ # License: BSD Style. from ...utils import verbose -from ..utils import _data_path_doc, _get_version, _version_doc, _download_mne_dataset +from ..utils import _data_path_doc, _download_mne_dataset, _get_version, _version_doc @verbose diff --git a/mne/datasets/spm_face/spm_data.py b/mne/datasets/spm_face/spm_data.py index 5fc1d47691f..d622e4b8453 100644 --- a/mne/datasets/spm_face/spm_data.py +++ b/mne/datasets/spm_face/spm_data.py @@ -4,16 +4,15 @@ from functools import partial -from ...utils import verbose, get_config +from ...utils import get_config, verbose from ..utils import ( - has_dataset, _data_path_doc, + _download_mne_dataset, _get_version, _version_doc, - _download_mne_dataset, + has_dataset, ) - has_spm_data = partial(has_dataset, name="spm") diff --git a/mne/datasets/ssvep/ssvep.py b/mne/datasets/ssvep/ssvep.py index 91f682a6728..6d1861ffca2 100644 --- a/mne/datasets/ssvep/ssvep.py +++ b/mne/datasets/ssvep/ssvep.py @@ -2,7 +2,7 @@ # License: BSD Style. from ...utils import verbose -from ..utils import _data_path_doc, _get_version, _version_doc, _download_mne_dataset +from ..utils import _data_path_doc, _download_mne_dataset, _get_version, _version_doc @verbose diff --git a/mne/datasets/testing/_testing.py b/mne/datasets/testing/_testing.py index 7f2a1c0074e..aee35e59372 100644 --- a/mne/datasets/testing/_testing.py +++ b/mne/datasets/testing/_testing.py @@ -5,13 +5,13 @@ from functools import partial -from ...utils import verbose, get_config +from ...utils import get_config, verbose from ..utils import ( - has_dataset, _data_path_doc, + _download_mne_dataset, _get_version, _version_doc, - _download_mne_dataset, + has_dataset, ) has_testing_data = partial(has_dataset, name="testing") diff --git a/mne/datasets/tests/test_datasets.py b/mne/datasets/tests/test_datasets.py index 211dd4d0b1c..f1569317f8e 100644 --- a/mne/datasets/tests/test_datasets.py +++ b/mne/datasets/tests/test_datasets.py @@ -1,31 +1,29 @@ -from functools import partial import os -from os import path as op import re import shutil import zipfile +from functools import partial +from os import path as op import pooch import pytest from mne import datasets, read_labels_from_annot, write_labels_to_annot -from mne.datasets import testing, fetch_infant_template, fetch_phantom, fetch_dataset +from mne.datasets import fetch_dataset, fetch_infant_template, fetch_phantom, testing from mne.datasets._fsaverage.base import _set_montage_coreg_path from mne.datasets._infant import base as infant_base from mne.datasets._phantom import base as phantom_base from mne.datasets.utils import _manifest_check_download - from mne.utils import ( - requires_good_network, - get_subjects_dir, ArgvSetter, _pl, - use_log_level, catch_logging, + get_subjects_dir, hashfunc, + requires_good_network, + use_log_level, ) - subjects_dir = testing.data_path(download=False) / "subjects" diff --git a/mne/datasets/ucl_opm_auditory/ucl_opm_auditory.py b/mne/datasets/ucl_opm_auditory/ucl_opm_auditory.py index 09853e640de..205dd2ec8ac 100644 --- a/mne/datasets/ucl_opm_auditory/ucl_opm_auditory.py +++ b/mne/datasets/ucl_opm_auditory/ucl_opm_auditory.py @@ -2,8 +2,7 @@ # License: BSD Style. from ...utils import verbose -from ..utils import _data_path_doc, _get_version, _version_doc, _download_mne_dataset - +from ..utils import _data_path_doc, _download_mne_dataset, _get_version, _version_doc _NAME = "ucl_opm_auditory" _PROCESSOR = "unzip" diff --git a/mne/datasets/utils.py b/mne/datasets/utils.py index 041baf32812..9ff4efd3400 100644 --- a/mne/datasets/utils.py +++ b/mne/datasets/utils.py @@ -8,34 +8,33 @@ # # License: BSD Style. -from collections import OrderedDict import importlib import inspect import logging import os import os.path as op -from pathlib import Path import sys -import time import tempfile +import time import zipfile +from collections import OrderedDict +from pathlib import Path import numpy as np -from .config import _hcp_mmp_license_text, MNE_DATASETS -from ..label import read_labels_from_annot, Label, write_labels_to_annot +from ..label import Label, read_labels_from_annot, write_labels_to_annot from ..utils import ( + _pl, + _safe_input, + _validate_type, get_config, - set_config, + get_subjects_dir, logger, - _validate_type, + set_config, verbose, - get_subjects_dir, - _pl, - _safe_input, ) -from ..utils.docs import docdict, _docformat - +from ..utils.docs import _docformat, docdict +from .config import MNE_DATASETS, _hcp_mmp_license_text _data_path_doc = """Get path to local copy of {name} dataset. @@ -213,6 +212,7 @@ def _download_mne_dataset( ): """Aux function for downloading internal MNE datasets.""" import pooch + from mne.datasets._fetch import fetch_dataset _check_in_testing_and_raise(name, download) @@ -341,12 +341,12 @@ def _download_all_example_data(verbose=True): # Now for the exceptions: from . import ( eegbci, - sleep_physionet, - limo, fetch_fsaverage, - fetch_infant_template, fetch_hcp_mmp_parcellation, + fetch_infant_template, fetch_phantom, + limo, + sleep_physionet, ) eegbci.load_data(1, [6, 10, 14], update_path=True) diff --git a/mne/datasets/visual_92_categories/visual_92_categories.py b/mne/datasets/visual_92_categories/visual_92_categories.py index d5fb1c1c8bb..e3a3dfaaae7 100644 --- a/mne/datasets/visual_92_categories/visual_92_categories.py +++ b/mne/datasets/visual_92_categories/visual_92_categories.py @@ -1,7 +1,7 @@ # License: BSD Style. from ...utils import verbose -from ..utils import _download_mne_dataset, _data_path_doc, _get_version, _version_doc +from ..utils import _data_path_doc, _download_mne_dataset, _get_version, _version_doc @verbose diff --git a/mne/decoding/base.py b/mne/decoding/base.py index 6489b7f113a..88838efeeb1 100644 --- a/mne/decoding/base.py +++ b/mne/decoding/base.py @@ -6,12 +6,14 @@ # # License: BSD-3-Clause -import numpy as np import datetime as dt import numbers + +import numpy as np + +from ..fixes import BaseEstimator, _check_fit_params, _get_check_scoring from ..parallel import parallel_func -from ..fixes import BaseEstimator, _get_check_scoring, _check_fit_params -from ..utils import warn, verbose +from ..utils import verbose, warn class LinearModel(BaseEstimator): @@ -155,7 +157,7 @@ def _set_cv(cv, estimator=None, X=None, y=None): est_is_classifier = is_classifier(estimator) # Setup CV from sklearn import model_selection as models - from sklearn.model_selection import check_cv, StratifiedKFold, KFold + from sklearn.model_selection import KFold, StratifiedKFold, check_cv if isinstance(cv, (int, np.int64)): XFold = StratifiedKFold if est_is_classifier else KFold @@ -366,8 +368,8 @@ def cross_val_multiscore( """ # This code is copied from sklearn from sklearn.base import clone, is_classifier - from sklearn.utils import indexable from sklearn.model_selection._split import check_cv + from sklearn.utils import indexable check_scoring = _get_check_scoring() diff --git a/mne/decoding/csp.py b/mne/decoding/csp.py index abb85afc2e9..53cecd53ab0 100644 --- a/mne/decoding/csp.py +++ b/mne/decoding/csp.py @@ -11,13 +11,13 @@ import numpy as np from scipy.linalg import eigh -from .base import BaseEstimator -from .mixin import TransformerMixin from ..cov import _regularized_covariance from ..defaults import _BORDER_DEFAULT, _EXTRAPOLATE_DEFAULT, _INTERPOLATION_DEFAULT -from ..fixes import pinv from ..evoked import EvokedArray -from ..utils import fill_doc, _check_option, _validate_type, copy_doc +from ..fixes import pinv +from ..utils import _check_option, _validate_type, copy_doc, fill_doc +from .base import BaseEstimator +from .mixin import TransformerMixin @fill_doc diff --git a/mne/decoding/ems.py b/mne/decoding/ems.py index efad63cc643..e8b3ac1da43 100644 --- a/mne/decoding/ems.py +++ b/mne/decoding/ems.py @@ -8,11 +8,11 @@ import numpy as np -from .mixin import TransformerMixin, EstimatorMixin -from .base import _set_cv -from .._fiff.pick import _picks_to_idx, pick_types, pick_info +from .._fiff.pick import _picks_to_idx, pick_info, pick_types from ..parallel import parallel_func from ..utils import logger, verbose +from .base import _set_cv +from .mixin import EstimatorMixin, TransformerMixin class EMS(TransformerMixin, EstimatorMixin): diff --git a/mne/decoding/receptive_field.py b/mne/decoding/receptive_field.py index b31fc411a61..4a274fd3997 100644 --- a/mne/decoding/receptive_field.py +++ b/mne/decoding/receptive_field.py @@ -8,10 +8,10 @@ import numpy as np from scipy.stats import pearsonr -from .base import get_coef, BaseEstimator, _check_estimator from ..fixes import pinv +from ..utils import _validate_type, fill_doc, verbose +from .base import BaseEstimator, _check_estimator, get_coef from .time_delaying_ridge import TimeDelayingRidge -from ..utils import _validate_type, verbose, fill_doc @fill_doc diff --git a/mne/decoding/search_light.py b/mne/decoding/search_light.py index 0b476a45e9a..a176c4accac 100644 --- a/mne/decoding/search_light.py +++ b/mne/decoding/search_light.py @@ -2,13 +2,14 @@ # # License: BSD-3-Clause import logging + import numpy as np -from .mixin import TransformerMixin -from .base import BaseEstimator, _check_estimator from ..fixes import _get_check_scoring from ..parallel import parallel_func -from ..utils import array_split_idx, ProgressBar, verbose, fill_doc, _parse_verbose +from ..utils import ProgressBar, _parse_verbose, array_split_idx, fill_doc, verbose +from .base import BaseEstimator, _check_estimator +from .mixin import TransformerMixin @fill_doc diff --git a/mne/decoding/ssd.py b/mne/decoding/ssd.py index 17e8ff11a2d..04c42a1970a 100644 --- a/mne/decoding/ssd.py +++ b/mne/decoding/ssd.py @@ -6,22 +6,22 @@ import numpy as np from scipy.linalg import eigh -from .mixin import TransformerMixin -from ..fixes import BaseEstimator -from ..cov import _regularized_covariance, Covariance +from .._fiff.pick import _picks_to_idx +from ..cov import Covariance, _regularized_covariance from ..defaults import _handle_default from ..filter import filter_data -from .._fiff.pick import _picks_to_idx +from ..fixes import BaseEstimator from ..rank import compute_rank from ..time_frequency import psd_array_welch from ..utils import ( - fill_doc, - logger, _check_option, _time_mask, _validate_type, _verbose_safe_false, + fill_doc, + logger, ) +from .mixin import TransformerMixin @fill_doc diff --git a/mne/decoding/tests/test_base.py b/mne/decoding/tests/test_base.py index 5aa2ebdc7d9..09dc43c1f8d 100644 --- a/mne/decoding/tests/test_base.py +++ b/mne/decoding/tests/test_base.py @@ -4,26 +4,25 @@ # License: BSD-3-Clause import numpy as np +import pytest from numpy.testing import ( - assert_array_equal, - assert_array_almost_equal, - assert_equal, assert_allclose, + assert_array_almost_equal, + assert_array_equal, assert_array_less, + assert_equal, ) -import pytest -from mne import create_info, EpochsArray +from mne import EpochsArray, create_info +from mne.decoding import GeneralizingEstimator, Scaler, TransformerMixin, Vectorizer from mne.decoding.base import ( - _get_inverse_funcs, + BaseEstimator, LinearModel, - get_coef, + _get_inverse_funcs, cross_val_multiscore, - BaseEstimator, + get_coef, ) from mne.decoding.search_light import SlidingEstimator -from mne.decoding import Scaler, TransformerMixin, Vectorizer, GeneralizingEstimator - pytest.importorskip("sklearn") @@ -69,17 +68,17 @@ def _make_data(n_samples=1000, n_features=5, n_targets=3): def test_get_coef(): """Test getting linear coefficients (filters/patterns) from estimators.""" + from sklearn import svm from sklearn.base import ( - TransformerMixin, BaseEstimator, + TransformerMixin, is_classifier, is_regressor, ) - from sklearn.pipeline import make_pipeline - from sklearn.preprocessing import StandardScaler - from sklearn import svm from sklearn.linear_model import Ridge from sklearn.model_selection import GridSearchCV + from sklearn.pipeline import make_pipeline + from sklearn.preprocessing import StandardScaler lm_classification = LinearModel() assert is_classifier(lm_classification) @@ -298,9 +297,9 @@ def test_get_coef_multiclass(n_features, n_targets): ) def test_get_coef_multiclass_full(n_classes, n_channels, n_times): """Test a full example with pattern extraction.""" - from sklearn.pipeline import make_pipeline from sklearn.linear_model import LogisticRegression from sklearn.model_selection import StratifiedKFold + from sklearn.pipeline import make_pipeline data = np.zeros((10 * n_classes, n_channels, n_times)) # Make only the first channel informative @@ -393,8 +392,8 @@ def test_linearmodel(): def test_cross_val_multiscore(): """Test cross_val_multiscore for computing scores on decoding over time.""" + from sklearn.linear_model import LinearRegression, LogisticRegression from sklearn.model_selection import KFold, StratifiedKFold, cross_val_score - from sklearn.linear_model import LogisticRegression, LinearRegression logreg = LogisticRegression(solver="liblinear", random_state=0) @@ -455,8 +454,8 @@ def test_cross_val_multiscore(): def test_sklearn_compliance(): """Test LinearModel compliance with sklearn.""" pytest.importorskip("sklearn") - from sklearn.utils.estimator_checks import check_estimator from sklearn.linear_model import LogisticRegression + from sklearn.utils.estimator_checks import check_estimator lm = LinearModel(LogisticRegression()) ignores = ( diff --git a/mne/decoding/tests/test_csp.py b/mne/decoding/tests/test_csp.py index 3ad04e8c8e3..788a4040de8 100644 --- a/mne/decoding/tests/test_csp.py +++ b/mne/decoding/tests/test_csp.py @@ -11,8 +11,8 @@ import pytest from numpy.testing import assert_array_almost_equal, assert_array_equal, assert_equal -from mne import io, Epochs, read_events, pick_types -from mne.decoding.csp import CSP, _ajd_pham, SPoC +from mne import Epochs, io, pick_types, read_events +from mne.decoding.csp import CSP, SPoC, _ajd_pham data_dir = Path(__file__).parent.parent.parent / "io" / "tests" / "data" raw_fname = data_dir / "test_raw.fif" @@ -283,8 +283,8 @@ def test_regularized_csp(): def test_csp_pipeline(): """Test if CSP works in a pipeline.""" pytest.importorskip("sklearn") - from sklearn.svm import SVC from sklearn.pipeline import Pipeline + from sklearn.svm import SVC csp = CSP(reg=1, norm_trace=False) svc = SVC() diff --git a/mne/decoding/tests/test_ems.py b/mne/decoding/tests/test_ems.py index 70260cf2e8f..6238058658d 100644 --- a/mne/decoding/tests/test_ems.py +++ b/mne/decoding/tests/test_ems.py @@ -5,11 +5,11 @@ from pathlib import Path import numpy as np -from numpy.testing import assert_array_almost_equal, assert_equal import pytest +from numpy.testing import assert_array_almost_equal, assert_equal -from mne import io, Epochs, read_events, pick_types -from mne.decoding import compute_ems, EMS +from mne import Epochs, io, pick_types, read_events +from mne.decoding import EMS, compute_ems data_dir = Path(__file__).parent.parent.parent / "io" / "tests" / "data" raw_fname = data_dir / "test_raw.fif" diff --git a/mne/decoding/tests/test_receptive_field.py b/mne/decoding/tests/test_receptive_field.py index 3153fccc846..6322b7506b1 100644 --- a/mne/decoding/tests/test_receptive_field.py +++ b/mne/decoding/tests/test_receptive_field.py @@ -4,21 +4,20 @@ from pathlib import Path -import pytest import numpy as np +import pytest from numpy import einsum -from numpy.fft import rfft, irfft -from numpy.testing import assert_array_equal, assert_allclose, assert_equal +from numpy.fft import irfft, rfft +from numpy.testing import assert_allclose, assert_array_equal, assert_equal from mne.decoding import ReceptiveField, TimeDelayingRidge from mne.decoding.receptive_field import ( - _delay_time_series, _SCORERS, - _times_to_delays, + _delay_time_series, _delays_to_slice, + _times_to_delays, ) -from mne.decoding.time_delaying_ridge import _compute_reg_neighbors, _compute_corrs - +from mne.decoding.time_delaying_ridge import _compute_corrs, _compute_reg_neighbors data_dir = Path(__file__).parent.parent.parent / "io" / "tests" / "data" raw_fname = data_dir / "test_raw.fif" diff --git a/mne/decoding/tests/test_search_light.py b/mne/decoding/tests/test_search_light.py index 69fce9c7e6f..00b4f98f997 100644 --- a/mne/decoding/tests/test_search_light.py +++ b/mne/decoding/tests/test_search_light.py @@ -5,12 +5,12 @@ from inspect import signature import numpy as np -from numpy.testing import assert_array_equal, assert_equal import pytest +from numpy.testing import assert_array_equal, assert_equal -from mne.utils import _record_warnings, use_log_level -from mne.decoding.search_light import SlidingEstimator, GeneralizingEstimator +from mne.decoding.search_light import GeneralizingEstimator, SlidingEstimator from mne.decoding.transformer import Vectorizer +from mne.utils import _record_warnings, use_log_level pytest.importorskip("sklearn") @@ -29,9 +29,9 @@ def make_data(): def test_search_light(): """Test SlidingEstimator.""" - from sklearn.linear_model import Ridge, LogisticRegression + from sklearn.linear_model import LogisticRegression, Ridge + from sklearn.metrics import make_scorer, roc_auc_score from sklearn.pipeline import make_pipeline - from sklearn.metrics import roc_auc_score, make_scorer with _record_warnings(): # NumPy module import from sklearn.ensemble import BaggingClassifier @@ -172,9 +172,9 @@ def transform(self, X): def test_generalization_light(): """Test GeneralizingEstimator.""" - from sklearn.pipeline import make_pipeline from sklearn.linear_model import LogisticRegression from sklearn.metrics import roc_auc_score + from sklearn.pipeline import make_pipeline logreg = LogisticRegression(solver="liblinear", multi_class="ovr", random_state=0) @@ -283,9 +283,9 @@ def test_verbose_arg(capsys, n_jobs, verbose): def test_cross_val_predict(): """Test cross_val_predict with predict_proba.""" - from sklearn.linear_model import LinearRegression - from sklearn.discriminant_analysis import LinearDiscriminantAnalysis from sklearn.base import BaseEstimator, clone + from sklearn.discriminant_analysis import LinearDiscriminantAnalysis + from sklearn.linear_model import LinearRegression from sklearn.model_selection import cross_val_predict rng = np.random.RandomState(42) @@ -320,8 +320,8 @@ def predict_proba(self, X): def test_sklearn_compliance(): """Test LinearModel compliance with sklearn.""" pytest.importorskip("sklearn") - from sklearn.utils.estimator_checks import check_estimator from sklearn.linear_model import LogisticRegression + from sklearn.utils.estimator_checks import check_estimator est = SlidingEstimator(LogisticRegression(), allow_2d=True) diff --git a/mne/decoding/tests/test_ssd.py b/mne/decoding/tests/test_ssd.py index b9dcfc361c2..18ab707137a 100644 --- a/mne/decoding/tests/test_ssd.py +++ b/mne/decoding/tests/test_ssd.py @@ -6,12 +6,12 @@ import numpy as np import pytest from numpy.testing import assert_array_almost_equal, assert_array_equal -from mne import io -from mne.time_frequency import psd_array_welch + +from mne import create_info, io +from mne.decoding import CSP from mne.decoding.ssd import SSD from mne.filter import filter_data -from mne import create_info -from mne.decoding import CSP +from mne.time_frequency import psd_array_welch freqs_sig = 9, 12 freqs_noise = 8, 13 diff --git a/mne/decoding/tests/test_time_frequency.py b/mne/decoding/tests/test_time_frequency.py index ba2031675e1..c2c24f5d808 100644 --- a/mne/decoding/tests/test_time_frequency.py +++ b/mne/decoding/tests/test_time_frequency.py @@ -4,8 +4,8 @@ import numpy as np -from numpy.testing import assert_array_equal import pytest +from numpy.testing import assert_array_equal from mne.decoding.time_frequency import TimeFrequency diff --git a/mne/decoding/tests/test_transformer.py b/mne/decoding/tests/test_transformer.py index 95076f7d0b4..f1a84c5d41d 100644 --- a/mne/decoding/tests/test_transformer.py +++ b/mne/decoding/tests/test_transformer.py @@ -6,23 +6,22 @@ from pathlib import Path import numpy as np - import pytest from numpy.testing import ( - assert_array_equal, - assert_array_almost_equal, assert_allclose, + assert_array_almost_equal, + assert_array_equal, assert_equal, ) -from mne import io, read_events, Epochs, pick_types +from mne import Epochs, io, pick_types, read_events from mne.decoding import ( - Scaler, FilterEstimator, PSDEstimator, - Vectorizer, - UnsupervisedSpatialFilter, + Scaler, TemporalFilter, + UnsupervisedSpatialFilter, + Vectorizer, ) from mne.defaults import DEFAULTS from mne.utils import check_version, use_log_level diff --git a/mne/decoding/time_delaying_ridge.py b/mne/decoding/time_delaying_ridge.py index 8a979d4a764..849719d56aa 100644 --- a/mne/decoding/time_delaying_ridge.py +++ b/mne/decoding/time_delaying_ridge.py @@ -9,11 +9,11 @@ from scipy.signal import fftconvolve from scipy.sparse.csgraph import laplacian -from .base import BaseEstimator from ..cuda import _setup_cuda_fft_multiply_repeated from ..filter import next_fast_len from ..fixes import jit -from ..utils import warn, ProgressBar, logger, _validate_type, _check_option +from ..utils import ProgressBar, _check_option, _validate_type, logger, warn +from .base import BaseEstimator def _compute_corrs( diff --git a/mne/decoding/time_frequency.py b/mne/decoding/time_frequency.py index d6ed4f6dd56..a138f085b59 100644 --- a/mne/decoding/time_frequency.py +++ b/mne/decoding/time_frequency.py @@ -3,10 +3,11 @@ # License: BSD-3-Clause import numpy as np -from .mixin import TransformerMixin -from .base import BaseEstimator + from ..time_frequency.tfr import _compute_tfr -from ..utils import fill_doc, _check_option, verbose +from ..utils import _check_option, fill_doc, verbose +from .base import BaseEstimator +from .mixin import TransformerMixin @fill_doc diff --git a/mne/decoding/transformer.py b/mne/decoding/transformer.py index 9675f7bef0c..97482dec64e 100644 --- a/mne/decoding/transformer.py +++ b/mne/decoding/transformer.py @@ -6,20 +6,19 @@ import numpy as np -from .mixin import TransformerMixin -from .base import BaseEstimator - -from ..filter import filter_data -from ..time_frequency import psd_array_multitaper -from ..utils import fill_doc, _check_option, _validate_type, verbose from .._fiff.pick import ( - pick_info, - pick_types, _pick_data_channels, _picks_by_type, _picks_to_idx, + pick_info, + pick_types, ) from ..cov import _check_scalings_user +from ..filter import filter_data +from ..time_frequency import psd_array_multitaper +from ..utils import _check_option, _validate_type, fill_doc, verbose +from .base import BaseEstimator +from .mixin import TransformerMixin class _ConstantScaler: diff --git a/mne/dipole.py b/mne/dipole.py index a71bbc590b9..810955011a2 100644 --- a/mne/dipole.py +++ b/mne/dipole.py @@ -5,58 +5,55 @@ # # License: Simplified BSD -from copy import deepcopy import functools -from functools import partial import re +from copy import deepcopy +from functools import partial import numpy as np from scipy.linalg import eigh from scipy.optimize import fmin_cobyla -from .cov import compute_whitener, _ensure_cov from ._fiff.constants import FIFF from ._fiff.pick import pick_types -from ._fiff.proj import make_projector, _needs_eeg_average_ref_proj -from .bem import _fit_sphere -from .evoked import _read_evoked, _aspect_rev, _write_evokeds -from .fixes import pinvh, _safe_svd -from ._freesurfer import read_freesurfer_lut, _get_aseg -from .transforms import _print_coord_trans, _coord_frame_name, apply_trans -from .viz.evoked import _plot_evoked -from ._freesurfer import head_to_mni, head_to_mri +from ._fiff.proj import _needs_eeg_average_ref_proj, make_projector +from ._freesurfer import _get_aseg, head_to_mni, head_to_mri, read_freesurfer_lut +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 .forward._compute_forward import _compute_forwards_meeg, _prep_field_computation from .forward._make_forward import ( _get_trans, - _setup_bem, - _prep_meg_channels, _prep_eeg_channels, + _prep_meg_channels, + _setup_bem, ) -from .forward._compute_forward import _compute_forwards_meeg, _prep_field_computation - -from .surface import transform_surface_to, _compute_nearest, _points_outside_surface -from .bem import _bem_find_surface, _bem_surf_name -from .source_space._source_space import _make_volume_source_space, SourceSpaces from .parallel import parallel_func +from .source_space._source_space import SourceSpaces, _make_volume_source_space +from .surface import _compute_nearest, _points_outside_surface, transform_surface_to +from .transforms import _coord_frame_name, _print_coord_trans, apply_trans from .utils import ( - logger, - verbose, - _time_mask, - warn, + ExtendedTimeMixin, + TimeMixin, _check_fname, - check_fname, - _pl, - fill_doc, _check_option, - _svd_lwork, - _repeated_svd, _get_blas_funcs, + _pl, + _repeated_svd, + _svd_lwork, + _time_mask, _validate_type, - copy_function_doc_to_method_doc, - ExtendedTimeMixin, - TimeMixin, _verbose_safe_false, + check_fname, + copy_function_doc_to_method_doc, + fill_doc, + logger, + verbose, + warn, ) -from .viz import plot_dipole_locations, plot_dipole_amplitudes +from .viz import plot_dipole_amplitudes, plot_dipole_locations +from .viz.evoked import _plot_evoked @fill_doc diff --git a/mne/epochs.py b/mne/epochs.py index e66826b6f50..5af8f382c88 100644 --- a/mne/epochs.py +++ b/mne/epochs.py @@ -9,105 +9,104 @@ # # License: BSD-3-Clause -from functools import partial -from collections import Counter -from copy import deepcopy import json import operator import os.path as op +from collections import Counter +from copy import deepcopy +from functools import partial import numpy as np from scipy.interpolate import interp1d -from .event import make_fixed_length_events, _read_events_fif, match_event_names -from ._fiff.utils import _make_split_fnames -from ._fiff.write import ( - start_and_end_file, - start_block, - end_block, - write_int, - write_float, - write_float_matrix, - write_double_matrix, - write_complex_float_matrix, - write_complex_double_matrix, - write_id, - write_string, - _get_split_size, - _NEXT_FILE_BUFFER, - INT32_MAX, -) +from ._fiff.constants import FIFF from ._fiff.meas_info import ( + ContainsMixin, + SetChannelsMixin, + _ensure_infos_match, read_meas_info, write_meas_info, - _ensure_infos_match, - ContainsMixin, ) -from ._fiff.open import fiff_open, _get_next_fname -from ._fiff.tree import dir_tree_find -from ._fiff.tag import read_tag, read_tag_info -from ._fiff.constants import FIFF +from ._fiff.open import _get_next_fname, fiff_open from ._fiff.pick import ( + _DATA_CH_TYPES_SPLIT, + _pick_data_channels, + _picks_to_idx, channel_indices_by_type, channel_type, pick_channels, pick_info, - _pick_data_channels, - _DATA_CH_TYPES_SPLIT, - _picks_to_idx, ) -from ._fiff.proj import setup_proj, ProjMixin -from ._fiff.meas_info import SetChannelsMixin +from ._fiff.proj import ProjMixin, setup_proj +from ._fiff.tag import read_tag, read_tag_info +from ._fiff.tree import dir_tree_find +from ._fiff.utils import _make_split_fnames +from ._fiff.write import ( + _NEXT_FILE_BUFFER, + INT32_MAX, + _get_split_size, + end_block, + start_and_end_file, + start_block, + write_complex_double_matrix, + write_complex_float_matrix, + write_double_matrix, + write_float, + write_float_matrix, + write_id, + write_int, + write_string, +) +from .annotations import ( + EpochAnnotationsMixin, + _read_annotations_fif, + _write_annotations, +) +from .baseline import _check_baseline, _log_rescale, rescale from .bem import _check_origin +from .channels.channels import InterpolationMixin, ReferenceMixin, UpdateChannelsMixin +from .event import _read_events_fif, make_fixed_length_events, match_event_names from .evoked import EvokedArray -from .baseline import rescale, _log_rescale, _check_baseline +from .filter import FilterMixin, _check_fun, detrend +from .fixes import rng_uniform from .html_templates import _get_html_template -from .channels.channels import UpdateChannelsMixin, InterpolationMixin, ReferenceMixin -from .filter import detrend, FilterMixin, _check_fun from .parallel import parallel_func - -from .fixes import rng_uniform from .time_frequency.spectrum import EpochsSpectrum, SpectrumMixin, _validate_method -from .viz import plot_epochs, plot_epochs_image, plot_topo_image_epochs, plot_drop_log from .utils import ( - _check_fname, - check_fname, - logger, - verbose, - repr_html, - check_random_state, - warn, - _pl, - sizeof_fmt, - SizeMixin, - copy_function_doc_to_method_doc, - _check_pandas_installed, - _check_preload, + ExtendedTimeMixin, GetEpochsMixin, - _prepare_read_metadata, - _prepare_write_metadata, + SizeMixin, + _build_data_frame, + _check_combine, _check_event_id, - _gen_events, + _check_fname, _check_option, - _check_combine, - _build_data_frame, _check_pandas_index_arguments, - _convert_times, - _scale_dataframe_data, + _check_pandas_installed, + _check_preload, _check_time_format, - object_size, - _on_missing, - _validate_type, + _convert_times, _ensure_events, + _gen_events, + _on_missing, _path_like, - ExtendedTimeMixin, + _pl, + _prepare_read_metadata, + _prepare_write_metadata, + _scale_dataframe_data, + _validate_type, + check_fname, + check_random_state, + copy_function_doc_to_method_doc, + logger, + object_size, + repr_html, + sizeof_fmt, + verbose, + warn, ) from .utils.docs import fill_doc -from .annotations import ( - _write_annotations, - _read_annotations_fif, - EpochAnnotationsMixin, -) +from .viz import plot_drop_log, plot_epochs, plot_epochs_image, plot_topo_image_epochs def _pack_reject_params(epochs): @@ -4315,17 +4314,17 @@ def average_movements( estimation, and compensation. NeuroImage 40:541–550, 2008. """ # noqa: E501 from .preprocessing.maxwell import ( - _trans_sss_basis, - _reset_meg_bads, + _check_destination, _check_usable, _col_norm_pinv, - _get_n_moments, + _get_coil_scale, _get_mf_picks_fix_mags, + _get_n_moments, + _get_sensor_operator, _prep_mf_coils, - _check_destination, _remove_meg_projs_comps, - _get_coil_scale, - _get_sensor_operator, + _reset_meg_bads, + _trans_sss_basis, ) if head_pos is None: diff --git a/mne/event.py b/mne/event.py index 625d8033490..17e45707844 100644 --- a/mne/event.py +++ b/mne/event.py @@ -12,26 +12,26 @@ import numpy as np +from ._fiff.constants import FIFF +from ._fiff.open import fiff_open +from ._fiff.pick import pick_channels +from ._fiff.tag import read_tag +from ._fiff.tree import dir_tree_find +from ._fiff.write import end_block, start_and_end_file, start_block, write_int from .utils import ( + _check_fname, + _check_integer_or_list, + _check_on_missing, + _check_option, + _get_stim_channel, + _on_missing, + _validate_type, check_fname, + fill_doc, logger, verbose, - _get_stim_channel, warn, - _validate_type, - _check_option, - fill_doc, - _check_fname, - _on_missing, - _check_on_missing, - _check_integer_or_list, ) -from ._fiff.constants import FIFF -from ._fiff.tree import dir_tree_find -from ._fiff.tag import read_tag -from ._fiff.open import fiff_open -from ._fiff.write import write_int, start_block, start_and_end_file, end_block -from ._fiff.pick import pick_channels @fill_doc diff --git a/mne/evoked.py b/mne/evoked.py index 63b0a93d8ba..f2c75f3754b 100644 --- a/mne/evoked.py +++ b/mne/evoked.py @@ -11,72 +11,71 @@ import numpy as np -from .baseline import rescale, _log_rescale, _check_baseline -from .channels.channels import UpdateChannelsMixin, InterpolationMixin, ReferenceMixin -from .channels.layout import _merge_ch_data, _pair_grad_sensors -from .defaults import _INTERPOLATION_DEFAULT, _EXTRAPOLATE_DEFAULT, _BORDER_DEFAULT -from .filter import detrend, FilterMixin, _check_fun -from .html_templates import _get_html_template -from .utils import ( - check_fname, - logger, - verbose, - warn, - sizeof_fmt, - repr_html, - SizeMixin, - copy_function_doc_to_method_doc, - _validate_type, - fill_doc, - _check_option, - _build_data_frame, - _check_pandas_installed, - _check_pandas_index_arguments, - _convert_times, - _scale_dataframe_data, - _check_time_format, - _check_preload, - _check_fname, - ExtendedTimeMixin, -) -from .viz import ( - plot_evoked, - plot_evoked_topomap, - plot_evoked_field, - plot_evoked_image, - plot_evoked_topo, -) -from .viz.evoked import plot_evoked_white, plot_evoked_joint -from .viz.topomap import _topomap_animation - from ._fiff.constants import FIFF -from ._fiff.open import fiff_open -from ._fiff.tag import read_tag -from ._fiff.tree import dir_tree_find -from ._fiff.pick import pick_types, _picks_to_idx, _FNIRS_CH_TYPES_SPLIT from ._fiff.meas_info import ( ContainsMixin, SetChannelsMixin, - read_meas_info, - write_meas_info, + _ensure_infos_match, _read_extended_ch_info, _rename_list, - _ensure_infos_match, + read_meas_info, + write_meas_info, ) +from ._fiff.open import fiff_open +from ._fiff.pick import _FNIRS_CH_TYPES_SPLIT, _picks_to_idx, pick_types from ._fiff.proj import ProjMixin +from ._fiff.tag import read_tag +from ._fiff.tree import dir_tree_find from ._fiff.write import ( + end_block, start_and_end_file, start_block, - end_block, - write_int, - write_string, + write_complex_float_matrix, + write_float, write_float_matrix, write_id, - write_float, - write_complex_float_matrix, + write_int, + write_string, ) +from .baseline import _check_baseline, _log_rescale, rescale +from .channels.channels import InterpolationMixin, ReferenceMixin, UpdateChannelsMixin +from .channels.layout import _merge_ch_data, _pair_grad_sensors +from .defaults import _BORDER_DEFAULT, _EXTRAPOLATE_DEFAULT, _INTERPOLATION_DEFAULT +from .filter import FilterMixin, _check_fun, detrend +from .html_templates import _get_html_template from .parallel import parallel_func from .time_frequency.spectrum import Spectrum, SpectrumMixin, _validate_method +from .utils import ( + ExtendedTimeMixin, + SizeMixin, + _build_data_frame, + _check_fname, + _check_option, + _check_pandas_index_arguments, + _check_pandas_installed, + _check_preload, + _check_time_format, + _convert_times, + _scale_dataframe_data, + _validate_type, + check_fname, + copy_function_doc_to_method_doc, + fill_doc, + logger, + repr_html, + sizeof_fmt, + verbose, + warn, +) +from .viz import ( + plot_evoked, + plot_evoked_field, + plot_evoked_image, + plot_evoked_topo, + plot_evoked_topomap, +) +from .viz.evoked import plot_evoked_joint, plot_evoked_white +from .viz.topomap import _topomap_animation _aspect_dict = { "average": FIFF.FIFFV_ASPECT_AVERAGE, diff --git a/mne/export/__init__.pyi b/mne/export/__init__.pyi index e5376ed94e3..6cd2a374ef3 100644 --- a/mne/export/__init__.pyi +++ b/mne/export/__init__.pyi @@ -1,3 +1,3 @@ __all__ = ["export_epochs", "export_evokeds", "export_evokeds_mff", "export_raw"] -from ._export import export_raw, export_epochs, export_evokeds from ._egimff import export_evokeds_mff +from ._export import export_epochs, export_evokeds, export_raw diff --git a/mne/export/_edf.py b/mne/export/_edf.py index f0bad43e66a..ae141d64f28 100644 --- a/mne/export/_edf.py +++ b/mne/export/_edf.py @@ -3,6 +3,7 @@ # License: BSD-3-Clause from contextlib import contextmanager + import numpy as np from ..utils import _check_edflib_installed, warn diff --git a/mne/export/_eeglab.py b/mne/export/_eeglab.py index 3fd1cc55902..6d9173f1329 100644 --- a/mne/export/_eeglab.py +++ b/mne/export/_eeglab.py @@ -7,8 +7,8 @@ from ..utils import _check_eeglabio_installed _check_eeglabio_installed() -import eeglabio.raw # noqa: E402 import eeglabio.epochs # noqa: E402 +import eeglabio.raw # noqa: E402 def _export_raw(fname, raw): diff --git a/mne/export/_egimff.py b/mne/export/_egimff.py index 7666ae6db99..0e44bd9531b 100644 --- a/mne/export/_egimff.py +++ b/mne/export/_egimff.py @@ -2,16 +2,16 @@ # # License: BSD-3-Clause -import os -import shutil import datetime +import os import os.path as op +import shutil import numpy as np +from .._fiff.pick import pick_channels, pick_types from ..io.egi.egimff import _import_mffpy -from .._fiff.pick import pick_types, pick_channels -from ..utils import verbose, warn, _check_fname +from ..utils import _check_fname, verbose, warn @verbose diff --git a/mne/export/_export.py b/mne/export/_export.py index 5afa420540c..faeebbb7cec 100644 --- a/mne/export/_export.py +++ b/mne/export/_export.py @@ -4,8 +4,8 @@ import os.path as op +from ..utils import _check_fname, _validate_type, logger, verbose, warn from ._egimff import export_evokeds_mff -from ..utils import logger, verbose, warn, _check_fname, _validate_type @verbose diff --git a/mne/export/tests/test_export.py b/mne/export/tests/test_export.py index 7aeb47a424d..723bac9606a 100644 --- a/mne/export/tests/test_export.py +++ b/mne/export/tests/test_export.py @@ -8,35 +8,35 @@ from os import remove from pathlib import Path -import pytest import numpy as np +import pytest from numpy.testing import assert_allclose, assert_array_almost_equal, assert_array_equal from mne import ( - read_epochs_eeglab, + Annotations, Epochs, + create_info, + read_epochs_eeglab, read_evokeds, read_evokeds_mff, - Annotations, - create_info, ) -from mne.datasets import testing, misc +from mne.datasets import misc, testing from mne.export import export_evokeds, export_evokeds_mff from mne.fixes import _compare_version from mne.io import ( RawArray, - read_raw_fif, - read_raw_eeglab, - read_raw_edf, read_raw_brainvision, + read_raw_edf, + read_raw_eeglab, + read_raw_fif, ) +from mne.tests.test_epochs import _get_data from mne.utils import ( - object_diff, _check_edflib_installed, - _resource_path, _record_warnings, + _resource_path, + object_diff, ) -from mne.tests.test_epochs import _get_data fname_evoked = _resource_path("mne.io.tests.data", "test-ave.fif") fname_raw = _resource_path("mne.io.tests.data", "test_raw.fif") diff --git a/mne/filter.py b/mne/filter.py index b9ef55841be..0592aaf6fbb 100644 --- a/mne/filter.py +++ b/mne/filter.py @@ -9,27 +9,26 @@ from scipy.stats import f as fstat from ._fiff.pick import _picks_to_idx +from ._ola import _COLA from .cuda import ( - _setup_cuda_fft_multiply_repeated, _fft_multiply_repeated, - _setup_cuda_fft_resample, _fft_resample, + _setup_cuda_fft_multiply_repeated, + _setup_cuda_fft_resample, _smart_pad, ) from .parallel import parallel_func from .utils import ( + _check_option, + _check_preload, + _ensure_int, + _pl, + _validate_type, logger, - verbose, sum_squared, + verbose, warn, - _pl, - _check_preload, - _validate_type, - _check_option, - _ensure_int, ) -from ._ola import _COLA - # These values from Ifeachor and Jervis. _length_factors = dict(hann=3.1, hamming=3.3, blackman=5.0) @@ -1858,9 +1857,9 @@ def _check_filterable(x, kind="filtered", alternative="filter"): # using these low-level functions. At the same time, let's # help people who might accidentally use low-level functions that they # shouldn't use by pushing them in the right direction - from .io import BaseRaw from .epochs import BaseEpochs from .evoked import Evoked + from .io import BaseRaw if isinstance(x, (BaseRaw, BaseEpochs, Evoked)): try: @@ -2583,8 +2582,8 @@ def filter( .. versionadded:: 0.15 """ - from .io import BaseRaw from .annotations import _annotations_starts_stops + from .io import BaseRaw _check_preload(self, "inst.filter") if pad is None and method != "iir": diff --git a/mne/fixes.py b/mne/fixes.py index bb7cb1fca01..fe9396f1996 100644 --- a/mne/fixes.py +++ b/mne/fixes.py @@ -16,14 +16,14 @@ # Imports for SciPy submodules need to stay nested in this module # because this module is imported many places (but not always used)! -from contextlib import contextmanager import inspect import operator as operator_module -from math import log -from pprint import pprint -from io import StringIO import os import warnings +from contextlib import contextmanager +from io import StringIO +from math import log +from pprint import pprint import numpy as np @@ -817,9 +817,10 @@ def bincount(x, weights, minlength): # noqa: D103 # scheduled to be fixed by MPL 3.6 def _close_event(fig): """Force calling of the MPL figure close event.""" - from .utils import logger from matplotlib import backend_bases + from .utils import logger + try: fig.canvas.callbacks.process( "close_event", diff --git a/mne/forward/__init__.pyi b/mne/forward/__init__.pyi index 36d35b913ca..dcac0d30881 100644 --- a/mne/forward/__init__.pyi +++ b/mne/forward/__init__.pyi @@ -39,48 +39,48 @@ __all__ = [ "write_forward_solution", ] from . import _lead_dots -from .forward import ( - Forward, - read_forward_solution, - write_forward_solution, - is_fixed_orient, - _read_forward_meas_info, - _select_orient_forward, - compute_orient_prior, - compute_depth_prior, - apply_forward, - apply_forward_raw, - restrict_forward_to_stc, - restrict_forward_to_label, - average_forward_solutions, - _stc_src_sel, - _fill_measurement_info, - _apply_forward, - _subject_from_forward, - convert_forward_solution, - _merge_fwds, - _do_forward_solution, -) from ._compute_forward import ( - _magnetic_dipole_field_vec, _compute_forwards, _concatenate_coils, + _magnetic_dipole_field_vec, ) from ._field_interpolation import ( - _make_surface_mapping, - make_field_map, _as_meg_type_inst, + _make_surface_mapping, _map_meg_or_eeg_channels, + make_field_map, ) from ._make_forward import ( - make_forward_solution, - _prepare_for_forward, - _prep_meg_channels, - _prep_eeg_channels, - _to_forward_dict, _create_meg_coils, + _prep_eeg_channels, + _prep_meg_channels, + _prepare_for_forward, _read_coil_defs, + _to_forward_dict, _transform_orig_meg_coils, make_forward_dipole, + make_forward_solution, use_coil_def, ) +from .forward import ( + Forward, + _apply_forward, + _do_forward_solution, + _fill_measurement_info, + _merge_fwds, + _read_forward_meas_info, + _select_orient_forward, + _stc_src_sel, + _subject_from_forward, + apply_forward, + apply_forward_raw, + average_forward_solutions, + compute_depth_prior, + compute_orient_prior, + convert_forward_solution, + is_fixed_orient, + read_forward_solution, + restrict_forward_to_label, + restrict_forward_to_stc, + write_forward_solution, +) diff --git a/mne/forward/_compute_forward.py b/mne/forward/_compute_forward.py index 970ce2a008c..e6e0ec22d81 100644 --- a/mne/forward/_compute_forward.py +++ b/mne/forward/_compute_forward.py @@ -15,17 +15,17 @@ # 2) EEG and MEG: forward solutions for inverse methods. Mosher, Leahy, and # Lewis, 1999. Generalized discussion of forward solutions. -import numpy as np from copy import deepcopy -from ..fixes import jit, bincount +import numpy as np + from .._fiff.constants import FIFF +from ..bem import _import_openmeeg, _make_openmeeg_geometry +from ..fixes import bincount, jit from ..parallel import parallel_func -from ..surface import _project_onto_surface, _jit_cross +from ..surface import _jit_cross, _project_onto_surface from ..transforms import apply_trans, invert_transform -from ..utils import logger, verbose, _pl, warn, fill_doc, _check_option -from ..bem import _make_openmeeg_geometry, _import_openmeeg - +from ..utils import _check_option, _pl, fill_doc, logger, verbose, warn # ############################################################################# # COIL SPECIFICATION AND FIELD COMPUTATION MATRIX diff --git a/mne/forward/_field_interpolation.py b/mne/forward/_field_interpolation.py index 8a609f29952..43196bcddbf 100644 --- a/mne/forward/_field_interpolation.py +++ b/mne/forward/_field_interpolation.py @@ -10,25 +10,25 @@ import numpy as np from scipy.interpolate import interp1d -from ..bem import _check_origin -from ..cov import make_ad_hoc_cov -from ..fixes import _safe_svd from .._fiff.constants import FIFF -from .._fiff.pick import pick_types, pick_info from .._fiff.meas_info import _simplify_info +from .._fiff.pick import pick_info, pick_types from .._fiff.proj import _has_eeg_average_ref_proj, make_projector +from ..bem import _check_origin +from ..cov import make_ad_hoc_cov +from ..epochs import BaseEpochs, EpochsArray +from ..evoked import Evoked, EvokedArray +from ..fixes import _safe_svd from ..surface import get_head_surf, get_meg_helmet_surf -from ..transforms import transform_surface_to, _find_trans, _get_trans -from ._make_forward import _create_meg_coils, _create_eeg_els, _read_coil_defs +from ..transforms import _find_trans, _get_trans, transform_surface_to +from ..utils import _check_fname, _check_option, _pl, _reg_pinv, logger, verbose from ._lead_dots import ( + _do_cross_dots, _do_self_dots, _do_surface_dots, _get_legen_table, - _do_cross_dots, ) -from ..utils import logger, verbose, _check_option, _reg_pinv, _pl, _check_fname -from ..epochs import EpochsArray, BaseEpochs -from ..evoked import Evoked, EvokedArray +from ._make_forward import _create_eeg_els, _create_meg_coils, _read_coil_defs def _setup_dots(mode, info, coils, ch_type): diff --git a/mne/forward/_lead_dots.py b/mne/forward/_lead_dots.py index 3eda719ac59..8aef9d77002 100644 --- a/mne/forward/_lead_dots.py +++ b/mne/forward/_lead_dots.py @@ -14,8 +14,7 @@ from numpy.polynomial import legendre from ..parallel import parallel_func -from ..utils import logger, verbose, _get_extra_data_path, fill_doc - +from ..utils import _get_extra_data_path, fill_doc, logger, verbose ############################################################################## # FAST LEGENDRE (DERIVATIVE) POLYNOMIALS USING LOOKUP TABLE diff --git a/mne/forward/_make_forward.py b/mne/forward/_make_forward.py index 8bd50339529..d8d4509c19c 100644 --- a/mne/forward/_make_forward.py +++ b/mne/forward/_make_forward.py @@ -8,43 +8,41 @@ # The computations in this code were primarily derived from Matti Hämäläinen's # C code. -from copy import deepcopy -from contextlib import contextmanager -from pathlib import Path import os import os.path as op +from contextlib import contextmanager +from copy import deepcopy +from pathlib import Path import numpy as np -from ._compute_forward import _compute_forwards -from .._fiff.meas_info import read_info, Info -from .._fiff.tag import _loc_to_coil_trans, _loc_to_eeg_loc from .._fiff.compensator import get_current_comp, make_compensator -from .._fiff.pick import _has_kit_refs, pick_types, pick_info from .._fiff.constants import FIFF, FWD +from .._fiff.meas_info import Info, read_info +from .._fiff.pick import _has_kit_refs, pick_info, pick_types +from .._fiff.tag import _loc_to_coil_trans, _loc_to_eeg_loc +from ..bem import ConductorModel, _bem_find_surface, read_bem_solution +from ..source_estimate import VolSourceEstimate +from ..source_space._source_space import ( + _complete_vol_src, + _ensure_src, + _filter_source_spaces, + _make_discrete_source_space, +) +from ..surface import _CheckInside, _normalize_vectors from ..transforms import ( + Transform, + _coord_frame_name, _ensure_trans, - transform_surface_to, - apply_trans, _get_trans, _print_coord_trans, - _coord_frame_name, - Transform, + apply_trans, invert_transform, + transform_surface_to, ) -from ..utils import logger, verbose, warn, _pl, _validate_type, _check_fname -from ..source_space._source_space import ( - _ensure_src, - _filter_source_spaces, - _make_discrete_source_space, - _complete_vol_src, -) -from ..source_estimate import VolSourceEstimate -from ..surface import _normalize_vectors, _CheckInside -from ..bem import read_bem_solution, _bem_find_surface, ConductorModel - -from .forward import Forward, _merge_fwds, convert_forward_solution, _FWD_ORDER - +from ..utils import _check_fname, _pl, _validate_type, logger, verbose, warn +from ._compute_forward import _compute_forwards +from .forward import _FWD_ORDER, Forward, _merge_fwds, convert_forward_solution _accuracy_dict = dict( point=FWD.COIL_ACCURACY_POINT, diff --git a/mne/forward/forward.py b/mne/forward/forward.py index 31aa3c2bdfc..03f5b8f5eae 100644 --- a/mne/forward/forward.py +++ b/mne/forward/forward.py @@ -7,83 +7,82 @@ # The computations in this code were primarily derived from Matti Hämäläinen's # C code. +import os import re +import shutil +import tempfile from copy import deepcopy from os import PathLike +from os import path as op from pathlib import Path from time import time import numpy as np from scipy import sparse -import shutil -import os -from os import path as op -import tempfile - -from ..io import RawArray, BaseRaw -from ..html_templates import _get_html_template from .._fiff.constants import FIFF -from .._fiff.open import fiff_open -from .._fiff.tree import dir_tree_find -from .._fiff.tag import find_tag, read_tag from .._fiff.matrix import ( _read_named_matrix, _transpose_named_matrix, write_named_matrix, ) from .._fiff.meas_info import ( + Info, + _make_ch_names_mapping, _read_bad_channels, - write_info, - _write_ch_infos, _read_extended_ch_info, - _make_ch_names_mapping, _write_bad_channels, - Info, + _write_ch_infos, + write_info, ) -from .._fiff.pick import pick_channels_forward, pick_info, pick_channels, pick_types +from .._fiff.open import fiff_open +from .._fiff.pick import pick_channels, pick_channels_forward, pick_info, pick_types +from .._fiff.tag import find_tag, read_tag +from .._fiff.tree import dir_tree_find from .._fiff.write import ( - write_int, - start_block, end_block, - write_coord_trans, - write_string, start_and_end_file, + start_block, + write_coord_trans, write_id, + write_int, + write_string, ) -from ..evoked import Evoked, EvokedArray from ..epochs import BaseEpochs +from ..evoked import Evoked, EvokedArray +from ..html_templates import _get_html_template +from ..io import BaseRaw, RawArray +from ..label import Label +from ..source_estimate import _BaseSourceEstimate, _BaseVectorSourceEstimate from ..source_space._source_space import ( + _get_src_nn, _read_source_spaces_from_tree, - find_source_space_hemi, _set_source_space_vertices, - _write_source_spaces_to_fid, - _get_src_nn, _src_kind_dict, + _write_source_spaces_to_fid, + find_source_space_hemi, ) -from ..source_estimate import _BaseVectorSourceEstimate, _BaseSourceEstimate from ..surface import _normal_orth -from ..transforms import transform_surface_to, invert_transform, write_trans +from ..transforms import invert_transform, transform_surface_to, write_trans from ..utils import ( - _check_fname, - get_subjects_dir, - has_mne_c, - warn, - run_subprocess, - check_fname, - logger, - verbose, - fill_doc, - _validate_type, _check_compensation_grade, + _check_fname, _check_option, _check_stc_units, - _stamp_to_dt, + _import_h5io_funcs, _on_missing, + _stamp_to_dt, + _validate_type, + check_fname, + fill_doc, + get_subjects_dir, + has_mne_c, + logger, repr_html, - _import_h5io_funcs, + run_subprocess, + verbose, + warn, ) -from ..label import Label class Forward(dict): diff --git a/mne/forward/tests/test_field_interpolation.py b/mne/forward/tests/test_field_interpolation.py index e7a509f7687..d7d0cb5ac3c 100644 --- a/mne/forward/tests/test_field_interpolation.py +++ b/mne/forward/tests/test_field_interpolation.py @@ -2,32 +2,30 @@ from pathlib import Path import numpy as np +import pytest from numpy.polynomial import legendre from numpy.testing import ( assert_allclose, + assert_array_almost_equal, assert_array_equal, assert_equal, - assert_array_almost_equal, ) from scipy.interpolate import interp1d -import pytest - import mne +from mne import Epochs, make_fixed_length_events, pick_types, read_evokeds +from mne.datasets import testing from mne.forward import _make_surface_mapping, make_field_map +from mne.forward._field_interpolation import _setup_dots from mne.forward._lead_dots import ( _comp_sum_eeg, _comp_sums_meg, - _get_legen_table, _do_cross_dots, + _get_legen_table, ) from mne.forward._make_forward import _create_meg_coils -from mne.forward._field_interpolation import _setup_dots -from mne.surface import get_meg_helmet_surf, get_head_surf -from mne.datasets import testing -from mne import read_evokeds, pick_types, make_fixed_length_events, Epochs from mne.io import read_raw_fif - +from mne.surface import get_head_surf, get_meg_helmet_surf base_dir = op.join(op.dirname(__file__), "..", "..", "io", "tests", "data") raw_fname = op.join(base_dir, "test_raw.fif") diff --git a/mne/forward/tests/test_forward.py b/mne/forward/tests/test_forward.py index ee37f11676c..fe9fe9b52bc 100644 --- a/mne/forward/tests/test_forward.py +++ b/mne/forward/tests/test_forward.py @@ -1,41 +1,41 @@ import gc from pathlib import Path -import pytest import numpy as np +import pytest from numpy.testing import ( + assert_allclose, assert_array_almost_equal, - assert_equal, assert_array_equal, - assert_allclose, + assert_equal, ) -from mne._fiff.pick import pick_channels_forward -from mne.datasets import testing from mne import ( - read_forward_solution, + SourceEstimate, + VectorSourceEstimate, apply_forward, apply_forward_raw, average_forward_solutions, - write_forward_solution, convert_forward_solution, - SourceEstimate, pick_types_forward, read_evokeds, - VectorSourceEstimate, + read_forward_solution, + write_forward_solution, ) -from mne.io import read_info -from mne.label import read_label -from mne.utils import requires_mne, run_subprocess +from mne._fiff.pick import pick_channels_forward +from mne.channels import equalize_channels +from mne.datasets import testing from mne.forward import ( - restrict_forward_to_stc, - restrict_forward_to_label, Forward, - is_fixed_orient, - compute_orient_prior, compute_depth_prior, + compute_orient_prior, + is_fixed_orient, + restrict_forward_to_label, + restrict_forward_to_stc, ) -from mne.channels import equalize_channels +from mne.io import read_info +from mne.label import read_label +from mne.utils import requires_mne, run_subprocess data_path = testing.data_path(download=False) fname_meeg = data_path / "MEG" / "sample" / "sample_audvis_trunc-meg-eeg-oct-4-fwd.fif" diff --git a/mne/forward/tests/test_make_forward.py b/mne/forward/tests/test_make_forward.py index 0a2c82b3698..86f3bf2556b 100644 --- a/mne/forward/tests/test_make_forward.py +++ b/mne/forward/tests/test_make_forward.py @@ -1,55 +1,53 @@ from itertools import product from pathlib import Path -import pytest import numpy as np -from numpy.testing import assert_allclose, assert_array_equal -from numpy.testing import assert_array_less +import pytest +from numpy.testing import assert_allclose, assert_array_equal, assert_array_less -from mne.bem import read_bem_surfaces, make_bem_solution -from mne.channels import make_standard_montage -from mne.datasets import testing -from mne.io import read_raw_fif, read_raw_kit, read_raw_bti, read_info -from mne._fiff.constants import FIFF from mne import ( - read_forward_solution, - write_forward_solution, - make_forward_solution, convert_forward_solution, - setup_volume_source_space, - read_source_spaces, create_info, + get_volume_labels_from_aseg, + make_forward_solution, make_sphere_model, - pick_types_forward, pick_info, pick_types, - read_evokeds, + pick_types_forward, read_cov, read_dipole, - get_volume_labels_from_aseg, -) -from mne.surface import _get_ico_surface -from mne.transforms import Transform -from mne.utils import ( - requires_mne, - run_subprocess, - catch_logging, - requires_mne_mark, - requires_openmeeg_mark, + read_evokeds, + read_forward_solution, + read_source_spaces, + setup_volume_source_space, + write_forward_solution, ) -from mne.forward._make_forward import _create_meg_coils, make_forward_dipole -from mne.forward._compute_forward import _magnetic_dipole_field_vec -from mne.forward import Forward, _do_forward_solution, use_coil_def +from mne._fiff.constants import FIFF +from mne.bem import make_bem_solution, read_bem_surfaces +from mne.channels import make_standard_montage +from mne.datasets import testing from mne.dipole import Dipole, fit_dipole +from mne.forward import Forward, _do_forward_solution, use_coil_def +from mne.forward._compute_forward import _magnetic_dipole_field_vec +from mne.forward._make_forward import _create_meg_coils, make_forward_dipole +from mne.forward.tests.test_forward import assert_forward_allclose +from mne.io import read_info, read_raw_bti, read_raw_fif, read_raw_kit from mne.simulation import simulate_evoked from mne.source_estimate import VolSourceEstimate from mne.source_space._source_space import ( - write_source_spaces, _compare_source_spaces, setup_source_space, + write_source_spaces, +) +from mne.surface import _get_ico_surface +from mne.transforms import Transform +from mne.utils import ( + catch_logging, + requires_mne, + requires_mne_mark, + requires_openmeeg_mark, + run_subprocess, ) - -from mne.forward.tests.test_forward import assert_forward_allclose data_path = testing.data_path(download=False) fname_meeg = data_path / "MEG" / "sample" / "sample_audvis_trunc-meg-eeg-oct-4-fwd.fif" diff --git a/mne/gui/__init__.pyi b/mne/gui/__init__.pyi index 77a7310e4ad..086c51a4904 100644 --- a/mne/gui/__init__.pyi +++ b/mne/gui/__init__.pyi @@ -1,2 +1,2 @@ __all__ = ["_GUIScraper", "coregistration"] -from ._gui import coregistration, _GUIScraper +from ._gui import _GUIScraper, coregistration diff --git a/mne/gui/_coreg.py b/mne/gui/_coreg.py index e11b61ed898..3782303c58e 100644 --- a/mne/gui/_coreg.py +++ b/mne/gui/_coreg.py @@ -1,66 +1,65 @@ -from contextlib import contextmanager -from functools import partial import inspect import os import os.path as op import platform -from pathlib import Path -import time import queue -import threading import re +import threading +import time +from contextlib import contextmanager +from functools import partial +from pathlib import Path import numpy as np -from traitlets import observe, HasTraits, Unicode, Bool, Float +from traitlets import Bool, Float, HasTraits, Unicode, observe -from ..defaults import DEFAULTS from .._fiff.constants import FIFF -from .._fiff.meas_info import read_info, read_fiducials, write_fiducials +from .._fiff.meas_info import _empty_info, read_fiducials, read_info, write_fiducials +from .._fiff.open import dir_tree_find, fiff_open from .._fiff.pick import pick_types -from .._fiff.open import fiff_open, dir_tree_find -from .._fiff.meas_info import _empty_info -from ..io._read_raw import _get_supported, read_raw from ..bem import make_bem_solution, write_bem_solution +from ..channels import read_dig_fif from ..coreg import ( Coregistration, + _find_head_bem, _is_mri_subject, - scale_mri, - bem_fname, + _map_fid_name_to_idx, _mri_subject_has_bem, + bem_fname, fid_fname, - _map_fid_name_to_idx, - _find_head_bem, -) -from ..viz._3d import ( - _plot_head_surface, - _plot_head_fiducials, - _plot_head_shape_points, - _plot_mri_fiducials, - _plot_hpi_coils, - _plot_sensors, - _plot_helmet, + scale_mri, ) -from ..viz.backends._utils import _qt_app_exec, _qt_safe_window -from ..viz.utils import safe_event +from ..defaults import DEFAULTS +from ..io._read_raw import _get_supported, read_raw +from ..surface import _CheckInside, _DistanceQuery from ..transforms import ( - read_trans, - write_trans, _ensure_trans, _get_trans, - rotation_angles, _get_transforms_to_coord_frame, + read_trans, + rotation_angles, + write_trans, ) from ..utils import ( - get_subjects_dir, - check_fname, _check_fname, + _validate_type, + check_fname, fill_doc, - verbose, + get_subjects_dir, logger, - _validate_type, + verbose, ) -from ..surface import _DistanceQuery, _CheckInside -from ..channels import read_dig_fif +from ..viz._3d import ( + _plot_head_fiducials, + _plot_head_shape_points, + _plot_head_surface, + _plot_helmet, + _plot_hpi_coils, + _plot_mri_fiducials, + _plot_sensors, +) +from ..viz.backends._utils import _qt_app_exec, _qt_safe_window +from ..viz.utils import safe_event class _WorkerData: diff --git a/mne/gui/_gui.py b/mne/gui/_gui.py index 122e2dc772c..de6e35482b8 100644 --- a/mne/gui/_gui.py +++ b/mne/gui/_gui.py @@ -2,7 +2,7 @@ # # License: BSD-3-Clause -from ..utils import verbose, get_config, warn +from ..utils import get_config, verbose, warn @verbose @@ -238,13 +238,13 @@ def __call__(self, block, block_vars, gallery_conf): try: from mne_gui_addons._ieeg_locate import ( IntracranialElectrodeLocator, - ) # noqa: E501 + ) except Exception: pass else: gui_classes = gui_classes + (IntracranialElectrodeLocator,) - from sphinx_gallery.scrapers import figure_rst from qtpy import QtGui + from sphinx_gallery.scrapers import figure_rst for gui in block_vars["example_globals"].values(): if ( diff --git a/mne/gui/tests/test_coreg.py b/mne/gui/tests/test_coreg.py index a0e52a2afd9..1805755d0ff 100644 --- a/mne/gui/tests/test_coreg.py +++ b/mne/gui/tests/test_coreg.py @@ -6,21 +6,20 @@ from contextlib import nullcontext from pathlib import Path +import numpy as np import pytest from numpy.testing import assert_allclose -import numpy as np import mne -from mne.datasets import testing -from mne.io import read_info -from mne.io.kit.tests import data_dir as kit_data_dir from mne._fiff.constants import FIFF -from mne.utils import get_config, catch_logging from mne.channels import DigMontage from mne.coreg import Coregistration +from mne.datasets import testing +from mne.io import read_info +from mne.io.kit.tests import data_dir as kit_data_dir +from mne.utils import catch_logging, get_config from mne.viz import _3d - data_path = testing.data_path(download=False) raw_path = data_path / "MEG" / "sample" / "sample_audvis_trunc_raw.fif" fname_trans = data_path / "MEG" / "sample" / "sample_audvis_trunc-trans.fif" @@ -335,6 +334,7 @@ def test_coreg_gui_scraper(tmp_path, renderer_interactive_pyvistaqt): def test_coreg_gui_notebook(renderer_notebook, nbexec): """Test the coregistration UI in a notebook.""" import pytest + import mne from mne.datasets import testing from mne.gui import coregistration diff --git a/mne/gui/tests/test_gui_api.py b/mne/gui/tests/test_gui_api.py index 26a3d59e581..8f805a18878 100644 --- a/mne/gui/tests/test_gui_api.py +++ b/mne/gui/tests/test_gui_api.py @@ -13,9 +13,10 @@ def test_gui_api(renderer_notebook, nbexec, *, n_warn=0, backend="qt"): """Test GUI API.""" import contextlib - import mne - import warnings import sys + import warnings + + import mne try: # Function diff --git a/mne/inverse_sparse/__init__.pyi b/mne/inverse_sparse/__init__.pyi index 5cd096d92c1..557921114ca 100644 --- a/mne/inverse_sparse/__init__.pyi +++ b/mne/inverse_sparse/__init__.pyi @@ -1,3 +1,3 @@ __all__ = ["gamma_map", "make_stc_from_dipoles", "mixed_norm", "tf_mixed_norm"] -from .mxne_inverse import mixed_norm, tf_mixed_norm, make_stc_from_dipoles from ._gamma_map import gamma_map +from .mxne_inverse import make_stc_from_dipoles, mixed_norm, tf_mixed_norm diff --git a/mne/inverse_sparse/_gamma_map.py b/mne/inverse_sparse/_gamma_map.py index 7cbb82dbe4e..999e0274a25 100644 --- a/mne/inverse_sparse/_gamma_map.py +++ b/mne/inverse_sparse/_gamma_map.py @@ -10,11 +10,11 @@ from ..utils import logger, verbose, warn from .mxne_inverse import ( _check_ori, + _compute_residual, + _make_dipoles_sparse, _make_sparse_stc, _prepare_gain, _reapply_source_weighting, - _compute_residual, - _make_dipoles_sparse, ) diff --git a/mne/inverse_sparse/mxne_debiasing.py b/mne/inverse_sparse/mxne_debiasing.py index 472afc1242c..6cb0159dde9 100644 --- a/mne/inverse_sparse/mxne_debiasing.py +++ b/mne/inverse_sparse/mxne_debiasing.py @@ -4,9 +4,10 @@ # License: BSD-3-Clause from math import sqrt + import numpy as np -from ..utils import check_random_state, logger, verbose, fill_doc +from ..utils import check_random_state, fill_doc, logger, verbose @fill_doc diff --git a/mne/inverse_sparse/mxne_inverse.py b/mne/inverse_sparse/mxne_inverse.py index 640e4df11a1..48da331ff6d 100644 --- a/mne/inverse_sparse/mxne_inverse.py +++ b/mne/inverse_sparse/mxne_inverse.py @@ -5,37 +5,36 @@ import numpy as np -from ..source_estimate import SourceEstimate, _BaseSourceEstimate, _make_stc +from .._fiff.proj import deactivate_proj +from ..dipole import Dipole +from ..fixes import _safe_svd +from ..forward import is_fixed_orient from ..minimum_norm.inverse import ( - combine_xyz, - _prepare_forward, _check_reference, _log_exp_var, + _prepare_forward, + combine_xyz, ) -from ..fixes import _safe_svd -from ..forward import is_fixed_orient -from .._fiff.proj import deactivate_proj +from ..source_estimate import SourceEstimate, _BaseSourceEstimate, _make_stc from ..utils import ( - logger, - verbose, _check_depth, _check_option, - sum_squared, _validate_type, check_random_state, + logger, + sum_squared, + verbose, warn, ) -from ..dipole import Dipole - from .mxne_optim import ( - mixed_norm_solver, - iterative_mixed_norm_solver, _Phi, - tf_mixed_norm_solver, + groups_norm2, + iterative_mixed_norm_solver, iterative_tf_mixed_norm_solver, - norm_l2inf, + mixed_norm_solver, norm_epsilon_inf, - groups_norm2, + norm_l2inf, + tf_mixed_norm_solver, ) diff --git a/mne/inverse_sparse/mxne_optim.py b/mne/inverse_sparse/mxne_optim.py index e4e29912b68..c9d4fc83618 100644 --- a/mne/inverse_sparse/mxne_optim.py +++ b/mne/inverse_sparse/mxne_optim.py @@ -8,17 +8,17 @@ import numpy as np -from .mxne_debiasing import compute_bias +from ..time_frequency._stft import istft, stft, stft_norm1, stft_norm2 from ..utils import ( + _check_option, + _get_blas_funcs, + _validate_type, logger, - verbose, sum_squared, + verbose, warn, - _get_blas_funcs, - _validate_type, - _check_option, ) -from ..time_frequency._stft import stft_norm1, stft_norm2, stft, istft +from .mxne_debiasing import compute_bias @functools.lru_cache(None) diff --git a/mne/inverse_sparse/tests/test_gamma_map.py b/mne/inverse_sparse/tests/test_gamma_map.py index c6b94c7d9eb..8aabf13352f 100644 --- a/mne/inverse_sparse/tests/test_gamma_map.py +++ b/mne/inverse_sparse/tests/test_gamma_map.py @@ -2,26 +2,26 @@ # # License: Simplified BSD -import pytest import numpy as np -from numpy.testing import assert_array_almost_equal, assert_allclose +import pytest +from numpy.testing import assert_allclose, assert_array_almost_equal import mne -from mne.datasets import testing from mne import ( + VectorSourceEstimate, + convert_forward_solution, + pick_types_forward, read_cov, - read_forward_solution, read_evokeds, - convert_forward_solution, - VectorSourceEstimate, + read_forward_solution, ) from mne.cov import regularize +from mne.datasets import testing +from mne.dipole import Dipole from mne.inverse_sparse import gamma_map from mne.inverse_sparse.mxne_inverse import make_stc_from_dipoles from mne.minimum_norm.tests.test_inverse import assert_stc_res, assert_var_exp_log -from mne import pick_types_forward from mne.utils import assert_stcs_equal, catch_logging -from mne.dipole import Dipole data_path = testing.data_path(download=False) fname_evoked = data_path / "MEG" / "sample" / "sample_audvis-ave.fif" diff --git a/mne/inverse_sparse/tests/test_mxne_inverse.py b/mne/inverse_sparse/tests/test_mxne_inverse.py index 19b26bc7483..0376ed83f93 100644 --- a/mne/inverse_sparse/tests/test_mxne_inverse.py +++ b/mne/inverse_sparse/tests/test_mxne_inverse.py @@ -4,29 +4,31 @@ # License: Simplified BSD import numpy as np +import pytest from numpy.testing import ( - assert_array_almost_equal, assert_allclose, - assert_array_less, + assert_array_almost_equal, assert_array_equal, + assert_array_less, ) -import pytest import mne +from mne import convert_forward_solution, read_cov, read_evokeds, read_forward_solution from mne.datasets import testing -from mne.label import read_label -from mne import read_cov, read_forward_solution, read_evokeds, convert_forward_solution +from mne.dipole import Dipole from mne.inverse_sparse import mixed_norm, tf_mixed_norm -from mne.inverse_sparse.mxne_inverse import make_stc_from_dipoles, _split_gof -from mne.inverse_sparse.mxne_inverse import _compute_mxne_sure +from mne.inverse_sparse.mxne_inverse import ( + _compute_mxne_sure, + _split_gof, + make_stc_from_dipoles, +) from mne.inverse_sparse.mxne_optim import norm_l2inf +from mne.label import read_label from mne.minimum_norm import apply_inverse, make_inverse_operator -from mne.minimum_norm.tests.test_inverse import assert_var_exp_log, assert_stc_res -from mne.utils import assert_stcs_equal, catch_logging, _record_warnings -from mne.dipole import Dipole +from mne.minimum_norm.tests.test_inverse import assert_stc_res, assert_var_exp_log +from mne.simulation import simulate_evoked, simulate_sparse_stc from mne.source_estimate import VolSourceEstimate -from mne.simulation import simulate_sparse_stc, simulate_evoked - +from mne.utils import _record_warnings, assert_stcs_equal, catch_logging data_path = testing.data_path(download=False) # NOTE: These use the ave and cov from sample dataset (no _trunc) diff --git a/mne/inverse_sparse/tests/test_mxne_optim.py b/mne/inverse_sparse/tests/test_mxne_optim.py index b0779c01e7c..bc1ed349acb 100644 --- a/mne/inverse_sparse/tests/test_mxne_optim.py +++ b/mne/inverse_sparse/tests/test_mxne_optim.py @@ -3,28 +3,28 @@ # # License: Simplified BSD -import pytest import numpy as np +import pytest from numpy.testing import ( - assert_array_equal, - assert_array_almost_equal, assert_allclose, + assert_array_almost_equal, + assert_array_equal, assert_array_less, ) from mne.inverse_sparse.mxne_optim import ( - mixed_norm_solver, - tf_mixed_norm_solver, - iterative_mixed_norm_solver, - iterative_tf_mixed_norm_solver, - norm_epsilon_inf, - norm_epsilon, _Phi, _PhiT, dgap_l21l1, + iterative_mixed_norm_solver, + iterative_tf_mixed_norm_solver, + mixed_norm_solver, + norm_epsilon, + norm_epsilon_inf, + tf_mixed_norm_solver, ) from mne.time_frequency._stft import stft_norm2 -from mne.utils import catch_logging, _record_warnings +from mne.utils import _record_warnings, catch_logging def _generate_tf_data(): diff --git a/mne/io/__init__.pyi b/mne/io/__init__.pyi index e7fba58667a..1a0cccf8c54 100644 --- a/mne/io/__init__.pyi +++ b/mne/io/__init__.pyi @@ -48,40 +48,40 @@ __all__ = [ "write_info", ] from . import constants, pick -from .base import BaseRaw, concatenate_raws, match_channel_orders +from ._fiff_wrap import ( + anonymize_info, + get_channel_type_constants, + read_fiducials, + read_info, + show_fiff, + write_fiducials, + write_info, +) +from ._read_raw import read_raw from .array import RawArray +from .artemis123 import read_raw_artemis123 +from .base import BaseRaw, concatenate_raws, match_channel_orders from .besa import read_evoked_besa +from .boxy import read_raw_boxy from .brainvision import read_raw_brainvision from .bti import read_raw_bti from .cnt import read_raw_cnt from .ctf import read_raw_ctf from .curry import read_raw_curry -from .edf import read_raw_edf, read_raw_bdf, read_raw_gdf -from .egi import read_raw_egi, read_evokeds_mff -from .kit import read_raw_kit, read_epochs_kit -from .fiff import read_raw_fif, Raw +from .edf import read_raw_bdf, read_raw_edf, read_raw_gdf +from .eeglab import read_epochs_eeglab, read_raw_eeglab +from .egi import read_evokeds_mff, read_raw_egi +from .eximia import read_raw_eximia +from .eyelink import read_raw_eyelink +from .fieldtrip import read_epochs_fieldtrip, read_evoked_fieldtrip, read_raw_fieldtrip +from .fiff import Raw, read_raw_fif from .fil import read_raw_fil +from .hitachi import read_raw_hitachi +from .kit import read_epochs_kit, read_raw_kit from .nedf import read_raw_nedf from .nicolet import read_raw_nicolet -from .artemis123 import read_raw_artemis123 -from .eeglab import read_raw_eeglab, read_epochs_eeglab -from .eximia import read_raw_eximia -from .hitachi import read_raw_hitachi -from .nirx import read_raw_nirx -from .boxy import read_raw_boxy -from .snirf import read_raw_snirf -from .persyst import read_raw_persyst -from .fieldtrip import read_raw_fieldtrip, read_epochs_fieldtrip, read_evoked_fieldtrip from .nihon import read_raw_nihon +from .nirx import read_raw_nirx from .nsx import read_raw_nsx -from ._read_raw import read_raw -from .eyelink import read_raw_eyelink -from ._fiff_wrap import ( - read_info, - write_info, - anonymize_info, - read_fiducials, - write_fiducials, - show_fiff, - get_channel_type_constants, -) +from .persyst import read_raw_persyst +from .snirf import read_raw_snirf diff --git a/mne/io/_fiff_wrap.py b/mne/io/_fiff_wrap.py index 974b08c3041..df9e48e7644 100644 --- a/mne/io/_fiff_wrap.py +++ b/mne/io/_fiff_wrap.py @@ -3,13 +3,15 @@ # Backward compat since these were in the public API before switching to _fiff # (and _empty_info is convenient to keep here for tests and is private) from .._fiff.meas_info import ( - read_info, - write_info, + Info as _info, +) +from .._fiff.meas_info import ( + _empty_info, anonymize_info, read_fiducials, + read_info, write_fiducials, - _empty_info, - Info as _info, + write_info, ) from .._fiff.open import show_fiff from .._fiff.pick import get_channel_type_constants # moved up a level diff --git a/mne/io/_read_raw.py b/mne/io/_read_raw.py index d82df247181..fafc43a0d9a 100644 --- a/mne/io/_read_raw.py +++ b/mne/io/_read_raw.py @@ -5,8 +5,8 @@ # License: BSD-3-Clause -from pathlib import Path from functools import partial +from pathlib import Path from ..utils import fill_doc @@ -24,27 +24,27 @@ def _read_unsupported(fname, **kwargs): # supported read file formats def _get_supported(): from . import ( - read_raw_edf, + read_raw_artemis123, read_raw_bdf, - read_raw_gdf, + read_raw_boxy, read_raw_brainvision, - read_raw_fif, - read_raw_eeglab, read_raw_cnt, + read_raw_ctf, + read_raw_curry, + read_raw_edf, + read_raw_eeglab, read_raw_egi, read_raw_eximia, - read_raw_nirx, read_raw_fieldtrip, - read_raw_artemis123, - read_raw_nicolet, - read_raw_kit, - read_raw_ctf, - read_raw_boxy, - read_raw_snirf, + read_raw_fif, read_raw_fil, - read_raw_nihon, - read_raw_curry, + read_raw_gdf, + read_raw_kit, read_raw_nedf, + read_raw_nicolet, + read_raw_nihon, + read_raw_nirx, + read_raw_snirf, ) return { diff --git a/mne/io/array/array.py b/mne/io/array/array.py index 7e7ffded42a..5b75ef838cb 100644 --- a/mne/io/array/array.py +++ b/mne/io/array/array.py @@ -6,8 +6,8 @@ import numpy as np +from ...utils import _check_option, _validate_type, fill_doc, logger, verbose from ..base import BaseRaw -from ...utils import verbose, logger, _validate_type, fill_doc, _check_option @fill_doc diff --git a/mne/io/array/tests/test_array.py b/mne/io/array/tests/test_array.py index e7b276ab903..315e921e1ab 100644 --- a/mne/io/array/tests/test_array.py +++ b/mne/io/array/tests/test_array.py @@ -4,18 +4,18 @@ from pathlib import Path +import matplotlib.pyplot as plt import numpy as np -from numpy.testing import assert_array_almost_equal, assert_allclose, assert_equal import pytest -import matplotlib.pyplot as plt +from numpy.testing import assert_allclose, assert_array_almost_equal, assert_equal -from mne import find_events, Epochs, pick_types -from mne.io import read_raw_fif -from mne.io.array import RawArray -from mne.io.tests.test_raw import _test_raw_reader +from mne import Epochs, find_events, pick_types from mne._fiff.meas_info import create_info from mne._fiff.pick import get_channel_type_constants from mne.channels import make_dig_montage +from mne.io import read_raw_fif +from mne.io.array import RawArray +from mne.io.tests.test_raw import _test_raw_reader base_dir = Path(__file__).parent.parent.parent / "tests" / "data" fif_fname = base_dir / "test_raw.fif" diff --git a/mne/io/artemis123/artemis123.py b/mne/io/artemis123/artemis123.py index 8d937067a5d..1d131da8376 100644 --- a/mne/io/artemis123/artemis123.py +++ b/mne/io/artemis123/artemis123.py @@ -2,21 +2,21 @@ # # License: BSD-3-Clause -import numpy as np -import os.path as op -import datetime import calendar +import datetime +import os.path as op +import numpy as np from scipy.spatial.distance import cdist -from .utils import _load_mne_locs, _read_pos -from ..base import BaseRaw -from ...utils import logger, warn, verbose, _check_fname -from ..._fiff.utils import _read_segments_file -from ..._fiff.meas_info import _empty_info -from ..._fiff._digitization import _make_dig_points, DigPoint +from ..._fiff._digitization import DigPoint, _make_dig_points from ..._fiff.constants import FIFF -from ...transforms import get_ras_to_neuromag_trans, apply_trans, Transform +from ..._fiff.meas_info import _empty_info +from ..._fiff.utils import _read_segments_file +from ...transforms import Transform, apply_trans, get_ras_to_neuromag_trans +from ...utils import _check_fname, logger, verbose, warn +from ..base import BaseRaw +from .utils import _load_mne_locs, _read_pos @verbose @@ -341,9 +341,9 @@ def __init__( add_head_trans=True, ): # noqa: D102 from ...chpi import ( + _fit_coil_order_dev_head_trans, compute_chpi_amplitudes, compute_chpi_locs, - _fit_coil_order_dev_head_trans, ) input_fname = str(_check_fname(input_fname, "read", True, "input_fname")) diff --git a/mne/io/artemis123/tests/test_artemis123.py b/mne/io/artemis123/tests/test_artemis123.py index 7587dfd0d96..ed17ab0f118 100644 --- a/mne/io/artemis123/tests/test_artemis123.py +++ b/mne/io/artemis123/tests/test_artemis123.py @@ -3,16 +3,16 @@ # License: BSD-3-Clause import numpy as np -from numpy.testing import assert_allclose, assert_equal import pytest +from numpy.testing import assert_allclose, assert_equal -from mne.io import read_raw_artemis123 -from mne.io.tests.test_raw import _test_raw_reader -from mne.datasets import testing -from mne.io.artemis123.utils import _generate_mne_locs_file, _load_mne_locs from mne import pick_types -from mne.transforms import rot_to_quat, _angle_between_quats from mne._fiff.constants import FIFF +from mne.datasets import testing +from mne.io import read_raw_artemis123 +from mne.io.artemis123.utils import _generate_mne_locs_file, _load_mne_locs +from mne.io.tests.test_raw import _test_raw_reader +from mne.transforms import _angle_between_quats, rot_to_quat artemis123_dir = testing.data_path(download=False) / "ARTEMIS123" short_HPI_dip_fname = ( diff --git a/mne/io/artemis123/utils.py b/mne/io/artemis123/utils.py index f478ecc5e28..fb2e72f01bf 100644 --- a/mne/io/artemis123/utils.py +++ b/mne/io/artemis123/utils.py @@ -1,8 +1,10 @@ -import numpy as np import os.path as op + +import numpy as np + from ..._fiff._digitization import _artemis123_read_pos -from ...utils import logger from ...transforms import rotation3d_align_z_axis +from ...utils import logger def _load_mne_locs(fname=None): diff --git a/mne/io/base.py b/mne/io/base.py index 5b1125a8115..4c1d13ffaf7 100644 --- a/mne/io/base.py +++ b/mne/io/base.py @@ -9,56 +9,49 @@ # # License: BSD-3-Clause -from contextlib import nullcontext -from copy import deepcopy -from datetime import timedelta import os import os.path as op import shutil from collections import defaultdict +from contextlib import nullcontext +from copy import deepcopy from dataclasses import dataclass, field +from datetime import timedelta import numpy as np -from ..filter import _check_resamp_noop -from ..event import find_events, concatenate_events +from .._fiff.compensator import make_compensator, set_current_comp from .._fiff.constants import FIFF -from .._fiff.utils import _make_split_fnames, _check_orig_units -from .._fiff.pick import ( - pick_types, - pick_channels, - pick_info, - _picks_to_idx, - channel_type, -) from .._fiff.meas_info import ( - write_meas_info, - _ensure_infos_match, ContainsMixin, SetChannelsMixin, + _ensure_infos_match, _unit2human, + write_meas_info, ) -from .._fiff.proj import setup_proj, activate_proj, _proj_equal, ProjMixin -from ..channels.channels import ( - UpdateChannelsMixin, - InterpolationMixin, - ReferenceMixin, +from .._fiff.pick import ( + _picks_to_idx, + channel_type, + pick_channels, + pick_info, + pick_types, ) -from .._fiff.compensator import set_current_comp, make_compensator +from .._fiff.proj import ProjMixin, _proj_equal, activate_proj, setup_proj +from .._fiff.utils import _check_orig_units, _make_split_fnames from .._fiff.write import ( + _NEXT_FILE_BUFFER, + _get_split_size, + end_block, start_and_end_file, start_block, - end_block, - write_dau_pack16, - write_float, - write_double, write_complex64, write_complex128, - write_int, + write_dau_pack16, + write_double, + write_float, write_id, + write_int, write_string, - _get_split_size, - _NEXT_FILE_BUFFER, ) from ..annotations import ( Annotations, @@ -68,49 +61,56 @@ _sync_onset, _write_annotations, ) +from ..channels.channels import ( + InterpolationMixin, + ReferenceMixin, + UpdateChannelsMixin, +) +from ..defaults import _handle_default +from ..event import concatenate_events, find_events from ..filter import ( FilterMixin, - notch_filter, - resample, + _check_fun, + _check_resamp_noop, _resamp_ratio_len, _resample_stim_channels, - _check_fun, + notch_filter, + resample, ) from ..html_templates import _get_html_template from ..parallel import parallel_func +from ..time_frequency.spectrum import Spectrum, SpectrumMixin, _validate_method from ..utils import ( + SizeMixin, + TimeMixin, + _arange_div, + _build_data_frame, _check_fname, - _check_pandas_installed, - sizeof_fmt, + _check_option, _check_pandas_index_arguments, - fill_doc, - copy_doc, - check_fname, + _check_pandas_installed, + _check_preload, + _check_time_format, + _convert_times, + _file_like, + _get_argvalues, _get_stim_channel, + _pl, + _scale_dataframe_data, _stamp_to_dt, - logger, - verbose, _time_mask, - warn, - SizeMixin, - copy_function_doc_to_method_doc, _validate_type, - _check_preload, - _get_argvalues, - _check_option, - _build_data_frame, - _convert_times, - _scale_dataframe_data, - _check_time_format, - _arange_div, - TimeMixin, + check_fname, + copy_doc, + copy_function_doc_to_method_doc, + fill_doc, + logger, repr_html, - _pl, - _file_like, + sizeof_fmt, + verbose, + warn, ) -from ..defaults import _handle_default -from ..viz import plot_raw, _RAW_CLIP_DEF -from ..time_frequency.spectrum import Spectrum, SpectrumMixin, _validate_method +from ..viz import _RAW_CLIP_DEF, plot_raw @fill_doc diff --git a/mne/io/besa/besa.py b/mne/io/besa/besa.py index dba8193d2bd..07fa1ee9eef 100644 --- a/mne/io/besa/besa.py +++ b/mne/io/besa/besa.py @@ -1,10 +1,11 @@ from collections import OrderedDict from pathlib import Path + import numpy as np -from ...utils import logger, fill_doc, verbose from ..._fiff.meas_info import create_info from ...evoked import EvokedArray +from ...utils import fill_doc, logger, verbose @fill_doc diff --git a/mne/io/besa/tests/test_besa.py b/mne/io/besa/tests/test_besa.py index fcaf32d651c..527ef86bff4 100644 --- a/mne/io/besa/tests/test_besa.py +++ b/mne/io/besa/tests/test_besa.py @@ -1,11 +1,11 @@ """Test reading BESA fileformats.""" import inspect -import pytest from pathlib import Path -from mne.io import read_evoked_besa -from mne.channels import read_custom_montage +import pytest +from mne.channels import read_custom_montage +from mne.io import read_evoked_besa FILE = Path(inspect.getfile(inspect.currentframe())) data_dir = FILE.parent / "data" diff --git a/mne/io/boxy/boxy.py b/mne/io/boxy/boxy.py index f2fa35f5ba2..9d4351df67f 100644 --- a/mne/io/boxy/boxy.py +++ b/mne/io/boxy/boxy.py @@ -6,11 +6,11 @@ import numpy as np -from ..base import BaseRaw from ..._fiff.meas_info import create_info from ..._fiff.utils import _mult_cal_one -from ...utils import logger, verbose, fill_doc, _check_fname from ...annotations import Annotations +from ...utils import _check_fname, fill_doc, logger, verbose +from ..base import BaseRaw @fill_doc diff --git a/mne/io/boxy/tests/test_boxy.py b/mne/io/boxy/tests/test_boxy.py index 0058075f107..e557980f2f8 100644 --- a/mne/io/boxy/tests/test_boxy.py +++ b/mne/io/boxy/tests/test_boxy.py @@ -2,10 +2,10 @@ # # License: BSD-3-Clause -import pytest import numpy as np -from numpy.testing import assert_allclose, assert_array_equal, assert_array_less +import pytest import scipy.io as spio +from numpy.testing import assert_allclose, assert_array_equal, assert_array_less from mne import pick_types from mne.datasets import testing diff --git a/mne/io/brainvision/brainvision.py b/mne/io/brainvision/brainvision.py index 444f3d2acef..2d75910ec4a 100644 --- a/mne/io/brainvision/brainvision.py +++ b/mne/io/brainvision/brainvision.py @@ -18,15 +18,15 @@ import numpy as np -from ..base import BaseRaw -from ...utils import verbose, logger, warn, fill_doc, _DefaultEventParser from ..._fiff.constants import FIFF from ..._fiff.meas_info import _empty_info -from ..._fiff.utils import _read_segments_file, _mult_cal_one +from ..._fiff.utils import _mult_cal_one, _read_segments_file from ...annotations import Annotations, read_annotations from ...channels import make_dig_montage from ...defaults import HEAD_SIZE_DEFAULT from ...transforms import _sph_to_cart +from ...utils import _DefaultEventParser, fill_doc, logger, verbose, warn +from ..base import BaseRaw @fill_doc diff --git a/mne/io/brainvision/tests/test_brainvision.py b/mne/io/brainvision/tests/test_brainvision.py index 5334941cb20..6aa222db1ec 100644 --- a/mne/io/brainvision/tests/test_brainvision.py +++ b/mne/io/brainvision/tests/test_brainvision.py @@ -4,22 +4,22 @@ # # License: BSD-3-Clause +import datetime import re import shutil from pathlib import Path import numpy as np -from numpy.testing import assert_array_equal, assert_allclose import pytest +from numpy.testing import assert_allclose, assert_array_equal -import datetime -from mne.utils import _stamp_to_dt, object_diff -from mne import pick_types, read_annotations, concatenate_raws +from mne import concatenate_raws, pick_types, read_annotations from mne._fiff.constants import FIFF -from mne.io import read_raw_fif, read_raw_brainvision -from mne.io.tests.test_raw import _test_raw_reader -from mne.datasets import testing from mne.annotations import events_from_annotations +from mne.datasets import testing +from mne.io import read_raw_brainvision, read_raw_fif +from mne.io.tests.test_raw import _test_raw_reader +from mne.utils import _stamp_to_dt, object_diff data_dir = Path(__file__).parent / "data" vhdr_path = data_dir / "test.vhdr" diff --git a/mne/io/bti/bti.py b/mne/io/bti/bti.py index a9a40b72a04..07e0d60d153 100644 --- a/mne/io/bti/bti.py +++ b/mne/io/bti/bti.py @@ -15,29 +15,29 @@ import numpy as np -from ..base import BaseRaw -from ..._fiff.tag import _coil_trans_to_loc, _loc_to_coil_trans -from ..._fiff.meas_info import _empty_info from ..._fiff._digitization import _make_bti_dig_points -from ...utils import logger, verbose, _stamp_to_dt, path_like, _validate_type -from ...transforms import combine_transforms, invert_transform, Transform from ..._fiff.constants import FIFF +from ..._fiff.meas_info import _empty_info +from ..._fiff.tag import _coil_trans_to_loc, _loc_to_coil_trans from ..._fiff.utils import _mult_cal_one, read_str +from ...transforms import Transform, combine_transforms, invert_transform +from ...utils import _stamp_to_dt, _validate_type, logger, path_like, verbose +from ..base import BaseRaw from .constants import BTI from .read import ( - read_int32, - read_int16, - read_float, - read_double, - read_transform, read_char, - read_int64, - read_uint16, - read_uint32, + read_dev_header, + read_double, read_double_matrix, + read_float, read_float_matrix, + read_int16, read_int16_matrix, - read_dev_header, + read_int32, + read_int64, + read_transform, + read_uint16, + read_uint32, ) FIFF_INFO_DIG_FIELDS = ("kind", "ident", "r", "coord_frame") diff --git a/mne/io/bti/tests/test_bti.py b/mne/io/bti/tests/test_bti.py index 7b15e3aa7a1..dfeb7ecab40 100644 --- a/mne/io/bti/tests/test_bti.py +++ b/mne/io/bti/tests/test_bti.py @@ -2,42 +2,42 @@ # # License: BSD-3-Clause +import os from collections import Counter -from functools import reduce, partial +from functools import partial, reduce from io import BytesIO -import os from pathlib import Path import numpy as np +import pytest from numpy.testing import ( + assert_allclose, assert_array_almost_equal, assert_array_equal, - assert_allclose, assert_equal, ) -import pytest import mne -from mne.datasets import testing -from mne.io import read_raw_fif, read_raw_bti +from mne import pick_info, pick_types from mne._fiff._digitization import _make_bti_dig_points +from mne._fiff.constants import FIFF +from mne.datasets import testing +from mne.io import read_raw_bti, read_raw_fif from mne.io.bti.bti import ( - _read_config, - _read_head_shape, - _read_bti_header, - _get_bti_dev_t, + _check_nan_dev_head_t, + _convert_coil_trans, _correct_trans, + _get_bti_dev_t, _get_bti_info, _loc_to_coil_trans, - _convert_coil_trans, - _check_nan_dev_head_t, + _read_bti_header, + _read_config, + _read_head_shape, _rename_channels, ) from mne.io.tests.test_raw import _test_raw_reader -from mne._fiff.constants import FIFF -from mne import pick_types, pick_info -from mne.utils import assert_dig_allclose from mne.transforms import Transform, combine_transforms, invert_transform +from mne.utils import assert_dig_allclose base_dir = Path(__file__).parent / "data" diff --git a/mne/io/cnt/_utils.py b/mne/io/cnt/_utils.py index dd13e688b8f..0cc5d277b48 100644 --- a/mne/io/cnt/_utils.py +++ b/mne/io/cnt/_utils.py @@ -2,13 +2,13 @@ # # License: BSD-3-Clause -from struct import Struct from collections import namedtuple -from math import modf from datetime import datetime +from math import modf from os import SEEK_END -import numpy as np +from struct import Struct +import numpy as np from ...utils import warn diff --git a/mne/io/cnt/cnt.py b/mne/io/cnt/cnt.py index 1a6aa15b7f3..249497fdf73 100644 --- a/mne/io/cnt/cnt.py +++ b/mne/io/cnt/cnt.py @@ -8,22 +8,20 @@ import numpy as np -from ..base import BaseRaw -from ...utils import warn, fill_doc, _check_option -from ...channels.layout import _topo_to_sphere -from ..._fiff.constants import FIFF from ..._fiff._digitization import _make_dig_points -from ..._fiff.utils import _mult_cal_one, _find_channels, _create_chs, read_str +from ..._fiff.constants import FIFF from ..._fiff.meas_info import _empty_info +from ..._fiff.utils import _create_chs, _find_channels, _mult_cal_one, read_str from ...annotations import Annotations - - +from ...channels.layout import _topo_to_sphere +from ...utils import _check_option, fill_doc, warn +from ..base import BaseRaw from ._utils import ( - _read_teeg, + CNTEventType3, + _compute_robust_event_table_position, _get_event_parser, + _read_teeg, _session_date_2_meas_date, - _compute_robust_event_table_position, - CNTEventType3, ) diff --git a/mne/io/cnt/tests/test_cnt.py b/mne/io/cnt/tests/test_cnt.py index 76cd5c0acc1..a5e5788eff3 100644 --- a/mne/io/cnt/tests/test_cnt.py +++ b/mne/io/cnt/tests/test_cnt.py @@ -4,14 +4,14 @@ # License: BSD-3-Clause import numpy as np -from numpy.testing import assert_array_equal import pytest +from numpy.testing import assert_array_equal from mne import pick_types +from mne.annotations import read_annotations from mne.datasets import testing -from mne.io.tests.test_raw import _test_raw_reader from mne.io.cnt import read_raw_cnt -from mne.annotations import read_annotations +from mne.io.tests.test_raw import _test_raw_reader data_path = testing.data_path(download=False) fname = data_path / "CNT" / "scan41_short.cnt" diff --git a/mne/io/ctf/ctf.py b/mne/io/ctf/ctf.py index 350ad757626..7f42da183d6 100644 --- a/mne/io/ctf/ctf.py +++ b/mne/io/ctf/ctf.py @@ -9,26 +9,24 @@ import numpy as np +from ..._fiff._digitization import _format_dig_points +from ..._fiff.utils import _blk_read_lims, _mult_cal_one from ...utils import ( - verbose, - logger, + _check_fname, + _check_option, _clean_names, fill_doc, - _check_option, - _check_fname, + logger, + verbose, ) - from ..base import BaseRaw -from ..._fiff.utils import _mult_cal_one, _blk_read_lims -from ..._fiff._digitization import _format_dig_points - -from .res4 import _read_res4, _make_ctf_name -from .hc import _read_hc -from .eeg import _read_eeg, _read_pos -from .trans import _make_ctf_coord_trans_set -from .info import _compose_meas_info, _read_bad_chans, _annotate_bad_segments from .constants import CTF +from .eeg import _read_eeg, _read_pos +from .hc import _read_hc +from .info import _annotate_bad_segments, _compose_meas_info, _read_bad_chans from .markers import _read_annotations_ctf_call +from .res4 import _make_ctf_name, _read_res4 +from .trans import _make_ctf_coord_trans_set @fill_doc diff --git a/mne/io/ctf/eeg.py b/mne/io/ctf/eeg.py index be827a469a9..c70fe8b626d 100644 --- a/mne/io/ctf/eeg.py +++ b/mne/io/ctf/eeg.py @@ -4,15 +4,15 @@ # # License: BSD-3-Clause -import numpy as np -from os.path import join from os import listdir +from os.path import join + +import numpy as np -from .res4 import _make_ctf_name -from ...utils import logger, warn from ..._fiff.constants import FIFF from ...transforms import apply_trans - +from ...utils import logger, warn +from .res4 import _make_ctf_name _cardinal_dict = dict( nasion=FIFF.FIFFV_POINT_NASION, diff --git a/mne/io/ctf/hc.py b/mne/io/ctf/hc.py index dc934aa297a..5bb94e4ec13 100644 --- a/mne/io/ctf/hc.py +++ b/mne/io/ctf/hc.py @@ -6,11 +6,10 @@ import numpy as np +from ..._fiff.constants import FIFF from ...utils import logger -from .res4 import _make_ctf_name from .constants import CTF -from ..._fiff.constants import FIFF - +from .res4 import _make_ctf_name _kind_dict = { "nasion": CTF.CTFV_COIL_NAS, diff --git a/mne/io/ctf/info.py b/mne/io/ctf/info.py index 66b7ad075e9..9e2b594dd9f 100644 --- a/mne/io/ctf/info.py +++ b/mne/io/ctf/info.py @@ -4,28 +4,26 @@ # # License: BSD-3-Clause -from time import strptime -from calendar import timegm import os.path as op +from calendar import timegm +from time import strptime import numpy as np -from ...utils import logger, warn, _clean_names +from ..._fiff.constants import FIFF +from ..._fiff.ctf_comp import _add_kind, _calibrate_comp +from ..._fiff.meas_info import _empty_info +from ..._fiff.write import get_new_file_id +from ...annotations import Annotations from ...transforms import ( - apply_trans, _coord_frame_name, - invert_transform, + apply_trans, combine_transforms, + invert_transform, ) -from ...annotations import Annotations -from ..._fiff.meas_info import _empty_info -from ..._fiff.write import get_new_file_id -from ..._fiff.ctf_comp import _add_kind, _calibrate_comp -from ..._fiff.constants import FIFF - +from ...utils import _clean_names, logger, warn from .constants import CTF - _ctf_to_fiff = { CTF.CTFV_COIL_LPA: FIFF.FIFFV_POINT_LPA, CTF.CTFV_COIL_RPA: FIFF.FIFFV_POINT_RPA, diff --git a/mne/io/ctf/markers.py b/mne/io/ctf/markers.py index 1d000c00aff..34d8c45b5a1 100644 --- a/mne/io/ctf/markers.py +++ b/mne/io/ctf/markers.py @@ -2,13 +2,14 @@ # # License: BSD-3-Clause -import numpy as np import os.path as op from io import BytesIO +import numpy as np + from ...annotations import Annotations -from .res4 import _read_res4 from .info import _convert_time +from .res4 import _read_res4 def _get_markers(fname): diff --git a/mne/io/ctf/tests/test_ctf.py b/mne/io/ctf/tests/test_ctf.py index 436ffdcf3f8..992f7f6ae37 100644 --- a/mne/io/ctf/tests/test_ctf.py +++ b/mne/io/ctf/tests/test_ctf.py @@ -3,36 +3,36 @@ # License: BSD-3-Clause import copy -from datetime import datetime, timezone import os -from os import path as op import shutil +from datetime import datetime, timezone +from os import path as op import numpy as np +import pytest from numpy import array_equal from numpy.testing import assert_allclose, assert_array_equal -import pytest import mne import mne.io.ctf.info from mne import ( - pick_types, - read_annotations, create_info, events_from_annotations, make_forward_solution, + pick_types, + read_annotations, ) -from mne.transforms import apply_trans -from mne.io import read_raw_fif, read_raw_ctf, RawArray from mne._fiff.compensator import get_current_comp +from mne._fiff.constants import FIFF from mne._fiff.pick import _picks_to_idx +from mne.datasets import brainstorm, spm_face, testing +from mne.io import RawArray, read_raw_ctf, read_raw_fif from mne.io.ctf.constants import CTF from mne.io.ctf.info import _convert_time from mne.io.tests.test_raw import _test_raw_reader from mne.tests.test_annotations import _assert_annotations_equal -from mne.utils import _clean_names, catch_logging, _stamp_to_dt, _record_warnings -from mne.datasets import testing, spm_face, brainstorm -from mne._fiff.constants import FIFF +from mne.transforms import apply_trans +from mne.utils import _clean_names, _record_warnings, _stamp_to_dt, catch_logging ctf_dir = testing.data_path(download=False) / "CTF" ctf_fname_continuous = "testdata_ctf.ds" diff --git a/mne/io/ctf/trans.py b/mne/io/ctf/trans.py index cc68116921e..3555dec7bff 100644 --- a/mne/io/ctf/trans.py +++ b/mne/io/ctf/trans.py @@ -6,18 +6,18 @@ import numpy as np -from .constants import CTF +from ..._fiff.constants import FIFF from ...transforms import ( - combine_transforms, - invert_transform, Transform, - _quat_to_affine, _fit_matched_points, + _quat_to_affine, apply_trans, + combine_transforms, get_ras_to_neuromag_trans, + invert_transform, ) from ...utils import logger -from ..._fiff.constants import FIFF +from .constants import CTF def _make_transform_card(fro, to, r_lpa, r_nasion, r_rpa): diff --git a/mne/io/curry/curry.py b/mne/io/curry/curry.py index 696d5969e10..2d1e342ea8b 100644 --- a/mne/io/curry/curry.py +++ b/mne/io/curry/curry.py @@ -5,33 +5,33 @@ # # License: BSD-3-Clause +import os.path as op +import re from collections import namedtuple from datetime import datetime, timezone -import os.path as op from pathlib import Path -import re import numpy as np -from ..base import BaseRaw -from ..ctf.trans import _quaternion_align from ..._fiff._digitization import _make_dig_points +from ..._fiff.constants import FIFF from ..._fiff.meas_info import create_info from ..._fiff.tag import _coil_trans_to_loc -from ..._fiff.utils import _read_segments_file, _mult_cal_one -from ..._fiff.constants import FIFF +from ..._fiff.utils import _mult_cal_one, _read_segments_file +from ...annotations import Annotations from ...surface import _normal_orth from ...transforms import ( - apply_trans, Transform, - get_ras_to_neuromag_trans, + _angle_between_quats, + apply_trans, combine_transforms, + get_ras_to_neuromag_trans, invert_transform, - _angle_between_quats, rot_to_quat, ) -from ...utils import check_fname, logger, verbose, _check_fname -from ...annotations import Annotations +from ...utils import _check_fname, check_fname, logger, verbose +from ..base import BaseRaw +from ..ctf.trans import _quaternion_align FILE_EXTENSIONS = { "Curry 7": { diff --git a/mne/io/curry/tests/test_curry.py b/mne/io/curry/tests/test_curry.py index 3ccb1aa035c..5e437101811 100644 --- a/mne/io/curry/tests/test_curry.py +++ b/mne/io/curry/tests/test_curry.py @@ -8,28 +8,27 @@ from pathlib import Path from shutil import copyfile -import pytest import numpy as np +import pytest from numpy.testing import assert_allclose, assert_array_equal -from mne.annotations import events_from_annotations +from mne._fiff.constants import FIFF +from mne._fiff.tag import _loc_to_coil_trans +from mne.annotations import events_from_annotations, read_annotations from mne.bem import _fit_sphere from mne.datasets import testing from mne.event import find_events -from mne._fiff.tag import _loc_to_coil_trans -from mne._fiff.constants import FIFF -from mne.io.edf import read_raw_bdf from mne.io.bti import read_raw_bti from mne.io.curry import read_raw_curry -from mne.io.tests.test_raw import _test_raw_reader -from mne.utils import catch_logging -from mne.annotations import read_annotations from mne.io.curry.curry import ( - _get_curry_version, + FILE_EXTENSIONS, _get_curry_file_structure, + _get_curry_version, _read_events_curry, - FILE_EXTENSIONS, ) +from mne.io.edf import read_raw_bdf +from mne.io.tests.test_raw import _test_raw_reader +from mne.utils import catch_logging data_dir = testing.data_path(download=False) curry_dir = data_dir / "curry" diff --git a/mne/io/edf/edf.py b/mne/io/edf/edf.py index a1dce08c049..8831989860c 100644 --- a/mne/io/edf/edf.py +++ b/mne/io/edf/edf.py @@ -10,21 +10,20 @@ # # License: BSD-3-Clause -from datetime import datetime, timezone, timedelta import os import re +from datetime import datetime, timedelta, timezone import numpy as np from scipy.interpolate import interp1d -from ..base import BaseRaw, _get_scaling -from ...utils import verbose, logger, warn, _validate_type -from ..._fiff.utils import _blk_read_lims, _mult_cal_one -from ..._fiff.meas_info import _empty_info, _unique_channel_names from ..._fiff.constants import FIFF -from ...filter import resample -from ...utils import fill_doc +from ..._fiff.meas_info import _empty_info, _unique_channel_names +from ..._fiff.utils import _blk_read_lims, _mult_cal_one from ...annotations import Annotations +from ...filter import resample +from ...utils import _validate_type, fill_doc, logger, verbose, warn +from ..base import BaseRaw, _get_scaling # common channel type names mapped to internal ch types CH_TYPE_MAPPING = { diff --git a/mne/io/edf/tests/test_edf.py b/mne/io/edf/tests/test_edf.py index e1c176c7e4c..6dec227800a 100644 --- a/mne/io/edf/tests/test_edf.py +++ b/mne/io/edf/tests/test_edf.py @@ -13,30 +13,29 @@ from pathlib import Path import numpy as np +import pytest from numpy.testing import ( + assert_allclose, assert_array_almost_equal, assert_array_equal, assert_equal, - assert_allclose, ) from scipy.io import loadmat -import pytest - -from mne import pick_types, Annotations +from mne import Annotations, pick_types +from mne._fiff.pick import channel_indices_by_type, get_channel_type_constants from mne.annotations import _ndarray_ch_names, events_from_annotations, read_annotations from mne.datasets import testing -from mne.io import read_raw_edf, read_raw_bdf, read_raw_fif, edf, read_raw_gdf -from mne.io.tests.test_raw import _test_raw_reader +from mne.io import edf, read_raw_bdf, read_raw_edf, read_raw_fif, read_raw_gdf from mne.io.edf.edf import ( + _edf_str, + _parse_prefilter_string, _read_annotations_edf, _read_ch, - _parse_prefilter_string, - _edf_str, _read_edf_header, _read_header, ) -from mne._fiff.pick import channel_indices_by_type, get_channel_type_constants +from mne.io.tests.test_raw import _test_raw_reader from mne.tests.test_annotations import _assert_annotations_equal td_mark = testing._pytest_mark() diff --git a/mne/io/edf/tests/test_gdf.py b/mne/io/edf/tests/test_gdf.py index 211230ca9b1..a851e372cee 100644 --- a/mne/io/edf/tests/test_gdf.py +++ b/mne/io/edf/tests/test_gdf.py @@ -3,18 +3,18 @@ # # License: BSD-3-Clause -from datetime import datetime, timezone, timedelta import shutil +from datetime import datetime, timedelta, timezone -import pytest -from numpy.testing import assert_array_almost_equal, assert_array_equal, assert_equal import numpy as np +import pytest import scipy.io as sio +from numpy.testing import assert_array_almost_equal, assert_array_equal, assert_equal +from mne import events_from_annotations, find_events, pick_types from mne.datasets import testing from mne.io import read_raw_gdf from mne.io.tests.test_raw import _test_raw_reader -from mne import pick_types, find_events, events_from_annotations data_path = testing.data_path(download=False) gdf1_path = data_path / "GDF" / "test_gdf_1.25" diff --git a/mne/io/eeglab/eeglab.py b/mne/io/eeglab/eeglab.py index 56a254b3a33..9e08807db49 100644 --- a/mne/io/eeglab/eeglab.py +++ b/mne/io/eeglab/eeglab.py @@ -9,29 +9,30 @@ from pathlib import Path import numpy as np + from mne.utils.check import _check_option -from ._eeglab import _readmat -from ..base import BaseRaw -from ...event import read_events from ..._fiff._digitization import _ensure_fiducials_head from ..._fiff.constants import FIFF from ..._fiff.meas_info import create_info from ..._fiff.pick import _PICK_TYPES_KEYS -from ..._fiff.utils import _read_segments_file, _find_channels +from ..._fiff.utils import _find_channels, _read_segments_file +from ...annotations import Annotations, read_annotations +from ...channels import make_dig_montage from ...defaults import DEFAULTS +from ...epochs import BaseEpochs +from ...event import read_events from ...utils import ( - logger, - verbose, - warn, - fill_doc, Bunch, _check_fname, _check_head_radius, + fill_doc, + logger, + verbose, + warn, ) -from ...channels import make_dig_montage -from ...epochs import BaseEpochs -from ...annotations import Annotations, read_annotations +from ..base import BaseRaw +from ._eeglab import _readmat # just fix the scaling for now, EEGLAB doesn't seem to provide this info CAL = 1e-6 diff --git a/mne/io/eeglab/tests/test_eeglab.py b/mne/io/eeglab/tests/test_eeglab.py index d441fbcfc14..c8519eb335d 100644 --- a/mne/io/eeglab/tests/test_eeglab.py +++ b/mne/io/eeglab/tests/test_eeglab.py @@ -9,25 +9,25 @@ from copy import deepcopy import numpy as np +import pytest from numpy.testing import ( - assert_array_equal, + assert_allclose, assert_array_almost_equal, + assert_array_equal, assert_equal, - assert_allclose, ) -import pytest from scipy import io import mne -from mne import write_events, read_epochs_eeglab +from mne import read_epochs_eeglab, write_events +from mne.annotations import events_from_annotations, read_annotations from mne.channels import read_custom_montage +from mne.datasets import testing from mne.io import read_raw_eeglab -from mne.io.eeglab.eeglab import _get_montage_information, _dol_to_lod from mne.io.eeglab._eeglab import _readmat +from mne.io.eeglab.eeglab import _dol_to_lod, _get_montage_information from mne.io.tests.test_raw import _test_raw_reader -from mne.datasets import testing from mne.utils import Bunch, _check_pymatreader_installed -from mne.annotations import events_from_annotations, read_annotations base_dir = testing.data_path(download=False) / "EEGLAB" raw_fname_mat = base_dir / "test_raw.set" diff --git a/mne/io/egi/egi.py b/mne/io/egi/egi.py index 8b7408d74bb..d6ba4b884f6 100644 --- a/mne/io/egi/egi.py +++ b/mne/io/egi/egi.py @@ -8,13 +8,13 @@ import numpy as np +from ..._fiff.constants import FIFF +from ..._fiff.meas_info import _empty_info +from ..._fiff.utils import _create_chs, _read_segments_file +from ...utils import _check_fname, _validate_type, logger, verbose, warn +from ..base import BaseRaw from .egimff import _read_raw_egi_mff from .events import _combine_triggers -from ..base import BaseRaw -from ..._fiff.utils import _create_chs, _read_segments_file -from ..._fiff.meas_info import _empty_info -from ..._fiff.constants import FIFF -from ...utils import verbose, logger, warn, _validate_type, _check_fname def _read_header(fid): diff --git a/mne/io/egi/egimff.py b/mne/io/egi/egimff.py index 5375cfadf60..b584acf9abd 100644 --- a/mne/io/egi/egimff.py +++ b/mne/io/egi/egimff.py @@ -1,33 +1,33 @@ """EGI NetStation Load Function.""" -from collections import OrderedDict import datetime import math import os.path as op import re -from defusedxml.minidom import parse +from collections import OrderedDict from pathlib import Path import numpy as np +from defusedxml.minidom import parse -from .events import _read_events, _combine_triggers -from .general import ( - _get_signalfname, - _get_ep_info, - _extract, - _get_blocks, - _get_gains, - _block_r, -) -from ..base import BaseRaw from ..._fiff.constants import FIFF -from ..._fiff.meas_info import _empty_info, create_info, _ensure_meas_date_none_or_dt +from ..._fiff.meas_info import _empty_info, _ensure_meas_date_none_or_dt, create_info from ..._fiff.proj import setup_proj from ..._fiff.utils import _create_chs, _mult_cal_one from ...annotations import Annotations from ...channels.montage import make_dig_montage -from ...utils import verbose, logger, warn, _check_option, _check_fname from ...evoked import EvokedArray +from ...utils import _check_fname, _check_option, logger, verbose, warn +from ..base import BaseRaw +from .events import _combine_triggers, _read_events +from .general import ( + _block_r, + _extract, + _get_blocks, + _get_ep_info, + _get_gains, + _get_signalfname, +) REFERENCE_NAMES = ("VREF", "Vertex Reference") diff --git a/mne/io/egi/events.py b/mne/io/egi/events.py index 9e5088d7617..8e035d42681 100644 --- a/mne/io/egi/events.py +++ b/mne/io/egi/events.py @@ -4,9 +4,9 @@ from datetime import datetime from glob import glob from os.path import basename, join, splitext -from defusedxml.ElementTree import parse import numpy as np +from defusedxml.ElementTree import parse from ...utils import logger diff --git a/mne/io/egi/general.py b/mne/io/egi/general.py index a82f087b59a..ed20fce0fbd 100644 --- a/mne/io/egi/general.py +++ b/mne/io/egi/general.py @@ -2,10 +2,10 @@ # License: BSD-3-Clause import os -from defusedxml.minidom import parse import re import numpy as np +from defusedxml.minidom import parse from ...utils import _pl diff --git a/mne/io/egi/tests/test_egi.py b/mne/io/egi/tests/test_egi.py index 7ed2743249d..7772d38f2d3 100644 --- a/mne/io/egi/tests/test_egi.py +++ b/mne/io/egi/tests/test_egi.py @@ -2,24 +2,24 @@ # simplified BSD-3 license -from copy import deepcopy -from pathlib import Path import os import shutil +from copy import deepcopy from datetime import datetime, timezone +from pathlib import Path import numpy as np -from numpy.testing import assert_array_equal, assert_allclose import pytest +from numpy.testing import assert_allclose, assert_array_equal from scipy import io as sio from mne import find_events, pick_types -from mne.io import read_raw_egi, read_evokeds_mff, read_raw_fif from mne._fiff.constants import FIFF +from mne.datasets.testing import data_path, requires_testing_data +from mne.io import read_evokeds_mff, read_raw_egi, read_raw_fif from mne.io.egi.egi import _combine_triggers from mne.io.tests.test_raw import _test_raw_reader from mne.utils import object_diff -from mne.datasets.testing import data_path, requires_testing_data base_dir = Path(__file__).parent / "data" egi_fname = base_dir / "test_egi.raw" diff --git a/mne/io/eximia/eximia.py b/mne/io/eximia/eximia.py index 03ab01eff04..2f13049fa2c 100644 --- a/mne/io/eximia/eximia.py +++ b/mne/io/eximia/eximia.py @@ -5,10 +5,10 @@ import os.path as op -from ..base import BaseRaw -from ..._fiff.utils import _read_segments_file, _file_size from ..._fiff.meas_info import create_info -from ...utils import logger, verbose, warn, fill_doc, _check_fname +from ..._fiff.utils import _file_size, _read_segments_file +from ...utils import _check_fname, fill_doc, logger, verbose, warn +from ..base import BaseRaw @fill_doc diff --git a/mne/io/eximia/tests/test_eximia.py b/mne/io/eximia/tests/test_eximia.py index 59a0ba8c784..d42e6b174a5 100644 --- a/mne/io/eximia/tests/test_eximia.py +++ b/mne/io/eximia/tests/test_eximia.py @@ -4,9 +4,9 @@ from numpy.testing import assert_array_equal from scipy import io as sio +from mne.datasets.testing import data_path, requires_testing_data from mne.io import read_raw_eximia from mne.io.tests.test_raw import _test_raw_reader -from mne.datasets.testing import data_path, requires_testing_data testing_path = data_path(download=False) diff --git a/mne/io/eyelink/_utils.py b/mne/io/eyelink/_utils.py index 23c9cb38329..65641e9cc43 100644 --- a/mne/io/eyelink/_utils.py +++ b/mne/io/eyelink/_utils.py @@ -3,8 +3,8 @@ # License: BSD-3-Clause -from datetime import datetime, timezone, timedelta import re +from datetime import datetime, timedelta, timezone import numpy as np diff --git a/mne/io/eyelink/eyelink.py b/mne/io/eyelink/eyelink.py index 501b8ad798b..9a465d76ce9 100644 --- a/mne/io/eyelink/eyelink.py +++ b/mne/io/eyelink/eyelink.py @@ -8,14 +8,14 @@ from pathlib import Path -from ._utils import _parse_eyelink_ascii, _make_eyelink_annots, _make_gap_annots -from ..base import BaseRaw from ...utils import ( _check_fname, fill_doc, logger, verbose, ) +from ..base import BaseRaw +from ._utils import _make_eyelink_annots, _make_gap_annots, _parse_eyelink_ascii @fill_doc diff --git a/mne/io/eyelink/tests/test_eyelink.py b/mne/io/eyelink/tests/test_eyelink.py index c482826dde8..a578bce25a5 100644 --- a/mne/io/eyelink/tests/test_eyelink.py +++ b/mne/io/eyelink/tests/test_eyelink.py @@ -1,16 +1,15 @@ from pathlib import Path -import pytest - import numpy as np +import pytest from numpy.testing import assert_allclose +from mne._fiff.constants import FIFF +from mne._fiff.pick import _DATA_CH_TYPES_SPLIT from mne.datasets.testing import data_path, requires_testing_data from mne.io import read_raw_eyelink -from mne.io.tests.test_raw import _test_raw_reader -from mne._fiff.constants import FIFF from mne.io.eyelink._utils import _adjust_times, _find_overlaps -from mne._fiff.pick import _DATA_CH_TYPES_SPLIT +from mne.io.tests.test_raw import _test_raw_reader pd = pytest.importorskip("pandas") diff --git a/mne/io/fieldtrip/fieldtrip.py b/mne/io/fieldtrip/fieldtrip.py index e5678522432..a3d13512146 100644 --- a/mne/io/fieldtrip/fieldtrip.py +++ b/mne/io/fieldtrip/fieldtrip.py @@ -6,17 +6,17 @@ import numpy as np +from ...epochs import EpochsArray +from ...evoked import EvokedArray +from ...utils import _check_fname, _import_pymatreader_funcs +from ..array.array import RawArray from .utils import ( + _create_event_metadata, + _create_events, _create_info, _set_tmin, - _create_events, - _create_event_metadata, _validate_ft_struct, ) -from ..array.array import RawArray -from ...utils import _check_fname, _import_pymatreader_funcs -from ...epochs import EpochsArray -from ...evoked import EvokedArray def read_raw_fieldtrip(fname, info, data_name="data"): diff --git a/mne/io/fieldtrip/tests/helpers.py b/mne/io/fieldtrip/tests/helpers.py index d93e6ad126e..cf0102a8139 100644 --- a/mne/io/fieldtrip/tests/helpers.py +++ b/mne/io/fieldtrip/tests/helpers.py @@ -3,15 +3,14 @@ # Dirk Gütlin # # License: BSD-3-Clause -from functools import partial import os +from functools import partial import numpy as np import mne from mne.utils import object_diff - info_ignored_fields = ( "file_id", "hpi_results", diff --git a/mne/io/fieldtrip/tests/test_fieldtrip.py b/mne/io/fieldtrip/tests/test_fieldtrip.py index 948edf6d7a3..37b5bb2cd41 100644 --- a/mne/io/fieldtrip/tests/test_fieldtrip.py +++ b/mne/io/fieldtrip/tests/test_fieldtrip.py @@ -8,24 +8,24 @@ import itertools from contextlib import nullcontext -import pytest import numpy as np +import pytest import mne from mne.datasets import testing from mne.io import read_raw_fieldtrip -from mne.io.fieldtrip.utils import NOINFO_WARNING, _create_events from mne.io.fieldtrip.tests.helpers import ( + assert_warning_in_record, + check_data, check_info_fields, get_data_paths, - get_raw_data, get_epochs, get_evoked, - pandas_not_found_warning_msg, + get_raw_data, get_raw_info, - check_data, - assert_warning_in_record, + pandas_not_found_warning_msg, ) +from mne.io.fieldtrip.utils import NOINFO_WARNING, _create_events from mne.io.tests.test_raw import _test_raw_reader from mne.utils import _check_pandas_installed, _record_warnings diff --git a/mne/io/fieldtrip/utils.py b/mne/io/fieldtrip/utils.py index a32fd680ead..ae975dc309c 100644 --- a/mne/io/fieldtrip/utils.py +++ b/mne/io/fieldtrip/utils.py @@ -10,7 +10,7 @@ from ..._fiff.meas_info import create_info from ..._fiff.pick import pick_info from ...transforms import rotation3d_align_z_axis -from ...utils import warn, _check_pandas_installed +from ...utils import _check_pandas_installed, warn _supported_megs = ["neuromag306"] diff --git a/mne/io/fiff/raw.py b/mne/io/fiff/raw.py index ea8aaa33d59..eab274ba108 100644 --- a/mne/io/fiff/raw.py +++ b/mne/io/fiff/raw.py @@ -12,33 +12,31 @@ import numpy as np -from ...channels import fix_mag_coil_types -from ...event import AcqParserFIF from ..._fiff.constants import FIFF -from ..._fiff.open import fiff_open, _fiff_get_fid, _get_next_fname from ..._fiff.meas_info import read_meas_info -from ..._fiff.tree import dir_tree_find +from ..._fiff.open import _fiff_get_fid, _get_next_fname, fiff_open from ..._fiff.tag import read_tag, read_tag_info -from ..base import ( - BaseRaw, - _RawShell, - _check_raw_compatibility, - _check_maxshield, - _get_fname_rep, -) +from ..._fiff.tree import dir_tree_find from ..._fiff.utils import _mult_cal_one - from ...annotations import Annotations, _read_annotations_fif - +from ...channels import fix_mag_coil_types +from ...event import AcqParserFIF from ...utils import ( + _check_fname, + _file_like, + _on_missing, check_fname, + fill_doc, logger, verbose, warn, - fill_doc, - _file_like, - _on_missing, - _check_fname, +) +from ..base import ( + BaseRaw, + _check_maxshield, + _check_raw_compatibility, + _get_fname_rep, + _RawShell, ) diff --git a/mne/io/fiff/tests/test_raw_fiff.py b/mne/io/fiff/tests/test_raw_fiff.py index 9b24930fb69..ed94c4c527a 100644 --- a/mne/io/fiff/tests/test_raw_fiff.py +++ b/mne/io/fiff/tests/test_raw_fiff.py @@ -3,47 +3,47 @@ # # License: BSD-3-Clause -from copy import deepcopy -from pathlib import Path -from functools import partial -from io import BytesIO import os import pathlib import pickle import shutil import sys +from copy import deepcopy +from functools import partial +from io import BytesIO +from pathlib import Path import numpy as np -from numpy.testing import assert_array_almost_equal, assert_array_equal, assert_allclose import pytest +from numpy.testing import assert_allclose, assert_array_almost_equal, assert_array_equal -from mne.datasets import testing -from mne.filter import filter_data -from mne._fiff.constants import FIFF -from mne.io import RawArray, concatenate_raws, read_raw_fif, match_channel_orders, base -from mne._fiff.open import read_tag, read_tag_info -from mne._fiff.tag import _read_tag_header -from mne.io.tests.test_raw import _test_concat, _test_raw_reader from mne import ( + compute_proj_raw, concatenate_events, - find_events, + create_info, equalize_channels, - compute_proj_raw, - pick_types, + find_events, + make_fixed_length_epochs, pick_channels, - create_info, pick_info, - make_fixed_length_epochs, + pick_types, ) +from mne._fiff.constants import FIFF +from mne._fiff.open import read_tag, read_tag_info +from mne._fiff.tag import _read_tag_header +from mne.annotations import Annotations +from mne.datasets import testing +from mne.filter import filter_data +from mne.io import RawArray, base, concatenate_raws, match_channel_orders, read_raw_fif +from mne.io.tests.test_raw import _test_concat, _test_raw_reader from mne.utils import ( - assert_object_equal, _dt_to_stamp, - requires_mne, - run_subprocess, _record_warnings, assert_and_remove_boundary_annot, + assert_object_equal, + requires_mne, + run_subprocess, ) -from mne.annotations import Annotations testing_path = testing.data_path(download=False) data_dir = testing_path / "MEG" / "sample" diff --git a/mne/io/fil/fil.py b/mne/io/fil/fil.py index 616c8e9fc1b..fc6472d0043 100644 --- a/mne/io/fil/fil.py +++ b/mne/io/fil/fil.py @@ -2,25 +2,24 @@ # # License: BSD-3-Clause -import pathlib import json +import pathlib import numpy as np -from ..base import BaseRaw +from ..._fiff._digitization import _make_dig_points from ..._fiff.constants import FIFF from ..._fiff.meas_info import _empty_info -from ..._fiff.write import get_new_file_id from ..._fiff.utils import _read_segments_file -from ..._fiff._digitization import _make_dig_points -from ...transforms import get_ras_to_neuromag_trans, apply_trans, Transform -from ...utils import warn, fill_doc, verbose, _check_fname - +from ..._fiff.write import get_new_file_id +from ...transforms import Transform, apply_trans, get_ras_to_neuromag_trans +from ...utils import _check_fname, fill_doc, verbose, warn +from ..base import BaseRaw from .sensors import ( - _refine_sensor_orientation, + _get_plane_vectors, _get_pos_units, + _refine_sensor_orientation, _size2units, - _get_plane_vectors, ) diff --git a/mne/io/fil/sensors.py b/mne/io/fil/sensors.py index ab94e65ccbc..3e251202fcf 100644 --- a/mne/io/fil/sensors.py +++ b/mne/io/fil/sensors.py @@ -3,6 +3,7 @@ # License: BSD-3-Clause from copy import deepcopy + import numpy as np from ...utils import logger diff --git a/mne/io/fil/tests/test_fil.py b/mne/io/fil/tests/test_fil.py index 9f9570f5adb..a88c4fe4d12 100644 --- a/mne/io/fil/tests/test_fil.py +++ b/mne/io/fil/tests/test_fil.py @@ -2,22 +2,19 @@ # # License: BSD-3-Clause -from numpy import isnan, empty, array -from numpy.testing import assert_array_equal, assert_array_almost_equal +import shutil from os import remove import pytest -import shutil - +import scipy.io +from numpy import array, empty, isnan +from numpy.testing import assert_array_almost_equal, assert_array_equal from mne import pick_types from mne.datasets import testing from mne.io import read_raw_fil from mne.io.fil.sensors import _get_pos_units -import scipy.io - - fil_path = testing.data_path(download=False) / "FIL" diff --git a/mne/io/hitachi/hitachi.py b/mne/io/hitachi/hitachi.py index 9af5a1354c3..12219dacd8b 100644 --- a/mne/io/hitachi/hitachi.py +++ b/mne/io/hitachi/hitachi.py @@ -7,12 +7,12 @@ import numpy as np -from ..base import BaseRaw -from ..nirx.nirx import _read_csv_rows_cols from ..._fiff.constants import FIFF -from ..._fiff.meas_info import create_info, _merge_info +from ..._fiff.meas_info import _merge_info, create_info from ..._fiff.utils import _mult_cal_one -from ...utils import logger, verbose, fill_doc, warn, _check_fname, _check_option +from ...utils import _check_fname, _check_option, fill_doc, logger, verbose, warn +from ..base import BaseRaw +from ..nirx.nirx import _read_csv_rows_cols @fill_doc diff --git a/mne/io/hitachi/tests/test_hitachi.py b/mne/io/hitachi/tests/test_hitachi.py index 89b94fd7a06..72fa846c6fb 100644 --- a/mne/io/hitachi/tests/test_hitachi.py +++ b/mne/io/hitachi/tests/test_hitachi.py @@ -4,8 +4,8 @@ import datetime as dt -import pytest import numpy as np +import pytest from numpy.testing import assert_allclose, assert_array_less from mne.channels import make_standard_montage @@ -13,14 +13,13 @@ from mne.io.hitachi.hitachi import _compute_pairs from mne.io.tests.test_raw import _test_raw_reader from mne.preprocessing.nirs import ( - source_detector_distances, - optical_density, - tddr, beer_lambert_law, + optical_density, scalp_coupling_index, + source_detector_distances, + tddr, ) - CONTENTS = dict() CONTENTS[ "1.18" diff --git a/mne/io/kit/coreg.py b/mne/io/kit/coreg.py index 881e63fb87a..209728dbaab 100644 --- a/mne/io/kit/coreg.py +++ b/mne/io/kit/coreg.py @@ -12,22 +12,21 @@ import numpy as np -from .constants import KIT, FIFF +from ..._fiff._digitization import _make_dig_points from ...channels.montage import ( - read_polhemus_fastscan, - read_dig_polhemus_isotrak, - read_custom_montage, _check_dig_shape, + read_custom_montage, + read_dig_polhemus_isotrak, + read_polhemus_fastscan, ) -from ..._fiff._digitization import _make_dig_points from ...transforms import ( Transform, + als_ras_trans, apply_trans, get_ras_to_neuromag_trans, - als_ras_trans, ) -from ...utils import warn, _check_option, _check_fname - +from ...utils import _check_fname, _check_option, warn +from .constants import FIFF, KIT INT32 = " # simplified BSD-3 license -import shutil -import os import datetime as dt -import numpy as np +import os +import shutil +import numpy as np import pytest from numpy.testing import assert_allclose, assert_array_equal from mne import pick_types +from mne._fiff.constants import FIFF from mne.datasets.testing import data_path, requires_testing_data from mne.io import read_raw_nirx from mne.io.tests.test_raw import _test_raw_reader from mne.preprocessing import annotate_nan -from mne.transforms import apply_trans, _get_trans from mne.preprocessing.nirs import ( - source_detector_distances, - short_channels, _reorder_nirx, + short_channels, + source_detector_distances, ) -from mne._fiff.constants import FIFF +from mne.transforms import _get_trans, apply_trans testing_path = data_path(download=False) fname_nirx_15_0 = testing_path / "NIRx" / "nirscout" / "nirx_15_0_recording" diff --git a/mne/io/nsx/nsx.py b/mne/io/nsx/nsx.py index 08cd5bc29a6..5d3b2e7a659 100644 --- a/mne/io/nsx/nsx.py +++ b/mne/io/nsx/nsx.py @@ -6,14 +6,12 @@ import numpy as np -from ..base import BaseRaw, _get_scaling from ..._fiff.constants import FIFF from ..._fiff.meas_info import _empty_info -from ..._fiff.utils import _read_segments_file, _file_size - +from ..._fiff.utils import _file_size, _read_segments_file from ...annotations import Annotations -from ...utils import logger, fill_doc, warn - +from ...utils import fill_doc, logger, warn +from ..base import BaseRaw, _get_scaling CH_TYPE_MAPPING = { "CC": "SEEG", diff --git a/mne/io/nsx/tests/test_nsx.py b/mne/io/nsx/tests/test_nsx.py index 8aa22677552..03b6ebb8606 100644 --- a/mne/io/nsx/tests/test_nsx.py +++ b/mne/io/nsx/tests/test_nsx.py @@ -2,19 +2,18 @@ # # License: BSD-3-Clause import os + import numpy as np import pytest - from numpy.testing import assert_allclose -from mne.io import read_raw_nsx -from mne.io.nsx.nsx import _decode_online_filters, _read_header -from mne._fiff.meas_info import _empty_info +from mne import make_fixed_length_epochs from mne._fiff.constants import FIFF +from mne._fiff.meas_info import _empty_info from mne.datasets.testing import data_path, requires_testing_data +from mne.io import read_raw_nsx +from mne.io.nsx.nsx import _decode_online_filters, _read_header from mne.io.tests.test_raw import _test_raw_reader -from mne import make_fixed_length_epochs - testing_path = data_path(download=False) nsx_21_fname = os.path.join(testing_path, "nsx", "test_NEURALSG_raw.ns3") diff --git a/mne/io/persyst/persyst.py b/mne/io/persyst/persyst.py index 3030c1ad1d4..873131ffad3 100644 --- a/mne/io/persyst/persyst.py +++ b/mne/io/persyst/persyst.py @@ -8,12 +8,12 @@ import numpy as np -from ..base import BaseRaw from ..._fiff.constants import FIFF from ..._fiff.meas_info import create_info from ..._fiff.utils import _mult_cal_one from ...annotations import Annotations -from ...utils import logger, verbose, fill_doc, warn, _check_fname +from ...utils import _check_fname, fill_doc, logger, verbose, warn +from ..base import BaseRaw @fill_doc diff --git a/mne/io/persyst/tests/test_persyst.py b/mne/io/persyst/tests/test_persyst.py index 88c4c3e2770..ac527157834 100644 --- a/mne/io/persyst/tests/test_persyst.py +++ b/mne/io/persyst/tests/test_persyst.py @@ -5,9 +5,9 @@ import os import shutil +import numpy as np import pytest from numpy.testing import assert_array_equal -import numpy as np from mne.datasets.testing import data_path, requires_testing_data from mne.io import read_raw_persyst diff --git a/mne/io/pick.py b/mne/io/pick.py index 5ae287de324..7b9b38b8b2d 100644 --- a/mne/io/pick.py +++ b/mne/io/pick.py @@ -5,9 +5,9 @@ from .._fiff import _io_dep_getattr from .._fiff.pick import ( - _picks_to_idx, _DATA_CH_TYPES_ORDER_DEFAULT, _DATA_CH_TYPES_SPLIT, + _picks_to_idx, ) __all__ = [ diff --git a/mne/io/snirf/_snirf.py b/mne/io/snirf/_snirf.py index 5da8642301f..a0de3550c88 100644 --- a/mne/io/snirf/_snirf.py +++ b/mne/io/snirf/_snirf.py @@ -2,20 +2,21 @@ # # License: BSD-3-Clause +import datetime import re + import numpy as np -import datetime -from ..base import BaseRaw -from ..nirx.nirx import _convert_fnirs_to_head -from ..._fiff.meas_info import create_info, _format_dig_points +from ..._fiff._digitization import _make_dig_points +from ..._fiff.constants import FIFF +from ..._fiff.meas_info import _format_dig_points, create_info from ..._fiff.utils import _mult_cal_one +from ..._freesurfer import get_mni_fiducials from ...annotations import Annotations -from ...utils import logger, verbose, fill_doc, warn, _check_fname, _import_h5py -from ..._fiff.constants import FIFF -from ..._fiff._digitization import _make_dig_points from ...transforms import _frame_to_str, apply_trans -from ..._freesurfer import get_mni_fiducials +from ...utils import _check_fname, _import_h5py, fill_doc, logger, verbose, warn +from ..base import BaseRaw +from ..nirx.nirx import _convert_fnirs_to_head @fill_doc diff --git a/mne/io/snirf/tests/test_snirf.py b/mne/io/snirf/tests/test_snirf.py index 141e34d00c6..276d003b791 100644 --- a/mne/io/snirf/tests/test_snirf.py +++ b/mne/io/snirf/tests/test_snirf.py @@ -1,26 +1,26 @@ # Authors: Robert Luke # simplified BSD-3 license -import numpy as np -from numpy.testing import assert_allclose, assert_almost_equal, assert_equal import shutil + +import numpy as np import pytest +from numpy.testing import assert_allclose, assert_almost_equal, assert_equal +from mne._fiff.constants import FIFF from mne.datasets.testing import data_path, requires_testing_data -from mne.io import read_raw_snirf, read_raw_nirx +from mne.io import read_raw_nirx, read_raw_snirf from mne.io.tests.test_raw import _test_raw_reader from mne.preprocessing.nirs import ( - optical_density, + _reorder_nirx, beer_lambert_law, + optical_density, short_channels, source_detector_distances, - _reorder_nirx, ) -from mne.transforms import apply_trans, _get_trans -from mne._fiff.constants import FIFF +from mne.transforms import _get_trans, apply_trans from mne.utils import catch_logging - testing_path = data_path(download=False) # SfNIRS files sfnirs_homer_103_wShort = ( diff --git a/mne/io/tests/test_apply_function.py b/mne/io/tests/test_apply_function.py index 920dd404dc6..94388ccf86e 100644 --- a/mne/io/tests/test_apply_function.py +++ b/mne/io/tests/test_apply_function.py @@ -7,7 +7,7 @@ from mne import create_info from mne.io import RawArray -from mne.utils import logger, catch_logging +from mne.utils import catch_logging, logger def bad_1(x): diff --git a/mne/io/tests/test_raw.py b/mne/io/tests/test_raw.py index 44640427108..362fb293fdf 100644 --- a/mne/io/tests/test_raw.py +++ b/mne/io/tests/test_raw.py @@ -4,16 +4,16 @@ # # License: BSD-3-Clause -from contextlib import redirect_stdout -from io import StringIO import math import os +import re +from contextlib import redirect_stdout +from io import StringIO from os import path as op from pathlib import Path -import re -import pytest import numpy as np +import pytest from numpy.testing import ( assert_allclose, assert_array_almost_equal, @@ -22,26 +22,26 @@ ) import mne -from mne import concatenate_raws, create_info, Annotations, pick_types +from mne import Annotations, concatenate_raws, create_info, pick_types +from mne._fiff._digitization import DigPoint, _dig_kind_dict +from mne._fiff.constants import FIFF +from mne._fiff.meas_info import Info, _get_valid_units, _writing_info_hdf5 +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.datasets import testing from mne.fixes import _numpy_h5py_dep -from mne.io import read_raw_fif, RawArray, BaseRaw +from mne.io import BaseRaw, RawArray, read_raw_fif from mne.io.base import _get_scaling -from mne._fiff.meas_info import Info, _writing_info_hdf5, _get_valid_units -from mne._fiff._digitization import _dig_kind_dict, DigPoint -from mne._fiff.pick import _ELECTRODE_CH_TYPES, _FNIRS_CH_TYPES_SPLIT from mne.utils import ( - _TempDir, - catch_logging, + _import_h5io_funcs, _raw_annot, _stamp_to_dt, - object_diff, + _TempDir, + catch_logging, check_version, - _import_h5io_funcs, + object_diff, ) -from mne._fiff.proj import Projection -from mne._fiff.utils import _mult_cal_one -from mne._fiff.constants import FIFF raw_fname = op.join( op.dirname(__file__), "..", "..", "io", "tests", "data", "test_raw.fif" diff --git a/mne/io/tests/test_read_raw.py b/mne/io/tests/test_read_raw.py index db9244dee5a..4f44b9c7473 100644 --- a/mne/io/tests/test_read_raw.py +++ b/mne/io/tests/test_read_raw.py @@ -11,8 +11,7 @@ from mne.datasets import testing from mne.io import read_raw -from mne.io._read_raw import split_name_ext, _get_readers - +from mne.io._read_raw import _get_readers, split_name_ext base = Path(__file__).parent.parent test_base = Path(testing.data_path(download=False)) diff --git a/mne/label.py b/mne/label.py index 19eb21ffdc7..58db1379068 100644 --- a/mne/label.py +++ b/mne/label.py @@ -4,12 +4,12 @@ # # License: BSD-3-Clause -from collections import defaultdict -from colorsys import hsv_to_rgb, rgb_to_hsv import copy as cp import os import os.path as op import re +from collections import defaultdict +from colorsys import hsv_to_rgb, rgb_to_hsv import numpy as np from scipy import linalg, sparse @@ -25,30 +25,30 @@ spatial_src_adjacency, ) from .source_space._source_space import ( - add_source_space_distances, SourceSpaces, _ensure_src, + add_source_space_distances, ) from .stats.cluster_level import _find_clusters, _get_components from .surface import ( + _mesh_borders, complete_surface_info, - read_surface, fast_cross_3d, - _mesh_borders, - mesh_edges, mesh_dist, + mesh_edges, + read_surface, ) from .utils import ( - get_subjects_dir, + _check_fname, + _check_option, _check_subject, + _validate_type, + check_random_state, + fill_doc, + get_subjects_dir, logger, verbose, warn, - check_random_state, - _validate_type, - fill_doc, - _check_option, - _check_fname, ) diff --git a/mne/minimum_norm/__init__.pyi b/mne/minimum_norm/__init__.pyi index bf8e3fc90ca..fb800479383 100644 --- a/mne/minimum_norm/__init__.pyi +++ b/mne/minimum_norm/__init__.pyi @@ -22,29 +22,29 @@ __all__ = [ "write_inverse_operator", ] from .inverse import ( + INVERSE_METHODS, InverseOperator, - read_inverse_operator, apply_inverse, - apply_inverse_raw, - make_inverse_operator, + apply_inverse_cov, apply_inverse_epochs, + apply_inverse_raw, apply_inverse_tfr_epochs, - write_inverse_operator, compute_rank_inverse, - prepare_inverse_operator, estimate_snr, - apply_inverse_cov, - INVERSE_METHODS, -) -from .time_frequency import ( - source_band_induced_power, - source_induced_power, - compute_source_psd, - compute_source_psd_epochs, + make_inverse_operator, + prepare_inverse_operator, + read_inverse_operator, + write_inverse_operator, ) from .resolution_matrix import ( - make_inverse_resolution_matrix, - get_point_spread, get_cross_talk, + get_point_spread, + make_inverse_resolution_matrix, ) from .spatial_resolution import resolution_metrics +from .time_frequency import ( + compute_source_psd, + compute_source_psd_epochs, + source_band_induced_power, + source_induced_power, +) diff --git a/mne/minimum_norm/_eloreta.py b/mne/minimum_norm/_eloreta.py index ec534503f79..0e497536825 100644 --- a/mne/minimum_norm/_eloreta.py +++ b/mne/minimum_norm/_eloreta.py @@ -3,12 +3,12 @@ # License: BSD-3-Clause from functools import partial + import numpy as np from ..defaults import _handle_default from ..fixes import _safe_svd -from ..utils import warn, logger, sqrtm_sym, eigh - +from ..utils import eigh, logger, sqrtm_sym, warn # For the reference implementation of eLORETA (force_equal=False), # 0 < loose <= 1 all produce solutions that are (more or less) @@ -26,7 +26,7 @@ def _compute_eloreta(inv, lambda2, options): """Compute the eLORETA solution.""" - from .inverse import compute_rank_inverse, _compute_reginv + from .inverse import _compute_reginv, compute_rank_inverse options = _handle_default("eloreta_options", options) eps, max_iter = options["eps"], options["max_iter"] diff --git a/mne/minimum_norm/inverse.py b/mne/minimum_norm/inverse.py index 7b23d137858..b8f1cdc11bd 100644 --- a/mne/minimum_norm/inverse.py +++ b/mne/minimum_norm/inverse.py @@ -11,77 +11,75 @@ from scipy import linalg from scipy.stats import chi2 -from ._eloreta import _compute_eloreta -from ..fixes import _safe_svd -from ..io import BaseRaw from .._fiff.constants import FIFF -from .._fiff.open import fiff_open -from .._fiff.tag import find_tag from .._fiff.matrix import ( _read_named_matrix, _transpose_named_matrix, write_named_matrix, ) +from .._fiff.open import fiff_open +from .._fiff.pick import channel_type, pick_channels, pick_info, pick_types from .._fiff.proj import ( + _electrode_types, + _needs_eeg_average_ref_proj, _read_proj, - make_projector, _write_proj, - _needs_eeg_average_ref_proj, - _electrode_types, + make_projector, ) +from .._fiff.tag import find_tag from .._fiff.tree import dir_tree_find from .._fiff.write import ( - write_int, - write_float_matrix, + end_block, start_and_end_file, start_block, - end_block, - write_float, write_coord_trans, + write_float, + write_float_matrix, + write_int, write_string, ) - -from .._fiff.pick import channel_type, pick_info, pick_types, pick_channels -from ..cov import compute_whitener, _read_cov, _write_cov, Covariance, prepare_noise_cov +from ..cov import Covariance, _read_cov, _write_cov, compute_whitener, prepare_noise_cov from ..epochs import BaseEpochs, EpochsArray -from ..evoked import EvokedArray, Evoked +from ..evoked import Evoked, EvokedArray +from ..fixes import _safe_svd from ..forward import ( - compute_depth_prior, _read_forward_meas_info, - is_fixed_orient, + _select_orient_forward, + compute_depth_prior, compute_orient_prior, convert_forward_solution, - _select_orient_forward, + is_fixed_orient, ) -from ..forward.forward import write_forward_meas_info, _triage_loose +from ..forward.forward import _triage_loose, write_forward_meas_info from ..html_templates import _get_html_template +from ..io import BaseRaw +from ..source_estimate import _get_src_type, _make_stc from ..source_space._source_space import ( - _read_source_spaces_from_tree, _get_src_nn, - find_source_space_hemi, _get_vertno, + _read_source_spaces_from_tree, _write_source_spaces_to_fid, + find_source_space_hemi, label_src_vertno_sel, ) from ..surface import _normal_orth -from ..transforms import _ensure_trans, transform_surface_to from ..time_frequency.tfr import _check_tfr_complex -from ..source_estimate import _make_stc, _get_src_type +from ..transforms import _ensure_trans, transform_surface_to from ..utils import ( - check_fname, - logger, - verbose, - warn, - _validate_type, _check_compensation_grade, - _check_option, - repr_html, _check_depth, - _check_src_normal, _check_fname, + _check_option, + _check_src_normal, + _validate_type, _verbose_safe_false, + check_fname, + logger, + repr_html, + verbose, + warn, ) - +from ._eloreta import _compute_eloreta INVERSE_METHODS = ("MNE", "dSPM", "sLORETA", "eLORETA") diff --git a/mne/minimum_norm/resolution_matrix.py b/mne/minimum_norm/resolution_matrix.py index a01ece26ccb..2d419c11484 100644 --- a/mne/minimum_norm/resolution_matrix.py +++ b/mne/minimum_norm/resolution_matrix.py @@ -8,15 +8,15 @@ from mne.minimum_norm.inverse import InverseOperator -from .inverse import apply_inverse -from ..evoked import EvokedArray from .._fiff.constants import FIFF from .._fiff.pick import pick_channels_forward -from ..utils import logger, verbose, _validate_type -from ..forward.forward import convert_forward_solution, Forward -from ..source_estimate import _prepare_label_extraction, _make_stc, _get_src_type -from ..source_space._source_space import SourceSpaces, _get_vertno +from ..evoked import EvokedArray +from ..forward.forward import Forward, convert_forward_solution from ..label import Label +from ..source_estimate import _get_src_type, _make_stc, _prepare_label_extraction +from ..source_space._source_space import SourceSpaces, _get_vertno +from ..utils import _validate_type, logger, verbose +from .inverse import apply_inverse @verbose diff --git a/mne/minimum_norm/spatial_resolution.py b/mne/minimum_norm/spatial_resolution.py index b075529120d..b72e3cebdfd 100644 --- a/mne/minimum_norm/spatial_resolution.py +++ b/mne/minimum_norm/spatial_resolution.py @@ -9,7 +9,7 @@ import numpy as np from ..source_estimate import SourceEstimate -from ..utils import logger, verbose, _check_option +from ..utils import _check_option, logger, verbose @verbose diff --git a/mne/minimum_norm/tests/test_inverse.py b/mne/minimum_norm/tests/test_inverse.py index 0bfb60fe0b1..bf9537306e1 100644 --- a/mne/minimum_norm/tests/test_inverse.py +++ b/mne/minimum_norm/tests/test_inverse.py @@ -1,61 +1,60 @@ +import copy import re from pathlib import Path import numpy as np +import pytest from numpy.testing import ( - assert_array_almost_equal, - assert_equal, assert_allclose, + assert_array_almost_equal, assert_array_equal, assert_array_less, + assert_equal, ) from scipy import sparse -import pytest -import copy - import mne -from mne.datasets import testing -from mne.label import read_label, label_sign_flip -from mne.event import read_events -from mne.epochs import Epochs, EpochsArray, make_fixed_length_epochs -from mne.forward import restrict_forward_to_stc, apply_forward, is_fixed_orient -from mne.source_estimate import read_source_estimate, VolSourceEstimate -from mne.source_space._source_space import _get_src_nn -from mne.surface import _normal_orth from mne import ( - read_cov, - read_forward_solution, - read_evokeds, - pick_types, - pick_types_forward, - make_forward_solution, - EvokedArray, - convert_forward_solution, Covariance, - combine_evoked, + EvokedArray, SourceEstimate, - make_sphere_model, + combine_evoked, + compute_raw_covariance, + convert_forward_solution, make_ad_hoc_cov, + make_forward_solution, + make_sphere_model, pick_channels_forward, - compute_raw_covariance, + pick_types, + pick_types_forward, + read_cov, + read_evokeds, + read_forward_solution, ) -from mne.io import read_raw_fif, read_info +from mne.datasets import testing +from mne.epochs import Epochs, EpochsArray, make_fixed_length_epochs +from mne.event import read_events +from mne.forward import apply_forward, is_fixed_orient, restrict_forward_to_stc +from mne.io import read_info, read_raw_fif +from mne.label import label_sign_flip, read_label from mne.minimum_norm import ( + INVERSE_METHODS, apply_inverse, - read_inverse_operator, - apply_inverse_raw, + apply_inverse_cov, apply_inverse_epochs, + apply_inverse_raw, apply_inverse_tfr_epochs, + compute_rank_inverse, make_inverse_operator, - apply_inverse_cov, - write_inverse_operator, prepare_inverse_operator, - compute_rank_inverse, - INVERSE_METHODS, + read_inverse_operator, + write_inverse_operator, ) +from mne.source_estimate import VolSourceEstimate, read_source_estimate +from mne.source_space._source_space import _get_src_nn +from mne.surface import _normal_orth from mne.time_frequency import EpochsTFR -from mne.utils import catch_logging, _record_warnings +from mne.utils import _record_warnings, catch_logging test_path = testing.data_path(download=False) s_path = test_path / "MEG" / "sample" diff --git a/mne/minimum_norm/tests/test_resolution_matrix.py b/mne/minimum_norm/tests/test_resolution_matrix.py index f976a59ec8e..6b2d56fd522 100644 --- a/mne/minimum_norm/tests/test_resolution_matrix.py +++ b/mne/minimum_norm/tests/test_resolution_matrix.py @@ -5,17 +5,18 @@ # License: BSD-3-Clause from contextlib import nullcontext + import numpy as np -from numpy.testing import assert_array_almost_equal, assert_array_equal, assert_allclose import pytest +from numpy.testing import assert_allclose, assert_array_almost_equal, assert_array_equal import mne from mne.datasets import testing from mne.minimum_norm.resolution_matrix import ( - make_inverse_resolution_matrix, + _vertices_for_get_psf_ctf, get_cross_talk, get_point_spread, - _vertices_for_get_psf_ctf, + make_inverse_resolution_matrix, ) data_path = testing.data_path(download=False) diff --git a/mne/minimum_norm/tests/test_resolution_metrics.py b/mne/minimum_norm/tests/test_resolution_metrics.py index 700b9243ba0..72afd2eb406 100644 --- a/mne/minimum_norm/tests/test_resolution_metrics.py +++ b/mne/minimum_norm/tests/test_resolution_metrics.py @@ -12,14 +12,14 @@ import numpy as np import pytest -from numpy.testing import assert_array_almost_equal, assert_array_equal, assert_ +from numpy.testing import assert_, assert_array_almost_equal, assert_array_equal import mne from mne.datasets import testing from mne.minimum_norm.resolution_matrix import make_inverse_resolution_matrix from mne.minimum_norm.spatial_resolution import ( - resolution_metrics, _rectify_resolution_matrix, + resolution_metrics, ) data_path = testing.data_path(download=False) diff --git a/mne/minimum_norm/tests/test_snr.py b/mne/minimum_norm/tests/test_snr.py index bfd4d1ed001..92d8132dcce 100644 --- a/mne/minimum_norm/tests/test_snr.py +++ b/mne/minimum_norm/tests/test_snr.py @@ -5,13 +5,13 @@ import os from os import path as op + import numpy as np from numpy.testing import assert_allclose from mne import read_evokeds from mne.datasets import testing -from mne.minimum_norm import read_inverse_operator, estimate_snr - +from mne.minimum_norm import estimate_snr, read_inverse_operator from mne.utils import requires_mne, run_subprocess s_path = op.join(testing.data_path(download=False), "MEG", "sample") diff --git a/mne/minimum_norm/tests/test_time_frequency.py b/mne/minimum_norm/tests/test_time_frequency.py index 82c73d6c575..e581b4ae694 100644 --- a/mne/minimum_norm/tests/test_time_frequency.py +++ b/mne/minimum_norm/tests/test_time_frequency.py @@ -1,26 +1,24 @@ import numpy as np -from numpy.testing import assert_allclose import pytest +from numpy.testing import assert_allclose +from mne import Epochs, find_events, pick_types +from mne._fiff.constants import FIFF from mne.datasets import testing -from mne import find_events, Epochs, pick_types from mne.io import read_raw_fif -from mne._fiff.constants import FIFF from mne.label import read_label from mne.minimum_norm import ( - read_inverse_operator, + INVERSE_METHODS, apply_inverse_epochs, prepare_inverse_operator, - INVERSE_METHODS, + read_inverse_operator, ) from mne.minimum_norm.time_frequency import ( - source_band_induced_power, - source_induced_power, compute_source_psd, compute_source_psd_epochs, + source_band_induced_power, + source_induced_power, ) - - from mne.time_frequency.multitaper import psd_array_multitaper data_path = testing.data_path(download=False) diff --git a/mne/minimum_norm/time_frequency.py b/mne/minimum_norm/time_frequency.py index aee85601d16..3c037c67085 100644 --- a/mne/minimum_norm/time_frequency.py +++ b/mne/minimum_norm/time_frequency.py @@ -5,32 +5,32 @@ import numpy as np +from .._fiff.constants import FIFF +from .._fiff.pick import pick_info +from ..baseline import _log_rescale, rescale from ..epochs import Epochs from ..event import make_fixed_length_events from ..evoked import EvokedArray from ..fixes import _safe_svd -from .._fiff.constants import FIFF -from .._fiff.pick import pick_info +from ..parallel import parallel_func from ..source_estimate import _make_stc -from ..time_frequency.tfr import cwt, morlet from ..time_frequency.multitaper import ( - _psd_from_mt, _compute_mt_params, - _psd_from_mt_adaptive, _mt_spectra, + _psd_from_mt, + _psd_from_mt_adaptive, ) -from ..baseline import rescale, _log_rescale +from ..time_frequency.tfr import cwt, morlet +from ..utils import ProgressBar, _check_option, logger, verbose from .inverse import ( - combine_xyz, - _check_or_prepare, - _assemble_kernel, - _pick_channels_inverse_operator, INVERSE_METHODS, + _assemble_kernel, + _check_or_prepare, _check_ori, + _pick_channels_inverse_operator, _subject_from_inverse, + combine_xyz, ) -from ..parallel import parallel_func -from ..utils import logger, verbose, ProgressBar, _check_option def _prepare_source_params( diff --git a/mne/morph.py b/mne/morph.py index e74ed3a8f77..edfa6643c9c 100644 --- a/mne/morph.py +++ b/mne/morph.py @@ -15,31 +15,33 @@ from .morph_map import read_morph_map from .parallel import parallel_func from .source_estimate import ( + _BaseSourceEstimate, _BaseSurfaceSourceEstimate, _BaseVolSourceEstimate, - _BaseSourceEstimate, _get_ico_tris, ) from .source_space._source_space import SourceSpaces, _ensure_src, _grid_interp -from .surface import mesh_edges, read_surface, _compute_nearest +from .surface import _compute_nearest, mesh_edges, read_surface from .utils import ( - logger, - verbose, - check_version, - get_subjects_dir, - warn as warn_, - fill_doc, - _check_option, - _validate_type, BunchConst, + ProgressBar, _check_fname, - warn, + _check_option, _custom_lru_cache, _ensure_int, - ProgressBar, - use_log_level, - _import_nibabel, _import_h5io_funcs, + _import_nibabel, + _validate_type, + check_version, + fill_doc, + get_subjects_dir, + logger, + use_log_level, + verbose, + warn, +) +from .utils import ( + warn as warn_, ) @@ -1026,10 +1028,12 @@ def _get_src_data(src, mri_resolution=True): def _triage_output(output): _check_option("output", output, ["nifti", "nifti1", "nifti2"]) if output in ("nifti", "nifti1"): - from nibabel import Nifti1Image as NiftiImage, Nifti1Header as NiftiHeader + from nibabel import Nifti1Header as NiftiHeader + from nibabel import Nifti1Image as NiftiImage else: assert output == "nifti2" - from nibabel import Nifti2Image as NiftiImage, Nifti2Header as NiftiHeader + from nibabel import Nifti2Header as NiftiHeader + from nibabel import Nifti2Image as NiftiImage return NiftiImage, NiftiHeader @@ -1140,9 +1144,10 @@ def _interpolate_data(stc, morph, mri_resolution, mri_space, output): def _compute_morph_sdr(mri_from, mri_to, niter_affine, niter_sdr, zooms): """Get a matrix that morphs data from one subject to another.""" - from .transforms import _compute_volume_registration from dipy.align.imaffine import AffineMap + from .transforms import _compute_volume_registration + pipeline = "all" if niter_sdr else "affines" niter = dict( translation=niter_affine, diff --git a/mne/morph_map.py b/mne/morph_map.py index 83f68715357..b1153123174 100644 --- a/mne/morph_map.py +++ b/mne/morph_map.py @@ -18,22 +18,22 @@ from ._fiff.tag import find_tag from ._fiff.tree import dir_tree_find from ._fiff.write import ( - start_block, end_block, - write_string, start_and_end_file, + start_block, write_float_sparse_rcs, write_int, + write_string, ) from .surface import ( - read_surface, - _triangle_neighbors, _compute_nearest, - _normalize_vectors, - _get_tri_supp_geom, _find_nearest_tri_pts, + _get_tri_supp_geom, + _normalize_vectors, + _triangle_neighbors, + read_surface, ) -from .utils import get_subjects_dir, warn, logger, verbose +from .utils import get_subjects_dir, logger, verbose, warn @verbose diff --git a/mne/parallel.py b/mne/parallel.py index 7d314f05718..2598d93ae92 100644 --- a/mne/parallel.py +++ b/mne/parallel.py @@ -9,13 +9,13 @@ import os from .utils import ( - logger, - verbose, - warn, ProgressBar, - _validate_type, _ensure_int, + _validate_type, get_config, + logger, + verbose, + warn, ) diff --git a/mne/preprocessing/__init__.pyi b/mne/preprocessing/__init__.pyi index bd4dd7f44c4..7d0741ab30a 100644 --- a/mne/preprocessing/__init__.pyi +++ b/mne/preprocessing/__init__.pyi @@ -47,45 +47,45 @@ __all__ = [ ] from . import eyetracking, ieeg, nirs from ._annotate_amplitude import annotate_amplitude -from .maxfilter import apply_maxfilter -from .ssp import compute_proj_ecg, compute_proj_eog -from .eog import find_eog_events, create_eog_epochs -from .ecg import find_ecg_events, create_ecg_epochs +from ._annotate_nan import annotate_nan +from ._csd import compute_bridged_electrodes, compute_current_source_density +from ._css import cortical_signal_suppression +from ._fine_cal import ( + compute_fine_calibration, + read_fine_calibration, + write_fine_calibration, +) +from ._peak_finder import peak_finder +from ._regress import EOGRegression, read_eog_regression, regress_artifact +from .artifact_detection import ( + annotate_break, + annotate_movement, + annotate_muscle_zscore, + compute_average_dev_head_t, +) +from .ecg import create_ecg_epochs, find_ecg_events +from .eog import create_eog_epochs, find_eog_events +from .hfc import compute_proj_hfc from .ica import ( ICA, - ica_find_eog_events, - ica_find_ecg_events, + corrmap, get_score_funcs, + ica_find_ecg_events, + ica_find_eog_events, read_ica, - corrmap, read_ica_eeglab, ) -from .otp import oversampled_temporal_projection -from ._peak_finder import peak_finder from .infomax_ import infomax -from .stim import fix_stim_artifact +from .interpolate import equalize_bads, interpolate_bridged_electrodes +from .maxfilter import apply_maxfilter from .maxwell import ( - maxwell_filter, - find_bad_channels_maxwell, compute_maxwell_basis, + find_bad_channels_maxwell, + maxwell_filter, maxwell_filter_prepare_emptyroom, ) +from .otp import oversampled_temporal_projection from .realign import realign_raw +from .ssp import compute_proj_ecg, compute_proj_eog +from .stim import fix_stim_artifact from .xdawn import Xdawn -from ._csd import compute_current_source_density, compute_bridged_electrodes -from .artifact_detection import ( - annotate_movement, - compute_average_dev_head_t, - annotate_muscle_zscore, - annotate_break, -) -from ._regress import regress_artifact, EOGRegression, read_eog_regression -from ._fine_cal import ( - compute_fine_calibration, - read_fine_calibration, - write_fine_calibration, -) -from ._annotate_nan import annotate_nan -from .interpolate import equalize_bads, interpolate_bridged_electrodes -from ._css import cortical_signal_suppression -from .hfc import compute_proj_hfc diff --git a/mne/preprocessing/_annotate_amplitude.py b/mne/preprocessing/_annotate_amplitude.py index 1a0a7053cc8..85c796258a2 100644 --- a/mne/preprocessing/_annotate_amplitude.py +++ b/mne/preprocessing/_annotate_amplitude.py @@ -4,15 +4,15 @@ import numpy as np -from ..fixes import jit -from ..io import BaseRaw +from .._fiff.pick import _picks_by_type, _picks_to_idx from ..annotations import ( Annotations, _adjust_onset_meas_date, _annotations_starts_stops, ) -from .._fiff.pick import _picks_to_idx, _picks_by_type -from ..utils import _validate_type, verbose, logger, _mask_to_onsets_offsets +from ..fixes import jit +from ..io import BaseRaw +from ..utils import _mask_to_onsets_offsets, _validate_type, logger, verbose @verbose diff --git a/mne/preprocessing/_csd.py b/mne/preprocessing/_csd.py index 022c94e8130..27f1481fcc0 100644 --- a/mne/preprocessing/_csd.py +++ b/mne/preprocessing/_csd.py @@ -13,17 +13,17 @@ # permission from authors of original GPL code import numpy as np -from scipy.stats import gaussian_kde from scipy.optimize import minimize_scalar +from scipy.stats import gaussian_kde -from ..utils import _validate_type, _ensure_int, _check_preload, verbose, logger -from ..io import BaseRaw from .._fiff.constants import FIFF from .._fiff.pick import pick_types -from ..epochs import BaseEpochs, make_fixed_length_epochs -from ..evoked import Evoked from ..bem import fit_sphere_to_headshape from ..channels.interpolation import _calc_g, _calc_h +from ..epochs import BaseEpochs, make_fixed_length_epochs +from ..evoked import Evoked +from ..io import BaseRaw +from ..utils import _check_preload, _ensure_int, _validate_type, logger, verbose def _prepare_G(G, lambda2): diff --git a/mne/preprocessing/_css.py b/mne/preprocessing/_css.py index a930d8f2fd2..9132d578331 100644 --- a/mne/preprocessing/_css.py +++ b/mne/preprocessing/_css.py @@ -2,9 +2,9 @@ import numpy as np -from ..evoked import Evoked from .._fiff.pick import _picks_to_idx -from ..utils import verbose, _validate_type, _ensure_int +from ..evoked import Evoked +from ..utils import _ensure_int, _validate_type, verbose def _temp_proj(ref_2, ref_1, raw_data, n_proj=6): diff --git a/mne/preprocessing/_fine_cal.py b/mne/preprocessing/_fine_cal.py index a3a124c60f9..1b89b758e9a 100644 --- a/mne/preprocessing/_fine_cal.py +++ b/mne/preprocessing/_fine_cal.py @@ -8,30 +8,29 @@ import numpy as np from scipy.optimize import fmin_cobyla +from .._fiff.pick import pick_info, pick_types +from .._fiff.tag import _coil_trans_to_loc, _loc_to_coil_trans from ..bem import _check_origin from ..io import BaseRaw -from .._fiff.pick import pick_info, pick_types -from .._fiff.tag import _loc_to_coil_trans, _coil_trans_to_loc from ..transforms import _find_vector_rotation from ..utils import ( - logger, - verbose, - check_fname, _check_fname, - _pl, - _ensure_int, _check_option, - _validate_type, + _ensure_int, + _pl, _reg_pinv, + _validate_type, + check_fname, + logger, + verbose, ) - from .maxwell import ( _col_norm_pinv, - _trans_sss_basis, - _prep_mf_coils, _get_grad_point_coilsets, - _read_cross_talk, _prep_fine_cal, + _prep_mf_coils, + _read_cross_talk, + _trans_sss_basis, ) diff --git a/mne/preprocessing/_peak_finder.py b/mne/preprocessing/_peak_finder.py index a9137d2bb4b..ec43aeb2ff4 100644 --- a/mne/preprocessing/_peak_finder.py +++ b/mne/preprocessing/_peak_finder.py @@ -1,6 +1,6 @@ import numpy as np -from ..utils import logger, verbose, _pl +from ..utils import _pl, logger, verbose @verbose diff --git a/mne/preprocessing/_regress.py b/mne/preprocessing/_regress.py index 4174736e68b..f748f7020be 100644 --- a/mne/preprocessing/_regress.py +++ b/mne/preprocessing/_regress.py @@ -5,22 +5,22 @@ import numpy as np -from ..defaults import _INTERPOLATION_DEFAULT, _EXTRAPOLATE_DEFAULT, _BORDER_DEFAULT +from .._fiff.pick import _picks_to_idx +from ..defaults import _BORDER_DEFAULT, _EXTRAPOLATE_DEFAULT, _INTERPOLATION_DEFAULT from ..epochs import BaseEpochs from ..evoked import Evoked -from .._fiff.pick import _picks_to_idx from ..io import BaseRaw +from ..minimum_norm.inverse import _needs_eeg_average_ref_proj from ..utils import ( + _check_fname, + _check_option, _check_preload, + _import_h5io_funcs, _validate_type, - _check_option, - verbose, - fill_doc, copy_function_doc_to_method_doc, - _check_fname, - _import_h5io_funcs, + fill_doc, + verbose, ) -from ..minimum_norm.inverse import _needs_eeg_average_ref_proj from ..viz import plot_regression_weights diff --git a/mne/preprocessing/artifact_detection.py b/mne/preprocessing/artifact_detection.py index 12f4a1e1538..2a34401734a 100644 --- a/mne/preprocessing/artifact_detection.py +++ b/mne/preprocessing/artifact_detection.py @@ -4,27 +4,27 @@ import numpy as np +from scipy.ndimage import distance_transform_edt, label from scipy.signal import find_peaks from scipy.stats import zscore -from scipy.ndimage import distance_transform_edt, label -from ..io.base import BaseRaw from ..annotations import ( Annotations, + _adjust_onset_meas_date, _annotations_starts_stops, annotations_from_events, - _adjust_onset_meas_date, ) +from ..filter import filter_data +from ..io.base import BaseRaw from ..transforms import ( - quat_to_rot, - _average_quats, + Transform, _angle_between_quats, - apply_trans, + _average_quats, _quat_to_affine, + apply_trans, + quat_to_rot, ) -from ..filter import filter_data -from ..transforms import Transform -from ..utils import _mask_to_onsets_offsets, logger, verbose, _validate_type, _pl +from ..utils import _mask_to_onsets_offsets, _pl, _validate_type, logger, verbose @verbose diff --git a/mne/preprocessing/ecg.py b/mne/preprocessing/ecg.py index 388fe9e416d..4ba1a4c0d1d 100644 --- a/mne/preprocessing/ecg.py +++ b/mne/preprocessing/ecg.py @@ -6,14 +6,14 @@ import numpy as np +from .._fiff.meas_info import create_info +from .._fiff.pick import _picks_to_idx, pick_channels, pick_types from ..annotations import _annotations_starts_stops -from ..utils import logger, verbose, sum_squared, warn, int_like +from ..epochs import BaseEpochs, Epochs +from ..evoked import Evoked from ..filter import filter_data -from ..epochs import Epochs, BaseEpochs from ..io import BaseRaw, RawArray -from ..evoked import Evoked -from .._fiff.meas_info import create_info -from .._fiff.pick import _picks_to_idx, pick_types, pick_channels +from ..utils import int_like, logger, sum_squared, verbose, warn @verbose diff --git a/mne/preprocessing/eog.py b/mne/preprocessing/eog.py index abd10c5ef88..a169ada87f3 100644 --- a/mne/preprocessing/eog.py +++ b/mne/preprocessing/eog.py @@ -6,11 +6,11 @@ import numpy as np -from ._peak_finder import peak_finder -from .._fiff.pick import pick_types, pick_channels -from ..utils import logger, verbose, _pl, _validate_type -from ..filter import filter_data +from .._fiff.pick import pick_channels, pick_types from ..epochs import Epochs +from ..filter import filter_data +from ..utils import _pl, _validate_type, logger, verbose +from ._peak_finder import peak_finder @verbose diff --git a/mne/preprocessing/eyetracking/_pupillometry.py b/mne/preprocessing/eyetracking/_pupillometry.py index 805b15de41a..859e413ce6a 100644 --- a/mne/preprocessing/eyetracking/_pupillometry.py +++ b/mne/preprocessing/eyetracking/_pupillometry.py @@ -4,9 +4,9 @@ import numpy as np -from ...io import BaseRaw from ..._fiff.constants import FIFF -from ...utils import logger, _check_preload, _validate_type, warn +from ...io import BaseRaw +from ...utils import _check_preload, _validate_type, logger, warn def interpolate_blinks(raw, buffer=0.05, match="BAD_blink", interpolate_gaze=False): diff --git a/mne/preprocessing/eyetracking/tests/test_calibration.py b/mne/preprocessing/eyetracking/tests/test_calibration.py index 55caad14cda..26320688ed5 100644 --- a/mne/preprocessing/eyetracking/tests/test_calibration.py +++ b/mne/preprocessing/eyetracking/tests/test_calibration.py @@ -1,8 +1,8 @@ -import pytest - import numpy as np +import pytest from mne.datasets.testing import data_path, requires_testing_data + from ..calibration import Calibration, read_eyelink_calibration # for test_read_eylink_calibration diff --git a/mne/preprocessing/eyetracking/tests/test_pupillometry.py b/mne/preprocessing/eyetracking/tests/test_pupillometry.py index 00d3518aa97..5bdd4866a53 100644 --- a/mne/preprocessing/eyetracking/tests/test_pupillometry.py +++ b/mne/preprocessing/eyetracking/tests/test_pupillometry.py @@ -6,7 +6,7 @@ from mne import create_info from mne.datasets.testing import data_path, requires_testing_data -from mne.io import read_raw_eyelink, RawArray +from mne.io import RawArray, read_raw_eyelink from mne.preprocessing.eyetracking import interpolate_blinks fname = data_path(download=False) / "eyetrack" / "test_eyelink.asc" diff --git a/mne/preprocessing/hfc.py b/mne/preprocessing/hfc.py index 25084aedc69..71870330dcc 100644 --- a/mne/preprocessing/hfc.py +++ b/mne/preprocessing/hfc.py @@ -4,10 +4,10 @@ import numpy as np -from .maxwell import _prep_mf_coils, _sss_basis from .._fiff.pick import _picks_to_idx, pick_info from .._fiff.proj import Projection from ..utils import verbose +from .maxwell import _prep_mf_coils, _sss_basis @verbose diff --git a/mne/preprocessing/ica.py b/mne/preprocessing/ica.py index fdb7d920267..c0139427a4a 100644 --- a/mne/preprocessing/ica.py +++ b/mne/preprocessing/ica.py @@ -5,102 +5,99 @@ # # License: BSD-3-Clause -from inspect import isfunction, signature, Parameter +import json +import math +import warnings from collections import namedtuple from collections.abc import Sequence from copy import deepcopy +from dataclasses import dataclass, is_dataclass +from inspect import Parameter, isfunction, signature from numbers import Integral from time import time -from dataclasses import dataclass, is_dataclass -from typing import Optional, List, Literal -import warnings - -import math -import json +from typing import List, Literal, Optional import numpy as np from scipy import linalg, stats from scipy.spatial import distance from scipy.special import expit -from .ecg import qrs_detector, _get_ecg_channel_index, _make_ecg, create_ecg_epochs -from .eog import _find_eog_events, _get_eog_channel_index -from ..html_templates import _get_html_template -from .infomax_ import infomax - -from ..cov import compute_whitener, Covariance -from ..evoked import Evoked -from ..defaults import _BORDER_DEFAULT, _EXTRAPOLATE_DEFAULT, _INTERPOLATION_DEFAULT +from .._fiff.constants import FIFF +from .._fiff.meas_info import ContainsMixin, read_meas_info, write_meas_info +from .._fiff.open import fiff_open from .._fiff.pick import ( - pick_types, + _DATA_CH_TYPES_SPLIT, + _contains_ch_type, + _picks_by_type, + _picks_to_idx, pick_channels, + pick_channels_regexp, pick_info, - _picks_to_idx, - _DATA_CH_TYPES_SPLIT, + pick_types, ) from .._fiff.proj import make_projector +from .._fiff.tag import read_tag +from .._fiff.tree import dir_tree_find from .._fiff.write import ( + end_block, + start_and_end_file, + start_block, write_double_matrix, - write_string, - write_name_list, + write_id, write_int, - start_block, - end_block, -) -from .._fiff.tree import dir_tree_find -from .._fiff.open import fiff_open -from .._fiff.tag import read_tag -from .._fiff.meas_info import write_meas_info, read_meas_info, ContainsMixin -from .._fiff.constants import FIFF -from .._fiff.write import start_and_end_file, write_id -from .._fiff.pick import pick_channels_regexp, _picks_by_type, _contains_ch_type -from ..io import BaseRaw -from ..io.eeglab.eeglab import _get_info, _check_load_mat - -from ..epochs import BaseEpochs -from ..viz import ( - plot_ica_components, - plot_ica_scores, - plot_ica_sources, - plot_ica_overlay, + write_name_list, + write_string, ) -from ..viz.ica import plot_ica_properties -from ..viz.topomap import _plot_corrmap - from ..channels.layout import _find_topomap_coords +from ..cov import Covariance, compute_whitener +from ..defaults import _BORDER_DEFAULT, _EXTRAPOLATE_DEFAULT, _INTERPOLATION_DEFAULT +from ..epochs import BaseEpochs +from ..evoked import Evoked +from ..filter import filter_data +from ..fixes import _safe_svd +from ..html_templates import _get_html_template +from ..io import BaseRaw +from ..io.eeglab.eeglab import _check_load_mat, _get_info from ..utils import ( - logger, - check_fname, + _PCA, + Bunch, + _check_all_same_channel_names, + _check_compensation_grade, _check_fname, - verbose, + _check_on_missing, + _check_option, + _check_preload, + _ensure_int, + _get_inst_data, + _on_missing, + _pl, _reject_data_segments, - check_random_state, + _require_version, _validate_type, + check_fname, + check_random_state, compute_corr, - _get_inst_data, - _ensure_int, - repr_html, copy_function_doc_to_method_doc, - _pl, - warn, - Bunch, - _check_preload, - _check_compensation_grade, fill_doc, - _check_option, - _PCA, int_like, - _require_version, - _check_all_same_channel_names, - _check_on_missing, - _on_missing, + logger, + repr_html, + verbose, + warn, ) - -from ..fixes import _safe_svd -from ..filter import filter_data +from ..viz import ( + plot_ica_components, + plot_ica_overlay, + plot_ica_scores, + plot_ica_sources, +) +from ..viz.ica import plot_ica_properties +from ..viz.topomap import _plot_corrmap from .bads import _find_outliers from .ctps_ import ctps - +from .ecg import _get_ecg_channel_index, _make_ecg, create_ecg_epochs, qrs_detector +from .eog import _find_eog_events, _get_eog_channel_index +from .infomax_ import infomax __all__ = ( "ICA", diff --git a/mne/preprocessing/ieeg/_projection.py b/mne/preprocessing/ieeg/_projection.py index 3df134e6a59..72292dd577d 100644 --- a/mne/preprocessing/ieeg/_projection.py +++ b/mne/preprocessing/ieeg/_projection.py @@ -3,20 +3,21 @@ # License: BSD-3-Clause from itertools import combinations + import numpy as np from scipy.spatial.distance import pdist, squareform -from ...channels import make_dig_montage from ..._fiff.pick import _picks_to_idx +from ...channels import make_dig_montage from ...surface import ( + _compute_nearest, _read_mri_surface, + _read_patch, fast_cross_3d, read_surface, - _read_patch, - _compute_nearest, ) -from ...transforms import apply_trans, invert_transform, _cart_to_sph, _ensure_trans -from ...utils import verbose, get_subjects_dir, _validate_type, _ensure_int +from ...transforms import _cart_to_sph, _ensure_trans, apply_trans, invert_transform +from ...utils import _ensure_int, _validate_type, get_subjects_dir, verbose @verbose diff --git a/mne/preprocessing/ieeg/_volume.py b/mne/preprocessing/ieeg/_volume.py index 9a35b802207..0e35e69de6d 100644 --- a/mne/preprocessing/ieeg/_volume.py +++ b/mne/preprocessing/ieeg/_volume.py @@ -6,8 +6,8 @@ from ...channels import DigMontage, make_dig_montage from ...surface import _voxel_neighbors -from ...transforms import apply_trans, _frame_to_str, Transform -from ...utils import verbose, warn, _pl, _validate_type, _require_version, _check_option +from ...transforms import Transform, _frame_to_str, apply_trans +from ...utils import _check_option, _pl, _require_version, _validate_type, verbose, warn @verbose @@ -35,9 +35,9 @@ def warp_montage(montage, moving, static, reg_affine, sdr_morph, verbose=None): _require_version("nibabel", "warp montage", "2.1.0") _require_version("dipy", "warping points using SDR", "1.6.0") + from dipy.align.imwarp import DiffeomorphicMap from nibabel import MGHImage from nibabel.spatialimages import SpatialImage - from dipy.align.imwarp import DiffeomorphicMap _validate_type(moving, SpatialImage, "moving") _validate_type(static, SpatialImage, "static") diff --git a/mne/preprocessing/ieeg/tests/test_projection.py b/mne/preprocessing/ieeg/tests/test_projection.py index 45e49e2ad7e..bd1ebffbc06 100644 --- a/mne/preprocessing/ieeg/tests/test_projection.py +++ b/mne/preprocessing/ieeg/tests/test_projection.py @@ -5,14 +5,15 @@ import os from shutil import copyfile + import numpy as np -from numpy.testing import assert_allclose import pytest +from numpy.testing import assert_allclose import mne +from mne.datasets import testing from mne.preprocessing.ieeg import project_sensors_onto_brain from mne.preprocessing.ieeg._projection import _project_sensors_onto_inflated -from mne.datasets import testing from mne.transforms import _get_trans data_path = testing.data_path(download=False) diff --git a/mne/preprocessing/ieeg/tests/test_volume.py b/mne/preprocessing/ieeg/tests/test_volume.py index 5b264e2d023..d08df4ecd30 100644 --- a/mne/preprocessing/ieeg/tests/test_volume.py +++ b/mne/preprocessing/ieeg/tests/test_volume.py @@ -6,8 +6,8 @@ import numpy as np import pytest -from mne.coreg import get_mni_fiducials from mne.channels import make_dig_montage +from mne.coreg import get_mni_fiducials from mne.datasets import testing from mne.preprocessing.ieeg import make_montage_volume, warp_montage from mne.transforms import apply_trans, compute_volume_registration diff --git a/mne/preprocessing/infomax_.py b/mne/preprocessing/infomax_.py index f66b7a3df83..556a7b4e4ad 100644 --- a/mne/preprocessing/infomax_.py +++ b/mne/preprocessing/infomax_.py @@ -10,7 +10,7 @@ from scipy.special import expit from scipy.stats import kurtosis -from ..utils import logger, verbose, check_random_state, random_permutation +from ..utils import check_random_state, logger, random_permutation, verbose @verbose diff --git a/mne/preprocessing/interpolate.py b/mne/preprocessing/interpolate.py index 45cb1d439a7..830f0bfb57a 100644 --- a/mne/preprocessing/interpolate.py +++ b/mne/preprocessing/interpolate.py @@ -7,12 +7,12 @@ import numpy as np from scipy.sparse.csgraph import connected_components -from ..utils import _validate_type, _ensure_int -from ..io import BaseRaw, RawArray from .._fiff.meas_info import create_info from ..epochs import BaseEpochs, EpochsArray from ..evoked import Evoked, EvokedArray -from ..transforms import _sph_to_cart, _cart_to_sph +from ..io import BaseRaw, RawArray +from ..transforms import _cart_to_sph, _sph_to_cart +from ..utils import _ensure_int, _validate_type def equalize_bads(insts, interp_thresh=1.0, copy=True): diff --git a/mne/preprocessing/maxfilter.py b/mne/preprocessing/maxfilter.py index 886e3f13637..54fbaf532e2 100644 --- a/mne/preprocessing/maxfilter.py +++ b/mne/preprocessing/maxfilter.py @@ -8,7 +8,7 @@ from ..bem import fit_sphere_to_headshape from ..io import read_raw_fif -from ..utils import logger, verbose, warn, deprecated +from ..utils import deprecated, logger, verbose, warn def _mxwarn(msg): diff --git a/mne/preprocessing/maxwell.py b/mne/preprocessing/maxwell.py index 60af14dbf5a..37521b0abeb 100644 --- a/mne/preprocessing/maxwell.py +++ b/mne/preprocessing/maxwell.py @@ -16,51 +16,50 @@ from scipy.special import lpmv, sph_harm from .. import __version__ +from .._fiff.compensator import make_compensator +from .._fiff.constants import FIFF, FWD +from .._fiff.meas_info import Info, _simplify_info +from .._fiff.pick import pick_info, pick_types +from .._fiff.proc_history import _read_ctc +from .._fiff.proj import Projection +from .._fiff.tag import _coil_trans_to_loc, _loc_to_coil_trans +from .._fiff.write import DATE_NONE, _generate_meas_id from ..annotations import _annotations_starts_stops from ..bem import _check_origin +from ..channels.channels import _get_T1T2_mag_inds, fix_mag_coil_types +from ..fixes import _safe_svd, bincount +from ..forward import _concatenate_coils, _create_meg_coils, _prep_meg_channels +from ..io import BaseRaw, RawArray +from ..surface import _normalize_vectors from ..transforms import ( - _str_to_frame, - _get_trans, Transform, - apply_trans, - _find_vector_rotation, + _average_quats, _cart_to_sph, - _get_n_moments, - _sph_to_cart_partials, _deg_ord_idx, - _average_quats, + _find_vector_rotation, + _get_n_moments, + _get_trans, _sh_complex_to_real, - _sh_real_to_complex, _sh_negate, + _sh_real_to_complex, + _sph_to_cart_partials, + _str_to_frame, + apply_trans, quat_to_rot, rot_to_quat, ) -from ..forward import _concatenate_coils, _prep_meg_channels, _create_meg_coils -from ..surface import _normalize_vectors -from .._fiff.compensator import make_compensator -from .._fiff.constants import FIFF, FWD -from .._fiff.meas_info import _simplify_info, Info -from .._fiff.pick import pick_types, pick_info -from .._fiff.proj import Projection -from .._fiff.proc_history import _read_ctc -from .._fiff.write import _generate_meas_id, DATE_NONE -from .._fiff.tag import _loc_to_coil_trans, _coil_trans_to_loc -from ..io import BaseRaw, RawArray from ..utils import ( - verbose, - logger, - _clean_names, - warn, - _time_mask, - _pl, _check_option, + _clean_names, _ensure_int, + _pl, + _time_mask, _validate_type, + logger, use_log_level, + verbose, + warn, ) -from ..fixes import _safe_svd, bincount -from ..channels.channels import _get_T1T2_mag_inds, fix_mag_coil_types - # Note: MF uses single precision and some algorithms might use # truncated versions of constants (e.g., μ0), which could lead to small diff --git a/mne/preprocessing/nirs/_beer_lambert_law.py b/mne/preprocessing/nirs/_beer_lambert_law.py index 2d66216bf36..52ee73c13e8 100644 --- a/mne/preprocessing/nirs/_beer_lambert_law.py +++ b/mne/preprocessing/nirs/_beer_lambert_law.py @@ -11,10 +11,10 @@ from scipy.interpolate import interp1d from scipy.io import loadmat -from ...io import BaseRaw from ..._fiff.constants import FIFF +from ...io import BaseRaw from ...utils import _validate_type, warn -from ..nirs import source_detector_distances, _validate_nirs_info +from ..nirs import _validate_nirs_info, source_detector_distances def beer_lambert_law(raw, ppf=6.0): diff --git a/mne/preprocessing/nirs/_optical_density.py b/mne/preprocessing/nirs/_optical_density.py index 826a2d8d521..a296ff94dec 100644 --- a/mne/preprocessing/nirs/_optical_density.py +++ b/mne/preprocessing/nirs/_optical_density.py @@ -6,10 +6,10 @@ import numpy as np -from ..nirs import _validate_nirs_info -from ...io import BaseRaw from ..._fiff.constants import FIFF -from ...utils import _validate_type, warn, verbose +from ...io import BaseRaw +from ...utils import _validate_type, verbose, warn +from ..nirs import _validate_nirs_info @verbose diff --git a/mne/preprocessing/nirs/nirs.py b/mne/preprocessing/nirs/nirs.py index 36d73658216..2a1d821b4a6 100644 --- a/mne/preprocessing/nirs/nirs.py +++ b/mne/preprocessing/nirs/nirs.py @@ -5,11 +5,11 @@ # License: BSD-3-Clause import re + import numpy as np from ..._fiff.pick import _picks_to_idx, pick_types -from ...utils import fill_doc, _check_option, _validate_type - +from ...utils import _check_option, _validate_type, fill_doc # Standardized fNIRS channel name regexs _S_D_F_RE = re.compile(r"S(\d+)_D(\d+) (\d+\.?\d*)") diff --git a/mne/preprocessing/nirs/tests/test_beer_lambert_law.py b/mne/preprocessing/nirs/tests/test_beer_lambert_law.py index 440b06590dc..6fe7fd96803 100644 --- a/mne/preprocessing/nirs/tests/test_beer_lambert_law.py +++ b/mne/preprocessing/nirs/tests/test_beer_lambert_law.py @@ -4,14 +4,14 @@ # # License: BSD-3-Clause -import pytest import numpy as np +import pytest +from mne.datasets import testing from mne.datasets.testing import data_path -from mne.io import read_raw_nirx, BaseRaw, read_raw_fif -from mne.preprocessing.nirs import optical_density, beer_lambert_law +from mne.io import BaseRaw, read_raw_fif, read_raw_nirx +from mne.preprocessing.nirs import beer_lambert_law, optical_density from mne.utils import _validate_type -from mne.datasets import testing testing_path = data_path(download=False) fname_nirx_15_0 = testing_path / "NIRx" / "nirscout" / "nirx_15_0_recording" diff --git a/mne/preprocessing/nirs/tests/test_nirs.py b/mne/preprocessing/nirs/tests/test_nirs.py index 5536f6cf2f6..43850482c9c 100644 --- a/mne/preprocessing/nirs/tests/test_nirs.py +++ b/mne/preprocessing/nirs/tests/test_nirs.py @@ -4,30 +4,29 @@ # # License: BSD-3-Clause -import pytest import numpy as np -from numpy.testing import assert_array_equal, assert_array_almost_equal, assert_allclose +import pytest +from numpy.testing import assert_allclose, assert_array_almost_equal, assert_array_equal from mne import create_info +from mne._fiff.constants import FIFF +from mne._fiff.pick import _picks_to_idx +from mne.datasets import testing from mne.datasets.testing import data_path -from mne.io import read_raw_nirx, RawArray +from mne.io import RawArray, read_raw_nirx from mne.preprocessing.nirs import ( - optical_density, - beer_lambert_law, - _fnirs_spread_bads, - _validate_nirs_info, - _check_channels_ordered, - tddr, - _channel_frequencies, _channel_chromophore, + _channel_frequencies, + _check_channels_ordered, _fnirs_optode_names, + _fnirs_spread_bads, _optode_position, + _validate_nirs_info, + beer_lambert_law, + optical_density, scalp_coupling_index, + tddr, ) -from mne._fiff.pick import _picks_to_idx - -from mne.datasets import testing -from mne._fiff.constants import FIFF fname_nirx_15_0 = ( data_path(download=False) / "NIRx" / "nirscout" / "nirx_15_0_recording" diff --git a/mne/preprocessing/nirs/tests/test_optical_density.py b/mne/preprocessing/nirs/tests/test_optical_density.py index 45f362c1111..b25fcc4a8cc 100644 --- a/mne/preprocessing/nirs/tests/test_optical_density.py +++ b/mne/preprocessing/nirs/tests/test_optical_density.py @@ -4,15 +4,15 @@ # # License: BSD-3-Clause -import pytest as pytest import numpy as np +import pytest as pytest from numpy.testing import assert_allclose +from mne.datasets import testing from mne.datasets.testing import data_path -from mne.io import read_raw_nirx, BaseRaw +from mne.io import BaseRaw, read_raw_nirx from mne.preprocessing.nirs import optical_density from mne.utils import _validate_type -from mne.datasets import testing fname_nirx = ( data_path(download=False) / "NIRx" / "nirscout" / "nirx_15_2_recording_w_short" diff --git a/mne/preprocessing/nirs/tests/test_scalp_coupling_index.py b/mne/preprocessing/nirs/tests/test_scalp_coupling_index.py index 3adda7002f4..240b50e8048 100644 --- a/mne/preprocessing/nirs/tests/test_scalp_coupling_index.py +++ b/mne/preprocessing/nirs/tests/test_scalp_coupling_index.py @@ -4,18 +4,18 @@ # # License: BSD-3-Clause -import pytest import numpy as np +import pytest from numpy.testing import assert_allclose, assert_array_less +from mne.datasets import testing from mne.datasets.testing import data_path from mne.io import read_raw_nirx from mne.preprocessing.nirs import ( + beer_lambert_law, optical_density, scalp_coupling_index, - beer_lambert_law, ) -from mne.datasets import testing fname_nirx_15_0 = ( data_path(download=False) / "NIRx" / "nirscout" / "nirx_15_0_recording" diff --git a/mne/preprocessing/nirs/tests/test_temporal_derivative_distribution_repair.py b/mne/preprocessing/nirs/tests/test_temporal_derivative_distribution_repair.py index c17fa10d5d2..c89d3180908 100644 --- a/mne/preprocessing/nirs/tests/test_temporal_derivative_distribution_repair.py +++ b/mne/preprocessing/nirs/tests/test_temporal_derivative_distribution_repair.py @@ -2,14 +2,14 @@ # # License: BSD-3-Clause -import pytest import numpy as np +import pytest from numpy.testing import assert_allclose +from mne.datasets import testing from mne.datasets.testing import data_path from mne.io import read_raw_nirx from mne.preprocessing.nirs import beer_lambert_law, optical_density, tddr -from mne.datasets import testing fname_nirx_15_2 = ( data_path(download=False) / "NIRx" / "nirscout" / "nirx_15_2_recording" diff --git a/mne/preprocessing/otp.py b/mne/preprocessing/otp.py index 5e4adeb9b2b..b110c0903c8 100644 --- a/mne/preprocessing/otp.py +++ b/mne/preprocessing/otp.py @@ -7,8 +7,8 @@ import numpy as np -from .._ola import _COLA, _Storer from .._fiff.pick import _picks_to_idx +from .._ola import _COLA, _Storer from ..surface import _normalize_vectors from ..utils import logger, verbose diff --git a/mne/preprocessing/realign.py b/mne/preprocessing/realign.py index e3710fa9d58..396e4ba33e6 100644 --- a/mne/preprocessing/realign.py +++ b/mne/preprocessing/realign.py @@ -8,7 +8,7 @@ from scipy.stats import pearsonr from ..io import BaseRaw -from ..utils import _validate_type, warn, logger, verbose +from ..utils import _validate_type, logger, verbose, warn @verbose diff --git a/mne/preprocessing/ssp.py b/mne/preprocessing/ssp.py index 9efc5710b04..82c5b78a741 100644 --- a/mne/preprocessing/ssp.py +++ b/mne/preprocessing/ssp.py @@ -8,11 +8,11 @@ import numpy as np -from ..epochs import Epochs -from ..proj import compute_proj_evoked, compute_proj_epochs -from ..utils import logger, verbose, warn from .._fiff.pick import pick_types from .._fiff.reference import make_eeg_average_ref_proj +from ..epochs import Epochs +from ..proj import compute_proj_epochs, compute_proj_evoked +from ..utils import logger, verbose, warn from .ecg import find_ecg_events from .eog import find_eog_events diff --git a/mne/preprocessing/stim.py b/mne/preprocessing/stim.py index c027a0d0497..a9d00dd66cc 100644 --- a/mne/preprocessing/stim.py +++ b/mne/preprocessing/stim.py @@ -6,13 +6,12 @@ from scipy.interpolate import interp1d from scipy.signal.windows import hann -from ..evoked import Evoked +from .._fiff.pick import _picks_to_idx from ..epochs import BaseEpochs -from ..io import BaseRaw from ..event import find_events - -from .._fiff.pick import _picks_to_idx -from ..utils import _check_preload, _check_option, fill_doc +from ..evoked import Evoked +from ..io import BaseRaw +from ..utils import _check_option, _check_preload, fill_doc def _get_window(start, end): diff --git a/mne/preprocessing/tests/test_annotate_amplitude.py b/mne/preprocessing/tests/test_annotate_amplitude.py index 108438fb7cc..9eb35084b09 100644 --- a/mne/preprocessing/tests/test_annotate_amplitude.py +++ b/mne/preprocessing/tests/test_annotate_amplitude.py @@ -4,8 +4,8 @@ import datetime import itertools -from pathlib import Path import re +from pathlib import Path import numpy as np import pytest @@ -16,7 +16,6 @@ from mne.io import RawArray, read_raw_fif from mne.preprocessing import annotate_amplitude - date = datetime.datetime(2021, 12, 10, 7, 52, 24, 405305, tzinfo=datetime.timezone.utc) data_path = Path(testing.data_path(download=False)) skip_fname = data_path / "misc" / "intervalrecording_raw.fif" diff --git a/mne/preprocessing/tests/test_annotate_nan.py b/mne/preprocessing/tests/test_annotate_nan.py index c313a581b6d..b5d4ba7b22e 100644 --- a/mne/preprocessing/tests/test_annotate_nan.py +++ b/mne/preprocessing/tests/test_annotate_nan.py @@ -5,13 +5,12 @@ from pathlib import Path import numpy as np -from numpy.testing import assert_array_equal import pytest +from numpy.testing import assert_array_equal import mne from mne.preprocessing import annotate_nan - raw_fname = ( Path(__file__).parent.parent.parent / "io" / "tests" / "data" / "test_raw.fif" ) diff --git a/mne/preprocessing/tests/test_artifact_detection.py b/mne/preprocessing/tests/test_artifact_detection.py index f122714f87a..19094068019 100644 --- a/mne/preprocessing/tests/test_artifact_detection.py +++ b/mne/preprocessing/tests/test_artifact_detection.py @@ -4,18 +4,18 @@ import numpy as np import pytest - from numpy.testing import assert_allclose, assert_array_equal + +from mne import Annotations, events_from_annotations from mne.chpi import read_head_pos from mne.datasets import testing from mne.io import read_raw_fif from mne.preprocessing import ( + annotate_break, annotate_movement, - compute_average_dev_head_t, annotate_muscle_zscore, - annotate_break, + compute_average_dev_head_t, ) -from mne import Annotations, events_from_annotations from mne.tests.test_annotations import _assert_annotations_equal data_path = testing.data_path(download=False) diff --git a/mne/preprocessing/tests/test_csd.py b/mne/preprocessing/tests/test_csd.py index 3ea6578daca..1387b6b4465 100644 --- a/mne/preprocessing/tests/test_csd.py +++ b/mne/preprocessing/tests/test_csd.py @@ -9,20 +9,18 @@ from pathlib import Path import numpy as np - import pytest from numpy.testing import assert_allclose -from scipy.io import loadmat from scipy import linalg +from scipy.io import loadmat -from mne.channels import make_dig_montage -from mne import create_info, EvokedArray, pick_types, Epochs, find_events, read_epochs -from mne.io import read_raw_fif, RawArray +from mne import Epochs, EvokedArray, create_info, find_events, pick_types, read_epochs from mne._fiff.constants import FIFF -from mne.utils import object_diff +from mne.channels import make_dig_montage from mne.datasets import testing - -from mne.preprocessing import compute_current_source_density, compute_bridged_electrodes +from mne.io import RawArray, read_raw_fif +from mne.preprocessing import compute_bridged_electrodes, compute_current_source_density +from mne.utils import object_diff data_path = testing.data_path(download=False) / "preprocessing" eeg_fname = data_path / "test_eeg.mat" diff --git a/mne/preprocessing/tests/test_css.py b/mne/preprocessing/tests/test_css.py index 96f01eac95b..88f196ee969 100644 --- a/mne/preprocessing/tests/test_css.py +++ b/mne/preprocessing/tests/test_css.py @@ -2,9 +2,9 @@ import numpy as np -from mne.preprocessing._css import cortical_signal_suppression from mne import pick_types, read_evokeds from mne.datasets import testing +from mne.preprocessing._css import cortical_signal_suppression data_path = testing.data_path(download=False) fname_evoked = data_path / "MEG" / "sample" / "sample_audvis-ave.fif" diff --git a/mne/preprocessing/tests/test_ctps.py b/mne/preprocessing/tests/test_ctps.py index 9407c650ea4..ec7918ac72a 100644 --- a/mne/preprocessing/tests/test_ctps.py +++ b/mne/preprocessing/tests/test_ctps.py @@ -3,11 +3,11 @@ # License: BSD-3-Clause import numpy as np -from numpy.testing import assert_array_equal import pytest +from numpy.testing import assert_array_equal +from mne.preprocessing.ctps_ import _compute_normalized_phase, _prob_kuiper, ctps from mne.time_frequency import morlet -from mne.preprocessing.ctps_ import ctps, _prob_kuiper, _compute_normalized_phase ############################################################################### # Generate testing signal diff --git a/mne/preprocessing/tests/test_ecg.py b/mne/preprocessing/tests/test_ecg.py index 3b1f2a5aa70..b540f0b2895 100644 --- a/mne/preprocessing/tests/test_ecg.py +++ b/mne/preprocessing/tests/test_ecg.py @@ -2,9 +2,9 @@ import numpy as np -from mne.io import read_raw_fif from mne import pick_types -from mne.preprocessing import find_ecg_events, create_ecg_epochs +from mne.io import read_raw_fif +from mne.preprocessing import create_ecg_epochs, find_ecg_events data_path = Path(__file__).parent.parent.parent / "io" / "tests" / "data" raw_fname = data_path / "test_raw.fif" diff --git a/mne/preprocessing/tests/test_eeglab_infomax.py b/mne/preprocessing/tests/test_eeglab_infomax.py index b493c633688..dd98fc080da 100644 --- a/mne/preprocessing/tests/test_eeglab_infomax.py +++ b/mne/preprocessing/tests/test_eeglab_infomax.py @@ -1,17 +1,16 @@ from pathlib import Path import numpy as np -from numpy.testing import assert_almost_equal import pytest - -from scipy.linalg import svd, pinv import scipy.io as sio +from numpy.testing import assert_almost_equal +from scipy.linalg import pinv, svd -from mne.io import read_raw_fif from mne import pick_types +from mne.datasets import testing +from mne.io import read_raw_fif from mne.preprocessing.infomax_ import infomax from mne.utils import random_permutation -from mne.datasets import testing base_dir = Path(__file__).parent / "data" testing_path = testing.data_path(download=False) diff --git a/mne/preprocessing/tests/test_fine_cal.py b/mne/preprocessing/tests/test_fine_cal.py index 4e153017548..d4adb0c1280 100644 --- a/mne/preprocessing/tests/test_fine_cal.py +++ b/mne/preprocessing/tests/test_fine_cal.py @@ -3,21 +3,21 @@ # # License: BSD-3-Clause import numpy as np -from numpy.testing import assert_allclose import pytest +from numpy.testing import assert_allclose from mne import pick_types -from mne.io import read_raw_fif -from mne.datasets import testing from mne._fiff.tag import _loc_to_coil_trans +from mne.datasets import testing +from mne.io import read_raw_fif from mne.preprocessing import ( - read_fine_calibration, - write_fine_calibration, compute_fine_calibration, maxwell_filter, + read_fine_calibration, + write_fine_calibration, ) from mne.preprocessing.tests.test_maxwell import _assert_shielding -from mne.transforms import rot_to_quat, _angle_between_quats +from mne.transforms import _angle_between_quats, rot_to_quat from mne.utils import object_diff # Define fine calibration filepaths diff --git a/mne/preprocessing/tests/test_hfc.py b/mne/preprocessing/tests/test_hfc.py index 4e9b727b0cc..46ecc49037f 100644 --- a/mne/preprocessing/tests/test_hfc.py +++ b/mne/preprocessing/tests/test_hfc.py @@ -6,14 +6,13 @@ import numpy as np import pytest - from numpy.testing import assert_allclose from scipy.io import loadmat +from mne._fiff.pick import pick_channels, pick_info, pick_types from mne.datasets import testing -from mne.io import read_raw_fil, read_info +from mne.io import read_info, read_raw_fil from mne.preprocessing.hfc import compute_proj_hfc -from mne._fiff.pick import pick_types, pick_info, pick_channels fil_path = testing.data_path(download=False) / "FIL" fname_root = "sub-noise_ses-001_task-noise220622_run-001" diff --git a/mne/preprocessing/tests/test_ica.py b/mne/preprocessing/tests/test_ica.py index a9110efea28..4f623e202ff 100644 --- a/mne/preprocessing/tests/test_ica.py +++ b/mne/preprocessing/tests/test_ica.py @@ -8,51 +8,53 @@ from contextlib import nullcontext from pathlib import Path -import pytest +import matplotlib.pyplot as plt import numpy as np +import pytest from numpy.testing import ( + assert_allclose, assert_array_almost_equal, assert_array_equal, - assert_allclose, assert_equal, ) -from scipy import stats, linalg +from scipy import linalg, stats from scipy.io import loadmat, savemat -import matplotlib.pyplot as plt from mne import ( + Annotations, Epochs, - Info, - read_events, - pick_types, - create_info, EpochsArray, EvokedArray, - Annotations, - pick_channels_regexp, + Info, + create_info, make_ad_hoc_cov, + pick_channels_regexp, + pick_types, + read_events, ) +from mne._fiff.pick import _DATA_CH_TYPES_SPLIT, get_channel_type_constants from mne.cov import read_cov +from mne.datasets import testing +from mne.event import make_fixed_length_events +from mne.io import RawArray, read_raw_ctf, read_raw_eeglab, read_raw_fif +from mne.io.eeglab.eeglab import _check_load_mat from mne.preprocessing import ( ICA as _ICA, +) +from mne.preprocessing import ( ica_find_ecg_events, ica_find_eog_events, read_ica, ) from mne.preprocessing.ica import ( - get_score_funcs, - corrmap, - _sort_components, _ica_explained_variance, + _sort_components, + corrmap, + get_score_funcs, read_ica_eeglab, ) -from mne.io import read_raw_fif, RawArray, read_raw_ctf, read_raw_eeglab -from mne._fiff.pick import _DATA_CH_TYPES_SPLIT, get_channel_type_constants -from mne.io.eeglab.eeglab import _check_load_mat from mne.rank import _compute_rank_int -from mne.utils import catch_logging, _record_warnings, check_version -from mne.datasets import testing -from mne.event import make_fixed_length_events +from mne.utils import _record_warnings, catch_logging, check_version data_dir = Path(__file__).parent.parent.parent / "io" / "tests" / "data" raw_fname = data_dir / "test_raw.fif" diff --git a/mne/preprocessing/tests/test_infomax.py b/mne/preprocessing/tests/test_infomax.py index 76e23be5e4a..647f3de53fa 100644 --- a/mne/preprocessing/tests/test_infomax.py +++ b/mne/preprocessing/tests/test_infomax.py @@ -4,13 +4,10 @@ # Parts of this code are taken from scikit-learn -import pytest - import numpy as np +import pytest from numpy.testing import assert_almost_equal - -from scipy import stats -from scipy import linalg +from scipy import linalg, stats from mne.preprocessing.infomax_ import infomax diff --git a/mne/preprocessing/tests/test_interpolate.py b/mne/preprocessing/tests/test_interpolate.py index 1746251611b..01309dbb250 100644 --- a/mne/preprocessing/tests/test_interpolate.py +++ b/mne/preprocessing/tests/test_interpolate.py @@ -4,7 +4,7 @@ import numpy as np import pytest -from mne import create_info, io, pick_types, read_events, Epochs +from mne import Epochs, create_info, io, pick_types, read_events from mne.channels import make_standard_montage from mne.preprocessing import equalize_bads, interpolate_bridged_electrodes from mne.preprocessing.interpolate import _find_centroid_sphere diff --git a/mne/preprocessing/tests/test_maxwell.py b/mne/preprocessing/tests/test_maxwell.py index 13a5c4358d7..f6466047f40 100644 --- a/mne/preprocessing/tests/test_maxwell.py +++ b/mne/preprocessing/tests/test_maxwell.py @@ -8,53 +8,52 @@ from pathlib import Path import numpy as np -from numpy.testing import assert_allclose, assert_array_equal import pytest +from numpy.testing import assert_allclose, assert_array_equal from scipy import sparse from scipy.special import sph_harm import mne -from mne import compute_raw_covariance, pick_types, concatenate_raws, pick_info +from mne import compute_raw_covariance, concatenate_raws, pick_info, pick_types +from mne._fiff.constants import FIFF from mne.annotations import _annotations_starts_stops -from mne.chpi import read_head_pos, filter_chpi -from mne.forward import _prep_meg_channels +from mne.chpi import filter_chpi, read_head_pos from mne.datasets import testing -from mne.forward import use_coil_def +from mne.forward import _prep_meg_channels, use_coil_def from mne.io import ( - read_raw_fif, + BaseRaw, read_info, read_raw_bti, - read_raw_kit, - BaseRaw, read_raw_ctf, + read_raw_fif, + read_raw_kit, ) -from mne._fiff.constants import FIFF from mne.preprocessing import ( - maxwell_filter, - find_bad_channels_maxwell, annotate_amplitude, + annotate_movement, compute_maxwell_basis, + find_bad_channels_maxwell, + maxwell_filter, maxwell_filter_prepare_emptyroom, - annotate_movement, ) from mne.preprocessing.maxwell import ( + _bases_complex_to_real, + _bases_real_to_complex, _get_n_moments, - _sss_basis_basic, + _prep_mf_coils, _sh_complex_to_real, - _sh_real_to_complex, _sh_negate, - _bases_complex_to_real, + _sh_real_to_complex, + _sss_basis_basic, _trans_sss_basis, - _bases_real_to_complex, - _prep_mf_coils, ) -from mne.rank import _get_rank_sss, _compute_rank_int, compute_rank +from mne.rank import _compute_rank_int, _get_rank_sss, compute_rank from mne.utils import ( + _record_warnings, assert_meg_snr, + buggy_mkl_svd, catch_logging, - _record_warnings, object_diff, - buggy_mkl_svd, use_log_level, ) diff --git a/mne/preprocessing/tests/test_otp.py b/mne/preprocessing/tests/test_otp.py index eccb17bda9b..ae10d683d2e 100644 --- a/mne/preprocessing/tests/test_otp.py +++ b/mne/preprocessing/tests/test_otp.py @@ -2,15 +2,14 @@ # # License: BSD-3-Clause -import pytest - import numpy as np +import pytest from numpy.fft import rfft, rfftfreq from mne import create_info +from mne._fiff.pick import _pick_data_channels from mne.datasets import testing from mne.io import RawArray, read_raw_fif -from mne._fiff.pick import _pick_data_channels from mne.preprocessing import oversampled_temporal_projection from mne.utils import catch_logging diff --git a/mne/preprocessing/tests/test_peak_finder.py b/mne/preprocessing/tests/test_peak_finder.py index f474f508a43..0ba97893d67 100644 --- a/mne/preprocessing/tests/test_peak_finder.py +++ b/mne/preprocessing/tests/test_peak_finder.py @@ -1,6 +1,6 @@ -from numpy.testing import assert_array_equal, assert_equal -import pytest import numpy as np +import pytest +from numpy.testing import assert_array_equal, assert_equal from mne.preprocessing import peak_finder diff --git a/mne/preprocessing/tests/test_realign.py b/mne/preprocessing/tests/test_realign.py index f9a2d7af79a..6ab16276290 100644 --- a/mne/preprocessing/tests/test_realign.py +++ b/mne/preprocessing/tests/test_realign.py @@ -4,11 +4,11 @@ # License: BSD-3-Clause import numpy as np +import pytest from numpy.testing import assert_allclose from scipy.interpolate import interp1d -import pytest -from mne import create_info, find_events, Epochs, Annotations +from mne import Annotations, Epochs, create_info, find_events from mne.io import RawArray from mne.preprocessing import realign_raw diff --git a/mne/preprocessing/tests/test_regress.py b/mne/preprocessing/tests/test_regress.py index 290c47bbac5..ab2a640343b 100644 --- a/mne/preprocessing/tests/test_regress.py +++ b/mne/preprocessing/tests/test_regress.py @@ -10,10 +10,10 @@ from mne.datasets import testing from mne.io import read_raw_fif from mne.preprocessing import ( - regress_artifact, - create_eog_epochs, EOGRegression, + create_eog_epochs, read_eog_regression, + regress_artifact, ) data_path = testing.data_path(download=False) diff --git a/mne/preprocessing/tests/test_ssp.py b/mne/preprocessing/tests/test_ssp.py index 110f3959d1f..181629541bf 100644 --- a/mne/preprocessing/tests/test_ssp.py +++ b/mne/preprocessing/tests/test_ssp.py @@ -1,14 +1,14 @@ from pathlib import Path +import numpy as np import pytest from numpy.testing import assert_array_almost_equal -import numpy as np -from mne.io import read_raw_fif, read_raw_ctf -from mne._fiff.proj import make_projector, activate_proj -from mne.preprocessing.ssp import compute_proj_ecg, compute_proj_eog -from mne.datasets import testing from mne import pick_types +from mne._fiff.proj import activate_proj, make_projector +from mne.datasets import testing +from mne.io import read_raw_ctf, read_raw_fif +from mne.preprocessing.ssp import compute_proj_ecg, compute_proj_eog data_path = Path(__file__).parent.parent.parent / "io" / "tests" / "data" raw_fname = data_path / "test_raw.fif" diff --git a/mne/preprocessing/tests/test_stim.py b/mne/preprocessing/tests/test_stim.py index 57805bc0679..a639ad5e5d1 100644 --- a/mne/preprocessing/tests/test_stim.py +++ b/mne/preprocessing/tests/test_stim.py @@ -5,12 +5,12 @@ from pathlib import Path import numpy as np -from numpy.testing import assert_array_almost_equal import pytest +from numpy.testing import assert_array_almost_equal -from mne.io import read_raw_fif -from mne.event import read_events from mne.epochs import Epochs +from mne.event import read_events +from mne.io import read_raw_fif from mne.preprocessing.stim import fix_stim_artifact data_path = Path(__file__).parent.parent.parent / "io" / "tests" / "data" diff --git a/mne/preprocessing/tests/test_xdawn.py b/mne/preprocessing/tests/test_xdawn.py index 6551eada581..047a35d75dd 100644 --- a/mne/preprocessing/tests/test_xdawn.py +++ b/mne/preprocessing/tests/test_xdawn.py @@ -6,18 +6,17 @@ from pathlib import Path import numpy as np - -from numpy.testing import assert_array_equal, assert_array_almost_equal, assert_allclose import pytest +from numpy.testing import assert_allclose, assert_array_almost_equal, assert_array_equal from scipy import stats from mne import ( Epochs, - read_events, - pick_types, + EpochsArray, compute_raw_covariance, create_info, - EpochsArray, + pick_types, + read_events, ) from mne.decoding import Vectorizer from mne.fixes import _safe_svd @@ -349,11 +348,11 @@ def _simulate_erplike_mixed_data(n_epochs=100, n_channels=10): def test_xdawn_decoding_performance(): """Test decoding performance and extracted pattern on synthetic data.""" pytest.importorskip("sklearn") + from sklearn.linear_model import LogisticRegression + from sklearn.metrics import accuracy_score from sklearn.model_selection import KFold from sklearn.pipeline import make_pipeline - from sklearn.linear_model import LogisticRegression from sklearn.preprocessing import MinMaxScaler - from sklearn.metrics import accuracy_score n_xdawn_comps = 3 expected_accuracy = 0.98 diff --git a/mne/preprocessing/xdawn.py b/mne/preprocessing/xdawn.py index f2cb819d563..ab9684cd07d 100644 --- a/mne/preprocessing/xdawn.py +++ b/mne/preprocessing/xdawn.py @@ -7,13 +7,13 @@ import numpy as np from scipy import linalg +from .._fiff.pick import _pick_data_channels, pick_info from ..cov import Covariance, _regularized_covariance -from ..decoding import TransformerMixin, BaseEstimator +from ..decoding import BaseEstimator, TransformerMixin from ..epochs import BaseEpochs -from ..evoked import EvokedArray, Evoked +from ..evoked import Evoked, EvokedArray from ..io import BaseRaw -from .._fiff.pick import _pick_data_channels, pick_info -from ..utils import logger, _check_option +from ..utils import _check_option, logger def _construct_signal_from_epochs(epochs, events, sfreq, tmin): diff --git a/mne/proj.py b/mne/proj.py index ff4182bd8a3..e8079d151b9 100644 --- a/mne/proj.py +++ b/mne/proj.py @@ -4,33 +4,33 @@ import numpy as np -from .epochs import Epochs -from .fixes import _safe_svd -from .utils import ( - check_fname, - logger, - verbose, - _check_option, - _check_fname, - _validate_type, -) from ._fiff.constants import FIFF from ._fiff.open import fiff_open -from ._fiff.pick import pick_types, pick_types_forward, _picks_to_idx +from ._fiff.pick import _picks_to_idx, pick_types, pick_types_forward from ._fiff.proj import ( Projection, _has_eeg_average_ref_proj, _read_proj, - make_projector, - make_eeg_average_ref_proj, _write_proj, + make_eeg_average_ref_proj, + make_projector, ) from ._fiff.write import start_and_end_file +from .cov import _check_n_samples +from .epochs import Epochs from .event import make_fixed_length_events +from .fixes import _safe_svd +from .forward import _subject_from_forward, convert_forward_solution, is_fixed_orient from .parallel import parallel_func -from .cov import _check_n_samples -from .forward import is_fixed_orient, _subject_from_forward, convert_forward_solution from .source_estimate import _make_stc +from .utils import ( + _check_fname, + _check_option, + _validate_type, + check_fname, + logger, + verbose, +) @verbose diff --git a/mne/rank.py b/mne/rank.py index fe4ccf83927..0cab1f3a563 100644 --- a/mne/rank.py +++ b/mne/rank.py @@ -6,24 +6,24 @@ import numpy as np from scipy import linalg -from .defaults import _handle_default -from ._fiff.meas_info import _simplify_info, Info -from ._fiff.pick import _picks_by_type, pick_info, pick_channels_cov, _picks_to_idx +from ._fiff.meas_info import Info, _simplify_info +from ._fiff.pick import _picks_by_type, _picks_to_idx, pick_channels_cov, pick_info from ._fiff.proj import make_projector +from .defaults import _handle_default from .utils import ( - logger, - _compute_row_norms, - _pl, - _validate_type, _apply_scaling_cov, - _undo_scaling_cov, - _scaled_array, - warn, + _check_on_missing, _check_rank, + _compute_row_norms, _on_missing, - verbose, - _check_on_missing, + _pl, + _scaled_array, + _undo_scaling_cov, + _validate_type, fill_doc, + logger, + verbose, + warn, ) @@ -351,9 +351,9 @@ def compute_rank( ----- .. versionadded:: 0.18 """ - from .io import BaseRaw - from .epochs import BaseEpochs from .cov import Covariance + from .epochs import BaseEpochs + from .io import BaseRaw rank = _check_rank(rank) scalings = _handle_default("scalings_cov_rank", scalings) diff --git a/mne/report/__init__.pyi b/mne/report/__init__.pyi index 5f62e1eafbf..fa99d6e3ea1 100644 --- a/mne/report/__init__.pyi +++ b/mne/report/__init__.pyi @@ -1,2 +1,2 @@ __all__ = ["Report", "_ReportScraper", "open_report"] -from .report import Report, open_report, _ReportScraper +from .report import Report, _ReportScraper, open_report diff --git a/mne/report/js_and_css/bootstrap-icons/gen_css_for_mne.py b/mne/report/js_and_css/bootstrap-icons/gen_css_for_mne.py index 54fa40b2d09..4b0626ab1e0 100644 --- a/mne/report/js_and_css/bootstrap-icons/gen_css_for_mne.py +++ b/mne/report/js_and_css/bootstrap-icons/gen_css_for_mne.py @@ -15,10 +15,10 @@ # License: BSD-3-Clause -from pathlib import Path import base64 -import rcssmin +from pathlib import Path +import rcssmin base_dir = Path(".") css_path_in = base_dir / "bootstrap-icons.css" diff --git a/mne/report/report.py b/mne/report/report.py index faf12a79bd6..5abee10e1eb 100644 --- a/mne/report/report.py +++ b/mne/report/report.py @@ -6,86 +6,85 @@ # # License: BSD-3-Clause -import io -import dataclasses -from dataclasses import dataclass -from functools import partial -from typing import Tuple, Optional -from collections.abc import Sequence import base64 -from io import BytesIO, StringIO +import dataclasses +import fnmatch +import io import os import os.path as op -from pathlib import Path -import fnmatch import re -from shutil import copyfile import time import warnings import webbrowser +from collections.abc import Sequence +from dataclasses import dataclass +from functools import partial +from io import BytesIO, StringIO +from pathlib import Path +from shutil import copyfile +from typing import Optional, Tuple import numpy as np from .. import __version__ as MNE_VERSION -from ..evoked import read_evokeds, Evoked +from .._fiff.meas_info import Info, read_info +from .._fiff.pick import _DATA_CH_TYPES_SPLIT +from .._freesurfer import _mri_orientation, _reorient_image +from ..cov import Covariance, read_cov +from ..defaults import _handle_default +from ..epochs import BaseEpochs, read_epochs from ..event import read_events -from ..cov import read_cov, Covariance +from ..evoked import Evoked, read_evokeds +from ..forward import Forward, read_forward_solution from ..html_templates import _get_html_template -from ..source_estimate import read_source_estimate, SourceEstimate -from ..transforms import read_trans, Transform -from ..utils import sys_info -from .._fiff.meas_info import Info -from ..defaults import _handle_default -from ..io import read_raw, BaseRaw +from ..io import BaseRaw, read_raw from ..io._read_raw import _get_supported as _get_extension_reader_map -from .._fiff.meas_info import read_info -from .._fiff.pick import _DATA_CH_TYPES_SPLIT +from ..minimum_norm import InverseOperator, read_inverse_operator +from ..parallel import parallel_func +from ..preprocessing.ica import read_ica from ..proj import read_proj -from .._freesurfer import _reorient_image, _mri_orientation +from ..source_estimate import SourceEstimate, read_source_estimate +from ..surface import dig_mri_distances +from ..transforms import Transform, read_trans from ..utils import ( - logger, - verbose, - get_subjects_dir, - warn, - _ensure_int, - fill_doc, + _check_ch_locs, + _check_fname, _check_option, - _validate_type, - _safe_input, + _ensure_int, + _import_h5io_funcs, + _import_nibabel, _path_like, - use_log_level, - _check_fname, _pl, - _check_ch_locs, - _import_h5io_funcs, + _safe_input, + _validate_type, _verbose_safe_false, check_version, - _import_nibabel, + fill_doc, + get_subjects_dir, + logger, + sys_info, + use_log_level, + verbose, + warn, ) from ..utils.spectrum import _split_psd_kwargs from ..viz import ( - plot_events, + Figure3D, + _get_plot_ch_type, + create_3d_figure, + get_3d_backend, plot_alignment, + plot_compare_evokeds, plot_cov, + plot_events, plot_projs_topomap, - plot_compare_evokeds, set_3d_view, - get_3d_backend, - Figure3D, use_browser_backend, - _get_plot_ch_type, - create_3d_figure, ) from ..viz._brain.view import views_dicts -from ..viz.misc import _plot_mri_contours, _get_bem_plotting_surfaces -from ..viz.utils import _ndarray_to_fig from ..viz._scraper import _mne_qt_browser_screenshot -from ..forward import read_forward_solution, Forward -from ..epochs import read_epochs, BaseEpochs -from ..preprocessing.ica import read_ica -from ..surface import dig_mri_distances -from ..minimum_norm import read_inverse_operator, InverseOperator -from ..parallel import parallel_func +from ..viz.misc import _get_bem_plotting_surfaces, _plot_mri_contours +from ..viz.utils import _ndarray_to_fig _BEM_VIEWS = ("axial", "sagittal", "coronal") @@ -391,7 +390,7 @@ def _fig_to_img(fig, *, image_format="png", own_figure=True): if fig.__class__.__name__ in ("MNEQtBrowser", "PyQtGraphBrowser"): img = _mne_qt_browser_screenshot(fig, return_type="ndarray") elif isinstance(fig, Figure3D): - from ..viz.backends.renderer import backend, MNE_3D_BACKEND_TESTING + from ..viz.backends.renderer import MNE_3D_BACKEND_TESTING, backend backend._check_3d_figure(figure=fig) if not MNE_3D_BACKEND_TESTING: diff --git a/mne/report/tests/test_report.py b/mne/report/tests/test_report.py index 4ed7ab2e557..7577774e313 100644 --- a/mne/report/tests/test_report.py +++ b/mne/report/tests/test_report.py @@ -19,28 +19,27 @@ from mne import ( Epochs, + create_info, + pick_channels_cov, + read_cov, read_events, read_evokeds, - read_cov, - pick_channels_cov, - create_info, ) +from mne._fiff.write import DATE_NONE +from mne.datasets import testing +from mne.epochs import make_metadata +from mne.io import RawArray, read_info, read_raw_fif +from mne.preprocessing import ICA +from mne.report import Report, _ReportScraper, open_report, report from mne.report import report as report_mod from mne.report.report import ( - CONTENT_ORDER, _ALLOWED_IMAGE_FORMATS, + CONTENT_ORDER, _webp_supported, ) -from mne.io import read_raw_fif, read_info, RawArray -from mne.datasets import testing -from mne.report import Report, open_report, _ReportScraper, report from mne.utils import Bunch from mne.utils._testing import assert_object_equal from mne.viz import plot_alignment -from mne._fiff.write import DATE_NONE -from mne.preprocessing import ICA -from mne.epochs import make_metadata - data_dir = testing.data_path(download=False) subjects_dir = data_dir / "subjects" diff --git a/mne/simulation/__init__.pyi b/mne/simulation/__init__.pyi index 1a49e48a882..9e0612f53a8 100644 --- a/mne/simulation/__init__.pyi +++ b/mne/simulation/__init__.pyi @@ -12,11 +12,11 @@ __all__ = [ "simulate_stc", ] from . import metrics -from .evoked import simulate_evoked, add_noise -from .raw import simulate_raw, add_ecg, add_eog, add_chpi +from .evoked import add_noise, simulate_evoked +from .raw import add_chpi, add_ecg, add_eog, simulate_raw from .source import ( + SourceSimulator, select_source_in_label, - simulate_stc, simulate_sparse_stc, - SourceSimulator, + simulate_stc, ) diff --git a/mne/simulation/evoked.py b/mne/simulation/evoked.py index d66c8dc9759..53112df695e 100644 --- a/mne/simulation/evoked.py +++ b/mne/simulation/evoked.py @@ -8,13 +8,13 @@ import numpy as np from scipy.signal import lfilter +from .._fiff.pick import pick_info from ..cov import Covariance, compute_whitener from ..epochs import BaseEpochs from ..evoked import Evoked -from .._fiff.pick import pick_info from ..forward import apply_forward from ..io import BaseRaw -from ..utils import logger, verbose, check_random_state, _check_preload, _validate_type +from ..utils import _check_preload, _validate_type, check_random_state, logger, verbose @verbose diff --git a/mne/simulation/metrics/metrics.py b/mne/simulation/metrics/metrics.py index 41630029880..4d904623b2e 100644 --- a/mne/simulation/metrics/metrics.py +++ b/mne/simulation/metrics/metrics.py @@ -10,7 +10,7 @@ import numpy as np from scipy.spatial.distance import cdist -from ...utils import _check_option, fill_doc, _validate_type +from ...utils import _check_option, _validate_type, fill_doc def _check_stc(stc1, stc2): diff --git a/mne/simulation/metrics/tests/test_metrics.py b/mne/simulation/metrics/tests/test_metrics.py index 908b8069f1c..a9abcd9ffad 100644 --- a/mne/simulation/metrics/tests/test_metrics.py +++ b/mne/simulation/metrics/tests/test_metrics.py @@ -5,22 +5,21 @@ import numpy as np -from numpy.testing import assert_allclose import pytest +from numpy.testing import assert_allclose from scipy.linalg import norm -from mne import SourceEstimate -from mne import read_source_spaces +from mne import SourceEstimate, read_source_spaces from mne.datasets import testing from mne.simulation import metrics from mne.simulation.metrics import ( cosine_score, - region_localization_error, + f1_score, + peak_position_error, precision_score, recall_score, - f1_score, + region_localization_error, roc_auc_score, - peak_position_error, spatial_deviation_error, ) diff --git a/mne/simulation/raw.py b/mne/simulation/raw.py index 4de9c8f0704..37b15aa1dbc 100644 --- a/mne/simulation/raw.py +++ b/mne/simulation/raw.py @@ -10,47 +10,47 @@ import numpy as np -from ..event import _get_stim_channel -from .._ola import _Interp2 from .._fiff.constants import FIFF from .._fiff.meas_info import Info -from .._fiff.pick import pick_types, pick_info, pick_channels, pick_channels_forward -from ..cov import make_ad_hoc_cov, read_cov, Covariance +from .._fiff.pick import pick_channels, pick_channels_forward, pick_info, pick_types +from .._ola import _Interp2 from ..bem import fit_sphere_to_headshape, make_sphere_model, read_bem_solution -from ..io import RawArray, BaseRaw from ..chpi import ( - read_head_pos, - head_pos_to_trans_rot_t, - get_chpi_info, _get_hpi_initial_fit, + get_chpi_info, + head_pos_to_trans_rot_t, + read_head_pos, ) +from ..cov import Covariance, make_ad_hoc_cov, read_cov +from ..event import _get_stim_channel from ..forward import ( + _compute_forwards, _magnetic_dipole_field_vec, _merge_fwds, - _stc_src_sel, - convert_forward_solution, + _prep_meg_channels, _prepare_for_forward, - _transform_orig_meg_coils, - _compute_forwards, + _stc_src_sel, _to_forward_dict, + _transform_orig_meg_coils, + convert_forward_solution, restrict_forward_to_stc, - _prep_meg_channels, ) -from ..transforms import _get_trans, transform_surface_to +from ..io import BaseRaw, RawArray +from ..source_estimate import _BaseSourceEstimate from ..source_space._source_space import ( _ensure_src, _set_source_space_vertices, setup_volume_source_space, ) -from ..source_estimate import _BaseSourceEstimate from ..surface import _CheckInside +from ..transforms import _get_trans, transform_surface_to from ..utils import ( - logger, - verbose, - check_random_state, + _check_preload, _pl, _validate_type, - _check_preload, + check_random_state, + logger, + verbose, ) from .source import SourceSimulator diff --git a/mne/simulation/source.py b/mne/simulation/source.py index d103b5c0374..62f14ebe67c 100644 --- a/mne/simulation/source.py +++ b/mne/simulation/source.py @@ -10,19 +10,19 @@ import numpy as np +from ..fixes import rng_uniform +from ..label import Label from ..source_estimate import SourceEstimate, VolSourceEstimate from ..source_space._source_space import _ensure_src -from ..fixes import rng_uniform +from ..surface import _compute_nearest from ..utils import ( - check_random_state, - warn, _check_option, - fill_doc, - _ensure_int, _ensure_events, + _ensure_int, + check_random_state, + fill_doc, + warn, ) -from ..label import Label -from ..surface import _compute_nearest @fill_doc diff --git a/mne/simulation/tests/test_evoked.py b/mne/simulation/tests/test_evoked.py index 179f613c732..3fb21933fdb 100644 --- a/mne/simulation/tests/test_evoked.py +++ b/mne/simulation/tests/test_evoked.py @@ -5,30 +5,30 @@ from pathlib import Path import numpy as np +import pytest from numpy.testing import ( + assert_allclose, assert_array_almost_equal, assert_array_equal, assert_equal, - assert_allclose, ) -import pytest from mne import ( - read_cov, - read_forward_solution, - convert_forward_solution, - pick_types_forward, - read_evokeds, - pick_types, EpochsArray, compute_covariance, compute_raw_covariance, + convert_forward_solution, pick_channels_cov, + pick_types, + pick_types_forward, + read_cov, + read_evokeds, + read_forward_solution, ) +from mne.cov import regularize, whiten_evoked from mne.datasets import testing -from mne.simulation import simulate_sparse_stc, simulate_evoked, add_noise from mne.io import read_raw_fif -from mne.cov import regularize, whiten_evoked +from mne.simulation import add_noise, simulate_evoked, simulate_sparse_stc from mne.utils import catch_logging data_path = testing.data_path(download=False) diff --git a/mne/simulation/tests/test_metrics.py b/mne/simulation/tests/test_metrics.py index c4c31393951..d5902660b5f 100644 --- a/mne/simulation/tests/test_metrics.py +++ b/mne/simulation/tests/test_metrics.py @@ -4,8 +4,8 @@ # License: BSD-3-Clause import numpy as np -from numpy.testing import assert_allclose import pytest +from numpy.testing import assert_allclose from mne import read_source_spaces from mne.datasets import testing diff --git a/mne/simulation/tests/test_raw.py b/mne/simulation/tests/test_raw.py index df2f0be1119..3fa07537956 100644 --- a/mne/simulation/tests/test_raw.py +++ b/mne/simulation/tests/test_raw.py @@ -8,54 +8,54 @@ from pathlib import Path import numpy as np -from numpy.testing import assert_allclose, assert_array_equal import pytest +from numpy.testing import assert_allclose, assert_array_equal from mne import ( - read_source_spaces, - pick_types, - read_trans, - read_cov, - make_sphere_model, + Epochs, + SourceEstimate, + VolSourceEstimate, + convert_forward_solution, create_info, - setup_volume_source_space, find_events, - Epochs, fit_dipole, - transform_surface_to, make_ad_hoc_cov, - SourceEstimate, - setup_source_space, - read_bem_solution, - make_forward_solution, - convert_forward_solution, - VolSourceEstimate, make_bem_solution, + make_forward_solution, + make_sphere_model, + pick_types, + read_bem_solution, + read_cov, + read_source_spaces, + read_trans, + setup_source_space, + setup_volume_source_space, + transform_surface_to, ) +from mne._fiff.constants import FIFF from mne.bem import _surfaces_to_bem from mne.chpi import ( - read_head_pos, compute_chpi_amplitudes, compute_chpi_locs, compute_head_pos, get_chpi_info, + read_head_pos, ) -from mne.tests.test_chpi import _assert_quats from mne.datasets import testing +from mne.io import RawArray, read_raw_fif +from mne.label import Label from mne.simulation import ( - simulate_sparse_stc, - simulate_raw, - add_eog, - add_ecg, add_chpi, + add_ecg, + add_eog, add_noise, + simulate_raw, + simulate_sparse_stc, ) -from mne.source_space._source_space import _compare_source_spaces from mne.simulation.source import SourceSimulator -from mne.label import Label +from mne.source_space._source_space import _compare_source_spaces from mne.surface import _get_ico_surface -from mne.io import read_raw_fif, RawArray -from mne._fiff.constants import FIFF +from mne.tests.test_chpi import _assert_quats from mne.utils import catch_logging raw_fname_short = ( diff --git a/mne/simulation/tests/test_source.py b/mne/simulation/tests/test_source.py index aa86b8a539b..96ae9d7630e 100644 --- a/mne/simulation/tests/test_source.py +++ b/mne/simulation/tests/test_source.py @@ -4,19 +4,18 @@ # License: BSD-3-Clause import numpy as np -from numpy.testing import assert_array_almost_equal, assert_array_equal, assert_equal import pytest +from numpy.testing import assert_array_almost_equal, assert_array_equal, assert_equal -from mne.datasets import testing from mne import ( - read_label, - read_forward_solution, - pick_types_forward, convert_forward_solution, + pick_types_forward, + read_forward_solution, + read_label, ) +from mne.datasets import testing from mne.label import Label -from mne.simulation import simulate_stc, simulate_sparse_stc, SourceSimulator - +from mne.simulation import SourceSimulator, simulate_sparse_stc, simulate_stc data_path = testing.data_path(download=False) fname_fwd = data_path / "MEG" / "sample" / "sample_audvis_trunc-meg-eeg-oct-6-fwd.fif" diff --git a/mne/source_estimate.py b/mne/source_estimate.py index 211d109222c..afdb5085c6e 100644 --- a/mne/source_estimate.py +++ b/mne/source_estimate.py @@ -14,58 +14,58 @@ from scipy import sparse from scipy.spatial.distance import cdist, pdist +from ._fiff.constants import FIFF +from ._fiff.meas_info import Info +from ._fiff.pick import pick_types +from ._freesurfer import _get_atlas_values, _get_mri_info_data, read_freesurfer_lut from .baseline import rescale from .cov import Covariance from .evoked import _get_peak from .filter import resample from .fixes import _safe_svd -from ._freesurfer import _get_mri_info_data, _get_atlas_values, read_freesurfer_lut -from ._fiff.constants import FIFF -from ._fiff.pick import pick_types -from .surface import read_surface, _get_ico_surface, mesh_edges, _project_onto_surface from .source_space._source_space import ( + SourceSpaces, + _check_volume_labels, _ensure_src, - _get_morph_src_reordering, _ensure_src_subject, - SourceSpaces, + _get_morph_src_reordering, _get_src_nn, - _check_volume_labels, ) +from .surface import _get_ico_surface, _project_onto_surface, mesh_edges, read_surface from .transforms import _get_trans, apply_trans from .utils import ( - get_subjects_dir, - _check_subject, - logger, - verbose, - _pl, - _time_mask, - warn, - copy_function_doc_to_method_doc, - fill_doc, + TimeMixin, + _build_data_frame, + _check_fname, _check_option, - _validate_type, + _check_pandas_index_arguments, + _check_pandas_installed, _check_src_normal, _check_stc_units, - _check_pandas_installed, - _import_nibabel, - _check_pandas_index_arguments, + _check_subject, + _check_time_format, _convert_times, _ensure_int, - _build_data_frame, - _check_time_format, + _import_h5io_funcs, + _import_nibabel, _path_like, - sizeof_fmt, + _pl, + _time_mask, + _validate_type, + copy_function_doc_to_method_doc, + fill_doc, + get_subjects_dir, + logger, object_size, - _check_fname, - _import_h5io_funcs, - TimeMixin, + sizeof_fmt, + verbose, + warn, ) from .viz import ( plot_source_estimates, plot_vector_source_estimates, plot_volume_source_estimates, ) -from ._fiff.meas_info import Info def _read_stc(filename): @@ -1528,7 +1528,7 @@ def in_label(self, label): The source estimate restricted to the given label. """ # make sure label and stc are compatible - from .label import Label, BiHemiLabel + from .label import BiHemiLabel, Label _validate_type(label, (Label, BiHemiLabel), "label") if ( @@ -1853,7 +1853,7 @@ def estimate_snr(self, info, fwd, cov, verbose=None): ---------- .. footbibliography:: """ - from .forward import convert_forward_solution, Forward + from .forward import Forward, convert_forward_solution from .minimum_norm.inverse import _prepare_forward _validate_type(fwd, Forward, "fwd") @@ -3283,7 +3283,7 @@ def _prepare_label_extraction(stc, labels, src, mode, allow_empty, use_sparse): # of vol src space. # If stc=None (i.e. no activation time courses provided) and mode='mean', # only computes vertex indices and label_flip will be list of None. - from .label import label_sign_flip, Label, BiHemiLabel + from .label import BiHemiLabel, Label, label_sign_flip # if source estimate provided in stc, get vertices from source space and # check that they are the same as in the stcs diff --git a/mne/source_space/__init__.pyi b/mne/source_space/__init__.pyi index fab90945882..aeb7657bd33 100644 --- a/mne/source_space/__init__.pyi +++ b/mne/source_space/__init__.pyi @@ -11,12 +11,12 @@ __all__ = [ ] from . import _source_space from ._source_space import ( + SourceSpaces, + add_source_space_distances, compute_distance_to_sensors, get_decimated_surfaces, - SourceSpaces, read_source_spaces, - write_source_spaces, setup_source_space, setup_volume_source_space, - add_source_space_distances, + write_source_spaces, ) diff --git a/mne/source_space/_source_space.py b/mne/source_space/_source_space.py index 653645a59ed..7368cf4a0d5 100644 --- a/mne/source_space/_source_space.py +++ b/mne/source_space/_source_space.py @@ -6,95 +6,94 @@ # Many of the computations in this code were derived from Matti Hämäläinen's # C code. -from copy import deepcopy -from functools import partial import os import os.path as op +from copy import deepcopy +from functools import partial import numpy as np from scipy.sparse import csr_matrix, triu from scipy.sparse.csgraph import dijkstra from scipy.spatial.distance import cdist -from ..bem import read_bem_surfaces, ConductorModel from .._fiff.constants import FIFF -from .._fiff.meas_info import create_info, Info +from .._fiff.meas_info import Info, create_info from .._fiff.open import fiff_open -from .._fiff.pick import channel_type, _picks_to_idx +from .._fiff.pick import _picks_to_idx, channel_type from .._fiff.tag import find_tag, read_tag from .._fiff.tree import dir_tree_find from .._fiff.write import ( - start_block, - start_and_end_file, end_block, + start_and_end_file, + start_block, + write_coord_trans, + write_float_matrix, + write_float_sparse_rcs, write_id, write_int, - write_float_sparse_rcs, - write_string, - write_float_matrix, write_int_matrix, - write_coord_trans, + write_string, +) + +# Remove get_mni_fiducials in 1.6 (deprecated) +from .._freesurfer import ( + _check_mri, + _get_atlas_values, + _get_mri_info_data, + get_mni_fiducials, # noqa: F401 + get_volume_labels_from_aseg, + read_freesurfer_lut, ) +from ..bem import ConductorModel, read_bem_surfaces from ..fixes import _get_img_fdata +from ..parallel import parallel_func from ..surface import ( - read_surface, + _CheckInside, + _compute_nearest, _create_surf_spacing, _get_ico_surface, - _tessellate_sphere_surf, _get_surf_neighbors, _normalize_vectors, + _tessellate_sphere_surf, _triangle_neighbors, - mesh_dist, complete_surface_info, - _compute_nearest, fast_cross_3d, - _CheckInside, + mesh_dist, + read_surface, ) -from ..viz import plot_alignment - -# Remove get_mni_fiducials in 1.6 (deprecated) -from .._freesurfer import ( - _get_mri_info_data, - _get_atlas_values, - read_freesurfer_lut, - get_mni_fiducials, # noqa: F401 - get_volume_labels_from_aseg, - _check_mri, +from ..transforms import ( + Transform, + _coord_frame_name, + _ensure_trans, + _get_trans, + _print_coord_trans, + _str_to_frame, + apply_trans, + combine_transforms, + invert_transform, ) from ..utils import ( - get_subjects_dir, - check_fname, - logger, - verbose, - fill_doc, - _ensure_int, - _get_call_line, - warn, - object_size, - sizeof_fmt, _check_fname, - _path_like, + _check_option, _check_sphere, + _ensure_int, + _get_call_line, _import_nibabel, - _validate_type, - _check_option, _is_numeric, + _path_like, _pl, _suggest, + _validate_type, + check_fname, + fill_doc, + get_subjects_dir, + logger, + object_size, + sizeof_fmt, + verbose, + warn, ) -from ..parallel import parallel_func -from ..transforms import ( - invert_transform, - apply_trans, - _print_coord_trans, - combine_transforms, - _get_trans, - _coord_frame_name, - Transform, - _str_to_frame, - _ensure_trans, -) - +from ..viz import plot_alignment _src_kind_dict = { "vol": "volume", @@ -3093,11 +3092,11 @@ def _compare_source_spaces(src0, src1, mode="exact", nearest=True, dist_tol=1.5e Note: this function is also used by forward/tests/test_make_forward.py """ from numpy.testing import ( + assert_, assert_allclose, assert_array_equal, - assert_equal, - assert_, assert_array_less, + assert_equal, ) if mode != "exact" and "approx" not in mode: # 'nointerp' can be appended diff --git a/mne/source_space/tests/test_source_space.py b/mne/source_space/tests/test_source_space.py index b29751aa08f..a0fe8dde4a1 100644 --- a/mne/source_space/tests/test_source_space.py +++ b/mne/source_space/tests/test_source_space.py @@ -6,44 +6,45 @@ from pathlib import Path from shutil import copytree -import pytest import numpy as np +import pytest from numpy.testing import ( - assert_array_equal, assert_allclose, - assert_equal, + assert_array_equal, assert_array_less, + assert_equal, ) -from mne.datasets import testing + import mne from mne import ( - read_source_spaces, - write_source_spaces, - setup_source_space, - setup_volume_source_space, - add_source_space_distances, - read_bem_surfaces, - morph_source_spaces, SourceEstimate, - make_sphere_model, + add_source_space_distances, compute_source_morph, + get_volume_labels_from_src, + make_sphere_model, + morph_source_spaces, pick_types, read_bem_solution, + read_bem_surfaces, read_freesurfer_lut, + read_source_spaces, read_trans, - get_volume_labels_from_src, + setup_source_space, + setup_volume_source_space, + write_source_spaces, ) +from mne._fiff.constants import FIFF +from mne._fiff.pick import _picks_to_idx +from mne.datasets import testing from mne.fixes import _get_img_fdata -from mne.utils import run_subprocess, _record_warnings, requires_mne -from mne.surface import _accumulate_normals, _triangle_neighbors from mne.source_estimate import _get_src_type -from mne.source_space._source_space import _compare_source_spaces from mne.source_space import ( - get_decimated_surfaces, compute_distance_to_sensors, + get_decimated_surfaces, ) -from mne._fiff.pick import _picks_to_idx -from mne._fiff.constants import FIFF +from mne.source_space._source_space import _compare_source_spaces +from mne.surface import _accumulate_normals, _triangle_neighbors +from mne.utils import _record_warnings, requires_mne, run_subprocess data_path = testing.data_path(download=False) subjects_dir = data_path / "subjects" diff --git a/mne/stats/__init__.pyi b/mne/stats/__init__.pyi index ac47d6c3680..5d15a0462be 100644 --- a/mne/stats/__init__.pyi +++ b/mne/stats/__init__.pyi @@ -20,23 +20,23 @@ __all__ = [ "ttest_1samp_no_p", "ttest_ind_no_p", ] +from ._adjacency import combine_adjacency +from .cluster_level import ( + _st_mask_from_s_inds, + permutation_cluster_1samp_test, + permutation_cluster_test, + spatio_temporal_cluster_1samp_test, + spatio_temporal_cluster_test, + summarize_clusters_stc, +) +from .multi_comp import bonferroni_correction, fdr_correction from .parametric import ( - f_threshold_mway_rm, + _parametric_ci, f_mway_rm, f_oneway, - _parametric_ci, + f_threshold_mway_rm, ttest_1samp_no_p, ttest_ind_no_p, ) -from .permutations import permutation_t_test, _ci, bootstrap_confidence_interval -from .cluster_level import ( - permutation_cluster_test, - permutation_cluster_1samp_test, - spatio_temporal_cluster_test, - spatio_temporal_cluster_1samp_test, - _st_mask_from_s_inds, - summarize_clusters_stc, -) -from .multi_comp import fdr_correction, bonferroni_correction +from .permutations import _ci, bootstrap_confidence_interval, permutation_t_test from .regression import linear_regression, linear_regression_raw -from ._adjacency import combine_adjacency diff --git a/mne/stats/_adjacency.py b/mne/stats/_adjacency.py index ccae2062a29..516733c8aed 100644 --- a/mne/stats/_adjacency.py +++ b/mne/stats/_adjacency.py @@ -6,7 +6,7 @@ import numpy as np from scipy import sparse -from ..utils import _validate_type, _check_option +from ..utils import _check_option, _validate_type from ..utils.check import int_like diff --git a/mne/stats/cluster_level.py b/mne/stats/cluster_level.py index 94159c351e6..d428df8cef8 100644 --- a/mne/stats/cluster_level.py +++ b/mne/stats/cluster_level.py @@ -12,25 +12,25 @@ import numpy as np from scipy import ndimage, sparse from scipy.sparse.csgraph import connected_components -from scipy.stats import t as tstat, f as fstat +from scipy.stats import f as fstat +from scipy.stats import t as tstat - -from .parametric import f_oneway, ttest_1samp_no_p +from ..fixes import has_numba, jit from ..parallel import parallel_func -from ..fixes import jit, has_numba +from ..source_estimate import MixedSourceEstimate, SourceEstimate, VolSourceEstimate +from ..source_space import SourceSpaces from ..utils import ( - split_list, - logger, - verbose, ProgressBar, - warn, - _pl, - check_random_state, _check_option, + _pl, _validate_type, + check_random_state, + logger, + split_list, + verbose, + warn, ) -from ..source_estimate import SourceEstimate, VolSourceEstimate, MixedSourceEstimate -from ..source_space import SourceSpaces +from .parametric import f_oneway, ttest_1samp_no_p def _get_buddies_fallback(r, s, neighbors, indices=None): diff --git a/mne/stats/parametric.py b/mne/stats/parametric.py index 6de66fe6dc0..68e424e1a2f 100644 --- a/mne/stats/parametric.py +++ b/mne/stats/parametric.py @@ -8,8 +8,8 @@ from string import ascii_uppercase import numpy as np -from scipy.signal import detrend from scipy import stats +from scipy.signal import detrend from ..utils import _check_option diff --git a/mne/stats/permutations.py b/mne/stats/permutations.py index 53a75686102..ac983208670 100644 --- a/mne/stats/permutations.py +++ b/mne/stats/permutations.py @@ -5,10 +5,11 @@ # License: Simplified BSD from math import sqrt + import numpy as np -from ..utils import check_random_state, verbose, logger from ..parallel import parallel_func +from ..utils import check_random_state, logger, verbose def _max_stat(X, X2, perms, dof_scaling): diff --git a/mne/stats/regression.py b/mne/stats/regression.py index d32853f1420..e005832824b 100644 --- a/mne/stats/regression.py +++ b/mne/stats/regression.py @@ -6,17 +6,17 @@ # # License: BSD-3-Clause -from inspect import isgenerator from collections import namedtuple +from inspect import isgenerator import numpy as np from scipy import linalg, sparse, stats -from ..source_estimate import SourceEstimate +from .._fiff.pick import _picks_to_idx, pick_info, pick_types from ..epochs import BaseEpochs from ..evoked import Evoked, EvokedArray -from ..utils import logger, _reject_data_segments, warn, fill_doc -from .._fiff.pick import pick_types, pick_info, _picks_to_idx +from ..source_estimate import SourceEstimate +from ..utils import _reject_data_segments, fill_doc, logger, warn def linear_regression(inst, design_matrix, names=None): diff --git a/mne/stats/tests/test_adjacency.py b/mne/stats/tests/test_adjacency.py index c0f4a08a001..9c3ec8a3133 100644 --- a/mne/stats/tests/test_adjacency.py +++ b/mne/stats/tests/test_adjacency.py @@ -2,8 +2,8 @@ # # License: Simplified BSD -import pytest import numpy as np +import pytest from numpy.testing import assert_array_equal from mne.stats import combine_adjacency diff --git a/mne/stats/tests/test_cluster_level.py b/mne/stats/tests/test_cluster_level.py index 5623c706bbc..7f185042329 100644 --- a/mne/stats/tests/test_cluster_level.py +++ b/mne/stats/tests/test_cluster_level.py @@ -3,32 +3,31 @@ # # License: BSD-3-Clause -from functools import partial import os +from functools import partial import numpy as np -from scipy import sparse, linalg, stats +import pytest from numpy.testing import ( - assert_equal, - assert_array_equal, - assert_array_almost_equal, assert_allclose, + assert_array_almost_equal, + assert_array_equal, + assert_equal, ) -import pytest +from scipy import linalg, sparse, stats -from mne import SourceEstimate, VolSourceEstimate, MixedSourceEstimate, SourceSpaces -from mne.stats import ttest_ind_no_p, combine_adjacency +from mne import MixedSourceEstimate, SourceEstimate, SourceSpaces, VolSourceEstimate +from mne.stats import combine_adjacency, ttest_ind_no_p from mne.stats.cluster_level import ( - permutation_cluster_test, f_oneway, permutation_cluster_1samp_test, - spatio_temporal_cluster_test, + permutation_cluster_test, spatio_temporal_cluster_1samp_test, - ttest_1samp_no_p, + spatio_temporal_cluster_test, summarize_clusters_stc, + ttest_1samp_no_p, ) -from mne.utils import catch_logging, _record_warnings - +from mne.utils import _record_warnings, catch_logging n_space = 50 diff --git a/mne/stats/tests/test_multi_comp.py b/mne/stats/tests/test_multi_comp.py index 84470e0fdf1..6bd2edd4f2a 100644 --- a/mne/stats/tests/test_multi_comp.py +++ b/mne/stats/tests/test_multi_comp.py @@ -1,9 +1,9 @@ import numpy as np -from numpy.testing import assert_almost_equal, assert_allclose, assert_array_equal -from scipy import stats import pytest +from numpy.testing import assert_allclose, assert_almost_equal, assert_array_equal +from scipy import stats -from mne.stats import fdr_correction, bonferroni_correction +from mne.stats import bonferroni_correction, fdr_correction def test_bonferroni_pval_clip(): diff --git a/mne/stats/tests/test_parametric.py b/mne/stats/tests/test_parametric.py index c945626b22e..698a4b9271e 100644 --- a/mne/stats/tests/test_parametric.py +++ b/mne/stats/tests/test_parametric.py @@ -1,13 +1,13 @@ from functools import partial from itertools import product -import pytest -from numpy.testing import assert_array_almost_equal, assert_allclose, assert_array_less import numpy as np +import pytest import scipy.stats +from numpy.testing import assert_allclose, assert_array_almost_equal, assert_array_less import mne -from mne.stats.parametric import f_mway_rm, f_threshold_mway_rm, _map_effects +from mne.stats.parametric import _map_effects, f_mway_rm, f_threshold_mway_rm # hardcoded external test results, manually transferred test_external = { diff --git a/mne/stats/tests/test_permutations.py b/mne/stats/tests/test_permutations.py index 245ac140182..891b0fa8529 100644 --- a/mne/stats/tests/test_permutations.py +++ b/mne/stats/tests/test_permutations.py @@ -2,16 +2,16 @@ # # License: BSD-3-Clause -from numpy.testing import assert_array_equal, assert_allclose import numpy as np -from scipy import stats, sparse import pytest +from numpy.testing import assert_allclose, assert_array_equal +from scipy import sparse, stats from mne.stats import permutation_cluster_1samp_test from mne.stats.permutations import ( - permutation_t_test, _ci, bootstrap_confidence_interval, + permutation_t_test, ) diff --git a/mne/stats/tests/test_regression.py b/mne/stats/tests/test_regression.py index 1e9f8f8384f..190e3ceff87 100644 --- a/mne/stats/tests/test_regression.py +++ b/mne/stats/tests/test_regression.py @@ -5,16 +5,15 @@ # License: BSD-3-Clause import numpy as np -from numpy.testing import assert_array_equal, assert_allclose, assert_equal import pytest - +from numpy.testing import assert_allclose, assert_array_equal, assert_equal from scipy.signal.windows import hann import mne from mne import read_source_estimate from mne.datasets import testing -from mne.stats.regression import linear_regression, linear_regression_raw from mne.io import RawArray +from mne.stats.regression import linear_regression, linear_regression_raw data_path = testing.data_path(download=False) stc_fname = data_path / "MEG" / "sample" / "sample_audvis_trunc-meg-lh.stc" diff --git a/mne/surface.py b/mne/surface.py index 1f75a764aaf..b042361305a 100644 --- a/mne/surface.py +++ b/mne/surface.py @@ -8,13 +8,13 @@ # Many of the computations in this code were derived from Matti Hämäläinen's # C code. -from copy import deepcopy -from functools import partial, lru_cache +import time +import warnings from collections import OrderedDict +from copy import deepcopy +from functools import lru_cache, partial from glob import glob from os import path as op -import time -import warnings import numpy as np from scipy.ndimage import binary_dilation @@ -22,37 +22,36 @@ from scipy.spatial import ConvexHull, Delaunay from scipy.spatial.distance import cdist -from .fixes import jit, prange, bincount from ._fiff.constants import FIFF from ._fiff.pick import pick_types +from .fixes import bincount, jit, prange from .parallel import parallel_func from .transforms import ( - transform_surface_to, - _pol_to_cart, + Transform, _cart_to_sph, _get_trans, + _pol_to_cart, apply_trans, - Transform, + transform_surface_to, ) from .utils import ( - logger, - verbose, - get_subjects_dir, - warn, _check_fname, + _check_freesurfer_home, _check_option, _ensure_int, - _TempDir, - run_subprocess, - _check_freesurfer_home, _hashable_ndarray, - fill_doc, - _validate_type, - _pl, _import_nibabel, + _pl, + _TempDir, + _validate_type, + fill_doc, + get_subjects_dir, + logger, + run_subprocess, + verbose, + warn, ) - ############################################################################### # AUTOMATED SURFACE FINDING @@ -181,7 +180,7 @@ def get_meg_helmet_surf(info, trans=None, verbose=None): A built-in helmet is loaded if possible. If not, a helmet surface will be approximated based on the sensor locations. """ - from .bem import read_bem_surfaces, _fit_sphere + from .bem import _fit_sphere, read_bem_surfaces from .channels.channels import _get_meg_system system, have_helmet = _get_meg_system(info) @@ -1332,8 +1331,8 @@ def _decimate_surface_vtk(points, triangles, n_triangles): """Aux function.""" try: from vtkmodules.util.numpy_support import numpy_to_vtk, numpy_to_vtkIdTypeArray - from vtkmodules.vtkCommonDataModel import vtkPolyData, vtkCellArray from vtkmodules.vtkCommonCore import vtkPoints + from vtkmodules.vtkCommonDataModel import vtkCellArray, vtkPolyData from vtkmodules.vtkFiltersCore import vtkQuadricDecimation except ImportError: raise ValueError("This function requires the VTK package to be " "installed") @@ -1882,14 +1881,14 @@ def _marching_cubes(image, level, smooth=0, fill_hole_size=None, use_flying_edge # Also vtkDiscreteFlyingEdges3D should be faster. # If we ever want not-discrete (continuous/float) marching cubes, # we should probably use vtkFlyingEdges3D rather than vtkMarchingCubes. - from vtkmodules.vtkCommonDataModel import vtkImageData, vtkDataSetAttributes + from vtkmodules.util.numpy_support import numpy_to_vtk, vtk_to_numpy + from vtkmodules.vtkCommonDataModel import vtkDataSetAttributes, vtkImageData from vtkmodules.vtkFiltersCore import vtkThreshold from vtkmodules.vtkFiltersGeneral import ( vtkDiscreteFlyingEdges3D, vtkDiscreteMarchingCubes, ) from vtkmodules.vtkFiltersGeometry import vtkGeometryFilter - from vtkmodules.util.numpy_support import vtk_to_numpy, numpy_to_vtk if image.ndim != 3: raise ValueError(f"3D data must be supplied, got {image.shape}") @@ -2033,8 +2032,8 @@ def get_montage_volume_labels( colors : dict The Freesurfer lookup table colors for the labels. """ + from ._freesurfer import _get_aseg, read_freesurfer_lut from .channels import DigMontage - from ._freesurfer import read_freesurfer_lut, _get_aseg _validate_type(montage, DigMontage, "montage") _validate_type(dist, (int, float), "dist") diff --git a/mne/tests/test_annotations.py b/mne/tests/test_annotations.py index be1082575a9..10325544fcc 100644 --- a/mne/tests/test_annotations.py +++ b/mne/tests/test_annotations.py @@ -5,46 +5,46 @@ import sys from collections import OrderedDict -from datetime import datetime, timezone, timedelta +from datetime import datetime, timedelta, timezone from itertools import repeat from pathlib import Path +import numpy as np import pytest -from pytest import approx from numpy.testing import ( - assert_equal, - assert_array_equal, - assert_array_almost_equal, assert_allclose, + assert_array_almost_equal, + assert_array_equal, + assert_equal, ) - -import numpy as np +from pytest import approx import mne from mne import ( - create_info, - read_annotations, + Annotations, + Epochs, annotations_from_events, - events_from_annotations, count_annotations, + create_info, + events_from_annotations, + read_annotations, ) -from mne import Epochs, Annotations -from mne.utils import ( - catch_logging, - assert_and_remove_boundary_annot, - _raw_annot, - _dt_to_stamp, - _stamp_to_dt, - check_version, - _record_warnings, -) -from mne.io import read_raw_fif, RawArray, concatenate_raws from mne.annotations import ( - _sync_onset, _handle_meas_date, _read_annotations_txt_parse_header, + _sync_onset, ) from mne.datasets import testing +from mne.io import RawArray, concatenate_raws, read_raw_fif +from mne.utils import ( + _dt_to_stamp, + _raw_annot, + _record_warnings, + _stamp_to_dt, + assert_and_remove_boundary_annot, + catch_logging, + check_version, +) data_path = testing.data_path(download=False) data_dir = data_path / "MEG" / "sample" diff --git a/mne/tests/test_bem.py b/mne/tests/test_bem.py index e99bbda260e..82c40553622 100644 --- a/mne/tests/test_bem.py +++ b/mne/tests/test_bem.py @@ -10,40 +10,40 @@ import numpy as np import pytest -from numpy.testing import assert_equal, assert_allclose +from numpy.testing import assert_allclose, assert_equal import mne from mne import ( + Info, + Transform, make_bem_model, - read_bem_surfaces, - write_bem_surfaces, make_bem_solution, + make_sphere_model, read_bem_solution, + read_bem_surfaces, write_bem_solution, - make_sphere_model, - Transform, - Info, - write_surface, + write_bem_surfaces, write_head_bem, + write_surface, ) -from mne.preprocessing.maxfilter import fit_sphere_to_headshape from mne._fiff.constants import FIFF -from mne.transforms import translation -from mne.datasets import testing -from mne.utils import catch_logging, check_version from mne.bem import ( - _ico_downsample, - _get_ico_map, - _order_surfaces, _assert_complete_surface, _assert_inside, - _check_surface_size, _bem_find_surface, - make_scalp_surfaces, + _check_surface_size, + _get_ico_map, + _ico_downsample, + _order_surfaces, distance_to_bem, + make_scalp_surfaces, ) -from mne.surface import read_surface, _get_ico_surface +from mne.datasets import testing from mne.io import read_info +from mne.preprocessing.maxfilter import fit_sphere_to_headshape +from mne.surface import _get_ico_surface, read_surface +from mne.transforms import translation +from mne.utils import catch_logging, check_version fname_raw = Path(__file__).parent.parent / "io" / "tests" / "data" / "test_raw.fif" subjects_dir = testing.data_path(download=False) / "subjects" diff --git a/mne/tests/test_chpi.py b/mne/tests/test_chpi.py index 1e9b249ce02..21145445ed9 100644 --- a/mne/tests/test_chpi.py +++ b/mne/tests/test_chpi.py @@ -5,44 +5,44 @@ from pathlib import Path import numpy as np -from numpy.testing import assert_allclose, assert_array_less, assert_array_equal +import pytest +from numpy.testing import assert_allclose, assert_array_equal, assert_array_less from scipy.interpolate import interp1d from scipy.spatial.distance import cdist -import pytest -from mne import pick_types, pick_info -from mne.forward._compute_forward import _MAG_FACTOR -from mne.io import ( - read_raw_fif, - read_raw_artemis123, - read_raw_ctf, - read_info, - RawArray, - read_raw_kit, -) +from mne import pick_info, pick_types from mne._fiff.constants import FIFF from mne.chpi import ( + _chpi_locs_to_times_dig, + _compute_good_distances, + _get_hpi_initial_fit, + _setup_ext_proj, compute_chpi_amplitudes, compute_chpi_locs, compute_chpi_snr, compute_head_pos, - _setup_ext_proj, - _chpi_locs_to_times_dig, - _compute_good_distances, extract_chpi_locs_ctf, - head_pos_to_trans_rot_t, - read_head_pos, - write_head_pos, + extract_chpi_locs_kit, filter_chpi, get_active_chpi, get_chpi_info, - _get_hpi_initial_fit, - extract_chpi_locs_kit, + head_pos_to_trans_rot_t, + read_head_pos, + write_head_pos, ) from mne.datasets import testing +from mne.forward._compute_forward import _MAG_FACTOR +from mne.io import ( + RawArray, + read_info, + read_raw_artemis123, + read_raw_ctf, + read_raw_fif, + read_raw_kit, +) from mne.simulation import add_chpi -from mne.transforms import rot_to_quat, _angle_between_quats -from mne.utils import catch_logging, assert_meg_snr, verbose, object_diff +from mne.transforms import _angle_between_quats, rot_to_quat +from mne.utils import assert_meg_snr, catch_logging, object_diff, verbose from mne.viz import plot_head_positions base_dir = Path(__file__).parent.parent / "io" / "tests" / "data" diff --git a/mne/tests/test_coreg.py b/mne/tests/test_coreg.py index fa80eff9dd7..b106b14c4b2 100644 --- a/mne/tests/test_coreg.py +++ b/mne/tests/test_coreg.py @@ -3,44 +3,44 @@ from glob import glob from shutil import copyfile -import pytest import numpy as np +import pytest from numpy.testing import ( - assert_array_almost_equal, assert_allclose, + assert_array_almost_equal, assert_array_equal, assert_array_less, ) import mne +from mne._fiff.constants import FIFF +from mne.channels import DigMontage +from mne.coreg import ( + Coregistration, + _is_mri_subject, + coregister_fiducials, + create_default_subject, + fit_matched_points, + get_mni_fiducials, + scale_labels, + scale_mri, + scale_source_space, +) from mne.datasets import testing +from mne.io import read_fiducials, read_info +from mne.source_space import write_source_spaces from mne.transforms import ( Transform, + _angle_between_quats, apply_trans, - rotation, - translation, - scaling, + invert_transform, read_trans, - _angle_between_quats, rot_to_quat, - invert_transform, -) -from mne.coreg import ( - fit_matched_points, - create_default_subject, - scale_mri, - _is_mri_subject, - scale_labels, - scale_source_space, - coregister_fiducials, - get_mni_fiducials, - Coregistration, + rotation, + scaling, + translation, ) -from mne.io import read_fiducials, read_info -from mne._fiff.constants import FIFF from mne.utils import catch_logging -from mne.source_space import write_source_spaces -from mne.channels import DigMontage data_path = testing.data_path(download=False) subjects_dir = data_path / "subjects" diff --git a/mne/tests/test_cov.py b/mne/tests/test_cov.py index 2a87f94d666..f7dd2ffce9e 100644 --- a/mne/tests/test_cov.py +++ b/mne/tests/test_cov.py @@ -8,50 +8,49 @@ from inspect import signature from pathlib import Path +import numpy as np +import pytest from numpy.testing import ( + assert_allclose, assert_array_almost_equal, assert_array_equal, assert_equal, - assert_allclose, -) -import pytest -import numpy as np - -from mne.cov import ( - regularize, - whiten_evoked, - _auto_low_rank_model, - prepare_noise_cov, - compute_whitener, - _regularized_covariance, ) from mne import ( - read_cov, - write_cov, Epochs, - merge_events, - find_events, - compute_raw_covariance, compute_covariance, - read_evokeds, compute_proj_raw, - pick_channels_cov, - pick_types, + compute_rank, + compute_raw_covariance, + create_info, + find_events, make_ad_hoc_cov, make_fixed_length_events, - create_info, + merge_events, + pick_channels_cov, pick_info, - compute_rank, + pick_types, + read_cov, + read_evokeds, + write_cov, ) +from mne._fiff.pick import _DATA_CH_TYPES_SPLIT from mne.channels import equalize_channels +from mne.cov import ( + _auto_low_rank_model, + _regularized_covariance, + compute_whitener, + prepare_noise_cov, + regularize, + whiten_evoked, +) from mne.datasets import testing from mne.fixes import _safe_svd -from mne.io import read_raw_fif, RawArray, read_raw_ctf, read_info -from mne._fiff.pick import _DATA_CH_TYPES_SPLIT +from mne.io import RawArray, read_info, read_raw_ctf, read_raw_fif from mne.preprocessing import maxwell_filter from mne.rank import _compute_rank_int -from mne.utils import catch_logging, assert_snr, _record_warnings +from mne.utils import _record_warnings, assert_snr, catch_logging base_dir = Path(__file__).parent.parent / "io" / "tests" / "data" cov_fname = base_dir / "test-cov.fif" diff --git a/mne/tests/test_defaults.py b/mne/tests/test_defaults.py index 58d5979fff3..235f8457623 100644 --- a/mne/tests/test_defaults.py +++ b/mne/tests/test_defaults.py @@ -2,6 +2,7 @@ import pytest from numpy.testing import assert_allclose + from mne.defaults import _handle_default from mne.io.base import _get_scaling diff --git a/mne/tests/test_dipole.py b/mne/tests/test_dipole.py index f5750a2bd44..26cde7a3ea4 100644 --- a/mne/tests/test_dipole.py +++ b/mne/tests/test_dipole.py @@ -4,47 +4,45 @@ import os -import numpy as np -from numpy.testing import assert_allclose, assert_array_equal, assert_array_less import matplotlib.pyplot as plt +import numpy as np import pytest +from numpy.testing import assert_allclose, assert_array_equal, assert_array_less from mne import ( - read_dipole, - read_forward_solution, - convert_forward_solution, - read_evokeds, - read_cov, - SourceEstimate, - write_evokeds, - fit_dipole, - transform_surface_to, - make_sphere_model, - pick_types, - pick_info, - EvokedArray, - read_source_spaces, - make_ad_hoc_cov, - make_forward_solution, Dipole, DipoleFixed, Epochs, - make_fixed_length_events, Evoked, + EvokedArray, + SourceEstimate, + convert_forward_solution, + fit_dipole, head_to_mni, + make_ad_hoc_cov, + make_fixed_length_events, + make_forward_solution, + make_sphere_model, + pick_info, + pick_types, + read_cov, + read_dipole, + read_evokeds, + read_forward_solution, + read_source_spaces, + transform_surface_to, + write_evokeds, ) -from mne.dipole import get_phantom_dipoles, _BDIP_ERROR_KEYS -from mne.simulation import simulate_evoked +from mne._fiff.constants import FIFF +from mne.bem import _bem_find_surface, read_bem_solution from mne.datasets import testing -from mne.utils import requires_mne, run_subprocess, _record_warnings +from mne.dipole import _BDIP_ERROR_KEYS, get_phantom_dipoles +from mne.io import read_raw_ctf, read_raw_fif from mne.proj import make_eeg_average_ref_proj - -from mne.io import read_raw_fif, read_raw_ctf -from mne._fiff.constants import FIFF - +from mne.simulation import simulate_evoked from mne.surface import _compute_nearest -from mne.bem import _bem_find_surface, read_bem_solution -from mne.transforms import apply_trans, _get_trans +from mne.transforms import _get_trans, apply_trans +from mne.utils import _record_warnings, requires_mne, run_subprocess data_path = testing.data_path(download=False) meg_path = data_path / "MEG" / "sample" diff --git a/mne/tests/test_docstring_parameters.py b/mne/tests/test_docstring_parameters.py index d18d9672c48..181a757d0d2 100644 --- a/mne/tests/test_docstring_parameters.py +++ b/mne/tests/test_docstring_parameters.py @@ -4,9 +4,9 @@ import importlib import inspect +import re from pathlib import Path from pkgutil import walk_packages -import re import pytest diff --git a/mne/tests/test_epochs.py b/mne/tests/test_epochs.py index 3a7f0a7451e..9f72be1803a 100644 --- a/mne/tests/test_epochs.py +++ b/mne/tests/test_epochs.py @@ -11,62 +11,62 @@ from io import BytesIO from pathlib import Path +import numpy as np import pytest +import scipy.signal +from numpy.fft import rfft, rfftfreq from numpy.testing import ( - assert_array_equal, - assert_array_almost_equal, assert_allclose, - assert_equal, + assert_array_almost_equal, + assert_array_equal, assert_array_less, + assert_equal, ) -import numpy as np -from numpy.fft import rfft, rfftfreq -import scipy.signal import mne from mne import ( - Epochs, Annotations, - read_events, - pick_events, - read_epochs, + Epochs, + combine_evoked, + create_info, equalize_channels, - pick_types, + make_fixed_length_epochs, + make_fixed_length_events, pick_channels, + pick_events, + pick_types, + read_epochs, + read_events, read_evokeds, write_evokeds, - create_info, - make_fixed_length_events, - make_fixed_length_epochs, - combine_evoked, ) +from mne._fiff.constants import FIFF +from mne._fiff.proj import _has_eeg_average_ref_proj +from mne._fiff.write import INT32_MAX, _get_split_size, write_float, write_int from mne.annotations import _handle_meas_date from mne.baseline import rescale +from mne.chpi import head_pos_to_trans_rot_t, read_head_pos from mne.datasets import testing -from mne.chpi import read_head_pos, head_pos_to_trans_rot_t -from mne.event import merge_events -from mne.io import RawArray, read_raw_fif -from mne._fiff.constants import FIFF -from mne._fiff.proj import _has_eeg_average_ref_proj -from mne._fiff.write import write_int, INT32_MAX, _get_split_size, write_float -from mne.preprocessing import maxwell_filter from mne.epochs import ( + BaseEpochs, + EpochsArray, + _handle_event_repeated, + average_movements, bootstrap, - equalize_epoch_counts, combine_event_ids, - EpochsArray, concatenate_epochs, - BaseEpochs, - average_movements, - _handle_event_repeated, + equalize_epoch_counts, make_metadata, ) +from mne.event import merge_events +from mne.io import RawArray, read_raw_fif +from mne.preprocessing import maxwell_filter from mne.utils import ( + _dt_to_stamp, + assert_meg_snr, + catch_logging, object_diff, use_log_level, - catch_logging, - assert_meg_snr, - _dt_to_stamp, ) data_path = testing.data_path(download=False) diff --git a/mne/tests/test_event.py b/mne/tests/test_event.py index 212bc8a9d98..3f2b8137345 100644 --- a/mne/tests/test_event.py +++ b/mne/tests/test_event.py @@ -6,38 +6,38 @@ from pathlib import Path import numpy as np +import pytest from numpy.testing import ( + assert_allclose, assert_array_almost_equal, assert_array_equal, assert_equal, - assert_allclose, ) -import pytest from mne import ( - read_events, - write_events, - make_fixed_length_events, + Annotations, + Epochs, + compute_raw_covariance, + count_events, + create_info, find_events, - pick_events, find_stim_steps, + make_fixed_length_events, pick_channels, + pick_events, + read_events, read_evokeds, - Epochs, - create_info, - compute_raw_covariance, - Annotations, - count_events, + write_events, ) -from mne.io import read_raw_fif, RawArray +from mne.datasets import testing from mne.event import ( + AcqParserFIF, define_target_events, + match_event_names, merge_events, - AcqParserFIF, shift_time_events, - match_event_names, ) -from mne.datasets import testing +from mne.io import RawArray, read_raw_fif base_dir = Path(__file__).parent.parent / "io" / "tests" / "data" fname = base_dir / "test-eve.fif" diff --git a/mne/tests/test_evoked.py b/mne/tests/test_evoked.py index 040978c9ff9..9820dcdb5e3 100644 --- a/mne/tests/test_evoked.py +++ b/mne/tests/test_evoked.py @@ -10,29 +10,29 @@ from pathlib import Path import numpy as np -from scipy import fftpack +import pytest from numpy.testing import ( + assert_allclose, assert_array_almost_equal, - assert_equal, assert_array_equal, - assert_allclose, + assert_equal, ) -import pytest +from scipy import fftpack from mne import ( + Epochs, + EpochsArray, + combine_evoked, + create_info, equalize_channels, pick_types, + read_events, read_evokeds, write_evokeds, - combine_evoked, - create_info, - read_events, - Epochs, - EpochsArray, ) -from mne.evoked import _get_peak, Evoked, EvokedArray -from mne.io import read_raw_fif from mne._fiff.constants import FIFF +from mne.evoked import Evoked, EvokedArray, _get_peak +from mne.io import read_raw_fif from mne.utils import grand_average base_dir = Path(__file__).parent.parent / "io" / "tests" / "data" diff --git a/mne/tests/test_filter.py b/mne/tests/test_filter.py index b3770abb7c4..552489f45d1 100644 --- a/mne/tests/test_filter.py +++ b/mne/tests/test_filter.py @@ -1,34 +1,34 @@ import numpy as np +import pytest +from numpy.fft import fft, fftfreq from numpy.testing import ( - assert_array_almost_equal, + assert_allclose, assert_almost_equal, + assert_array_almost_equal, assert_array_equal, - assert_allclose, assert_array_less, ) -import pytest -from scipy.signal import resample as sp_resample, butter, freqz, sosfreqz +from scipy.signal import butter, freqz, sosfreqz +from scipy.signal import resample as sp_resample -from mne import create_info, Epochs -from numpy.fft import fft, fftfreq -from mne.io import RawArray, read_raw_fif +from mne import Epochs, create_info from mne._fiff.pick import _DATA_CH_TYPES_SPLIT from mne.filter import ( - filter_data, - resample, - _resample_stim_channels, - construct_iir_filter, - notch_filter, - detrend, + _length_factors, _overlap_add_filter, + _resample_stim_channels, _smart_pad, + construct_iir_filter, + create_filter, design_mne_c_filter, + detrend, estimate_ringing_samples, - create_filter, - _length_factors, + filter_data, + notch_filter, + resample, ) - -from mne.utils import sum_squared, catch_logging, requires_mne, run_subprocess +from mne.io import RawArray, read_raw_fif +from mne.utils import catch_logging, requires_mne, run_subprocess, sum_squared def test_filter_array(): diff --git a/mne/tests/test_freesurfer.py b/mne/tests/test_freesurfer.py index 30c8e39d5dd..f60993bddf1 100644 --- a/mne/tests/test_freesurfer.py +++ b/mne/tests/test_freesurfer.py @@ -6,20 +6,20 @@ import mne from mne import ( - vertex_to_mni, + get_volume_labels_from_aseg, head_to_mni, - read_talxfm, read_freesurfer_lut, - get_volume_labels_from_aseg, + read_talxfm, + vertex_to_mni, ) -from mne.datasets import testing from mne._freesurfer import ( - _get_mgz_header, _check_subject_dir, - read_lta, _estimate_talxfm_rigid, + _get_mgz_header, + read_lta, ) -from mne.transforms import apply_trans, _get_trans, rot_to_quat, _angle_between_quats +from mne.datasets import testing +from mne.transforms import _angle_between_quats, _get_trans, apply_trans, rot_to_quat data_path = testing.data_path(download=False) subjects_dir = data_path / "subjects" diff --git a/mne/tests/test_import_nesting.py b/mne/tests/test_import_nesting.py index f52a2bf837b..f4fe63d1469 100644 --- a/mne/tests/test_import_nesting.py +++ b/mne/tests/test_import_nesting.py @@ -5,14 +5,14 @@ import ast import glob import os -from pathlib import Path import sys +from pathlib import Path from types import ModuleType + import pytest import mne -from mne.utils import run_subprocess, logger, _pl - +from mne.utils import _pl, logger, run_subprocess # To avoid circular import issues, we have a defined order of submodule # priority. A submodule should nest an import from another submodule if and @@ -141,12 +141,12 @@ def generic_visit(self, node): ("mne/utils/docs.py", " import mne", "non-relative mne import"), ( "mne/io/_read_raw.py", - " from . import read_raw_edf, read_raw_bdf, read_raw_gdf, read_raw_brainvision, read_raw_fif, read_raw_eeglab, read_raw_cnt, read_raw_egi, read_raw_eximia, read_raw_nirx, read_raw_fieldtrip, read_raw_artemis123, read_raw_nicolet, read_raw_kit, read_raw_ctf, read_raw_boxy, read_raw_snirf, read_raw_fil, read_raw_nihon, read_raw_curry, read_raw_nedf", # noqa: E501 + " from . import read_raw_artemis123, read_raw_bdf, read_raw_boxy, read_raw_brainvision, read_raw_cnt, read_raw_ctf, read_raw_curry, read_raw_edf, read_raw_eeglab, read_raw_egi, read_raw_eximia, read_raw_fieldtrip, read_raw_fif, read_raw_fil, read_raw_gdf, read_raw_kit, read_raw_nedf, read_raw_nicolet, read_raw_nihon, read_raw_nirx, read_raw_snirf", # noqa: E501 "non-explicit relative import", ), ( "mne/datasets/utils.py", - " from . import eegbci, sleep_physionet, limo, fetch_fsaverage, fetch_infant_template, fetch_hcp_mmp_parcellation, fetch_phantom", # noqa: E501 + " from . import eegbci, fetch_fsaverage, fetch_hcp_mmp_parcellation, fetch_infant_template, fetch_phantom, limo, sleep_physionet", # noqa: E501 "non-explicit relative import", ), ( diff --git a/mne/tests/test_label.py b/mne/tests/test_label.py index a7126401997..a3b4f74435c 100644 --- a/mne/tests/test_label.py +++ b/mne/tests/test_label.py @@ -10,48 +10,46 @@ from pathlib import Path import numpy as np -from scipy import sparse - +import pytest from numpy.testing import ( - assert_array_equal, - assert_array_almost_equal, - assert_equal, assert_allclose, + assert_array_almost_equal, + assert_array_equal, assert_array_less, + assert_equal, ) -import pytest +from scipy import sparse -from mne.datasets import testing from mne import ( + grow_labels, + labels_to_stc, + morph_labels, + random_parcellation, read_label, - stc_to_label, + read_labels_from_annot, read_source_estimate, read_source_spaces, - grow_labels, - read_labels_from_annot, - write_labels_to_annot, - split_label, - spatial_tris_adjacency, read_surface, - random_parcellation, - morph_labels, - labels_to_stc, + spatial_tris_adjacency, + split_label, + stc_to_label, + write_labels_to_annot, ) +from mne.datasets import testing from mne.label import ( Label, _blend_colors, - label_sign_flip, _load_vert_pos, - select_sources, _n_colors, _read_annot, _read_annot_cands, + label_sign_flip, + select_sources, ) -from mne.source_space import SourceSpaces from mne.source_estimate import mesh_edges +from mne.source_space import SourceSpaces from mne.surface import _mesh_borders -from mne.utils import get_subjects_dir, _record_warnings - +from mne.utils import _record_warnings, get_subjects_dir data_path = testing.data_path(download=False) subjects_dir = data_path / "subjects" diff --git a/mne/tests/test_line_endings.py b/mne/tests/test_line_endings.py index bc5744c4e8f..6dcacf554a2 100644 --- a/mne/tests/test_line_endings.py +++ b/mne/tests/test_line_endings.py @@ -4,14 +4,13 @@ # License: BSD-3-Clause import os -from os import path as op import sys +from os import path as op import pytest from mne.utils import _get_root_dir - skip_files = ( # known crlf "FreeSurferColorLUT.txt", diff --git a/mne/tests/test_morph.py b/mne/tests/test_morph.py index 0dc6ae2e086..f26da4ca6b2 100644 --- a/mne/tests/test_morph.py +++ b/mne/tests/test_morph.py @@ -3,39 +3,40 @@ # License: BSD-3-Clause from inspect import signature -import pytest import numpy as np -from numpy.testing import assert_array_less, assert_allclose, assert_array_equal +import pytest +from numpy.testing import assert_allclose, assert_array_equal, assert_array_less +from scipy.sparse import csr_matrix +from scipy.sparse import eye as speye from scipy.spatial.distance import cdist -from scipy.sparse import csr_matrix, eye as speye import mne from mne import ( SourceEstimate, - VolSourceEstimate, - VectorSourceEstimate, - read_evokeds, SourceMorph, + VectorSourceEstimate, + VolSourceEstimate, + VolVectorSourceEstimate, compute_source_morph, - read_source_morph, - read_source_estimate, - read_forward_solution, + get_volume_labels_from_aseg, grade_to_vertices, - setup_volume_source_space, + make_ad_hoc_cov, make_forward_solution, make_sphere_model, - make_ad_hoc_cov, - VolVectorSourceEstimate, - get_volume_labels_from_aseg, + read_evokeds, + read_forward_solution, read_freesurfer_lut, + read_source_estimate, + read_source_morph, + setup_volume_source_space, ) +from mne._freesurfer import _get_atlas_values, _get_mri_info_data from mne.datasets import testing from mne.fixes import _get_img_fdata -from mne._freesurfer import _get_mri_info_data, _get_atlas_values -from mne.minimum_norm import apply_inverse, read_inverse_operator, make_inverse_operator +from mne.minimum_norm import apply_inverse, make_inverse_operator, read_inverse_operator from mne.source_space._source_space import _add_interpolator, _grid_interp from mne.transforms import quat_to_rot -from mne.utils import catch_logging, _record_warnings +from mne.utils import _record_warnings, catch_logging # Setup paths diff --git a/mne/tests/test_morph_map.py b/mne/tests/test_morph_map.py index 036b7fa17b7..a5dfe563027 100644 --- a/mne/tests/test_morph_map.py +++ b/mne/tests/test_morph_map.py @@ -5,14 +5,14 @@ import os from shutil import copyfile -import pytest import numpy as np +import pytest from numpy.testing import assert_allclose from scipy import sparse -from mne.datasets import testing -from mne.utils import catch_logging, _record_warnings from mne import read_morph_map +from mne.datasets import testing +from mne.utils import _record_warnings, catch_logging data_path = testing.data_path(download=False) subjects_dir = data_path / "subjects" diff --git a/mne/tests/test_ola.py b/mne/tests/test_ola.py index a364c534518..ab8935added 100644 --- a/mne/tests/test_ola.py +++ b/mne/tests/test_ola.py @@ -1,6 +1,6 @@ import numpy as np -from numpy.testing import assert_allclose import pytest +from numpy.testing import assert_allclose from mne._ola import _COLA, _Interp2, _Storer diff --git a/mne/tests/test_parallel.py b/mne/tests/test_parallel.py index 80d60a02f7c..6f90e12de1b 100644 --- a/mne/tests/test_parallel.py +++ b/mne/tests/test_parallel.py @@ -2,9 +2,9 @@ # # License: BSD-3-Clause -from contextlib import nullcontext import multiprocessing import os +from contextlib import nullcontext import pytest diff --git a/mne/tests/test_proj.py b/mne/tests/test_proj.py index 7f4a79ae723..562d4e846f6 100644 --- a/mne/tests/test_proj.py +++ b/mne/tests/test_proj.py @@ -2,40 +2,40 @@ from pathlib import Path import numpy as np -from numpy.testing import assert_array_almost_equal, assert_allclose, assert_equal import pytest +from numpy.testing import assert_allclose, assert_array_almost_equal, assert_equal from scipy import linalg from mne import ( + Epochs, compute_proj_epochs, compute_proj_evoked, compute_proj_raw, - pick_types, - read_events, - Epochs, - sensitivity_map, - read_source_estimate, compute_raw_covariance, + convert_forward_solution, create_info, + pick_types, + read_events, read_forward_solution, - convert_forward_solution, + read_source_estimate, + sensitivity_map, ) -from mne.cov import regularize, compute_whitener -from mne.datasets import testing -from mne.io import read_raw_fif, RawArray from mne._fiff.proj import ( - make_projector, + _EEG_AVREF_PICK_DICT, + _needs_eeg_average_ref_proj, activate_proj, + make_projector, setup_proj, - _needs_eeg_average_ref_proj, - _EEG_AVREF_PICK_DICT, ) +from mne.cov import compute_whitener, regularize +from mne.datasets import testing +from mne.io import RawArray, read_raw_fif from mne.preprocessing import maxwell_filter from mne.proj import ( + _has_eeg_average_ref_proj, + make_eeg_average_ref_proj, read_proj, write_proj, - make_eeg_average_ref_proj, - _has_eeg_average_ref_proj, ) from mne.rank import _compute_rank_int from mne.utils import _record_warnings diff --git a/mne/tests/test_rank.py b/mne/tests/test_rank.py index 4a513975684..f88dd68e282 100644 --- a/mne/tests/test_rank.py +++ b/mne/tests/test_rank.py @@ -1,27 +1,25 @@ import itertools from pathlib import Path -from numpy.testing import assert_array_equal import numpy as np - import pytest +from numpy.testing import assert_array_equal -from mne import read_evokeds, read_cov, compute_raw_covariance, pick_types, pick_info +from mne import compute_raw_covariance, pick_info, pick_types, read_cov, read_evokeds +from mne._fiff.pick import _picks_by_type +from mne._fiff.proj import _has_eeg_average_ref_proj from mne.cov import prepare_noise_cov from mne.datasets import testing from mne.io import read_raw_fif -from mne._fiff.pick import _picks_by_type -from mne._fiff.proj import _has_eeg_average_ref_proj from mne.proj import compute_proj_raw from mne.rank import ( - estimate_rank, - compute_rank, - _get_rank_sss, _compute_rank_int, _estimate_rank_raw, + _get_rank_sss, + compute_rank, + estimate_rank, ) - base_dir = Path(__file__).parent.parent / "io" / "tests" / "data" cov_fname = base_dir / "test-cov.fif" raw_fname = base_dir / "test_raw.fif" diff --git a/mne/tests/test_source_estimate.py b/mne/tests/test_source_estimate.py index 9b78113127c..2bfba7ce5fb 100644 --- a/mne/tests/test_source_estimate.py +++ b/mne/tests/test_source_estimate.py @@ -9,76 +9,75 @@ from shutil import copyfile import numpy as np +import pytest from numpy.fft import fft from numpy.testing import ( + assert_allclose, assert_array_almost_equal, assert_array_equal, - assert_allclose, - assert_equal, assert_array_less, + assert_equal, ) -import pytest from scipy import sparse from scipy.optimize import fmin_cobyla from scipy.spatial.distance import cdist import mne from mne import ( - stats, + Epochs, + EvokedArray, + Label, + MixedSourceEstimate, + MixedVectorSourceEstimate, SourceEstimate, + SourceSpaces, VectorSourceEstimate, VolSourceEstimate, - Label, - read_source_spaces, - read_evokeds, - MixedSourceEstimate, - find_events, - Epochs, - read_source_estimate, + VolVectorSourceEstimate, + compute_source_morph, + convert_forward_solution, extract_label_time_course, - spatio_temporal_tris_adjacency, - stc_near_sensors, - spatio_temporal_src_adjacency, - read_cov, - EvokedArray, - spatial_inter_hemi_adjacency, - read_forward_solution, - spatial_src_adjacency, - spatial_tris_adjacency, + find_events, + labels_to_stc, pick_info, - SourceSpaces, - VolVectorSourceEstimate, - read_trans, pick_types, - MixedVectorSourceEstimate, - setup_volume_source_space, - convert_forward_solution, pick_types_forward, - compute_source_morph, - labels_to_stc, + read_cov, + read_evokeds, + read_forward_solution, + read_source_estimate, + read_source_spaces, + read_trans, scale_mri, + setup_volume_source_space, + spatial_inter_hemi_adjacency, + spatial_src_adjacency, + spatial_tris_adjacency, + spatio_temporal_src_adjacency, + spatio_temporal_tris_adjacency, + stats, + stc_near_sensors, write_source_spaces, ) +from mne._fiff.constants import FIFF from mne.datasets import testing from mne.fixes import _get_img_fdata -from mne.io import read_info -from mne._fiff.constants import FIFF -from mne.morph_map import _make_morph_map_hemi -from mne.source_estimate import grade_to_tris, _get_vol_mask -from mne.source_space._source_space import _get_src_nn -from mne.transforms import apply_trans, invert_transform, transform_surface_to +from mne.io import read_info, read_raw_fif +from mne.label import label_sign_flip, read_labels_from_annot from mne.minimum_norm import ( - read_inverse_operator, apply_inverse, apply_inverse_epochs, make_inverse_operator, + read_inverse_operator, ) -from mne.label import read_labels_from_annot, label_sign_flip +from mne.morph_map import _make_morph_map_hemi +from mne.source_estimate import _get_vol_mask, grade_to_tris +from mne.source_space._source_space import _get_src_nn +from mne.transforms import apply_trans, invert_transform, transform_surface_to from mne.utils import ( - catch_logging, _record_warnings, + catch_logging, ) -from mne.io import read_raw_fif data_path = testing.data_path(download=False) subjects_dir = data_path / "subjects" diff --git a/mne/tests/test_surface.py b/mne/tests/test_surface.py index 066632904d6..60b9fed5a17 100644 --- a/mne/tests/test_surface.py +++ b/mne/tests/test_surface.py @@ -4,38 +4,38 @@ from pathlib import Path -import pytest import numpy as np -from numpy.testing import assert_array_equal, assert_allclose, assert_equal +import pytest +from numpy.testing import assert_allclose, assert_array_equal, assert_equal from mne import ( - read_surface, - write_surface, decimate_surface, - pick_types, dig_mri_distances, get_montage_volume_labels, + pick_types, + read_surface, + write_surface, ) +from mne._fiff.constants import FIFF from mne.channels import make_dig_montage from mne.datasets import testing from mne.io import read_info -from mne._fiff.constants import FIFF from mne.surface import ( _compute_nearest, + _get_ico_surface, + _marching_cubes, + _normal_orth, + _project_onto_surface, + _read_patch, _tessellate_sphere, + _voxel_neighbors, fast_cross_3d, get_head_surf, - read_curvature, get_meg_helmet_surf, - _normal_orth, - _read_patch, - _marching_cubes, - _voxel_neighbors, - _project_onto_surface, - _get_ico_surface, + read_curvature, ) from mne.transforms import _get_trans -from mne.utils import catch_logging, object_diff, requires_freesurfer, _record_warnings +from mne.utils import _record_warnings, catch_logging, object_diff, requires_freesurfer data_path = testing.data_path(download=False) subjects_dir = data_path / "subjects" diff --git a/mne/tests/test_transforms.py b/mne/tests/test_transforms.py index 4a533d9bf38..a09071d571b 100644 --- a/mne/tests/test_transforms.py +++ b/mne/tests/test_transforms.py @@ -6,52 +6,54 @@ import os from pathlib import Path -import pytest import numpy as np +import pytest from numpy.testing import ( - assert_array_equal, - assert_equal, assert_allclose, - assert_array_less, assert_almost_equal, + assert_array_equal, + assert_array_less, + assert_equal, ) import mne +from mne import read_trans, write_trans from mne.datasets import testing from mne.fixes import _get_img_fdata -from mne import read_trans, write_trans from mne.io import read_info from mne.transforms import ( - invert_transform, - _get_trans, - rotation, - rotation3d, - rotation_angles, + _angle_between_quats, + _average_quats, + _cart_to_sph, + _compute_r2, + _euler_to_quat, _find_trans, - combine_transforms, + _find_vector_rotation, + _fit_matched_points, + _get_trans, + _pol_to_cart, + _quat_real, + _quat_to_affine, + _quat_to_euler, + _read_fs_xfm, + _sph_to_cart, + _topo_to_sph, + _validate_pipeline, + _write_fs_xfm, apply_trans, - translation, + combine_transforms, get_ras_to_neuromag_trans, - _pol_to_cart, + invert_transform, quat_to_rot, rot_to_quat, - _angle_between_quats, - _find_vector_rotation, - _sph_to_cart, - _cart_to_sph, - _topo_to_sph, - _average_quats, - _SphericalSurfaceWarp as SphericalSurfaceWarp, + rotation, + rotation3d, rotation3d_align_z_axis, - _read_fs_xfm, - _write_fs_xfm, - _quat_real, - _fit_matched_points, - _quat_to_euler, - _euler_to_quat, - _quat_to_affine, - _compute_r2, - _validate_pipeline, + rotation_angles, + translation, +) +from mne.transforms import ( + _SphericalSurfaceWarp as SphericalSurfaceWarp, ) data_path = testing.data_path(download=False) diff --git a/mne/time_frequency/__init__.pyi b/mne/time_frequency/__init__.pyi index 258fa5ef96c..9fc0c271cc4 100644 --- a/mne/time_frequency/__init__.pyi +++ b/mne/time_frequency/__init__.pyi @@ -60,9 +60,9 @@ from .spectrum import ( read_spectrum, ) from .tfr import ( - _BaseTFR, AverageTFR, EpochsTFR, + _BaseTFR, fwhm, morlet, read_tfrs, diff --git a/mne/time_frequency/_stockwell.py b/mne/time_frequency/_stockwell.py index 6196ae59c92..e8c94be08ad 100644 --- a/mne/time_frequency/_stockwell.py +++ b/mne/time_frequency/_stockwell.py @@ -9,8 +9,8 @@ from scipy.fft import fft, fftfreq, ifft from .._fiff.pick import _pick_data_channels, pick_info -from ..utils import verbose, logger, fill_doc, _validate_type from ..parallel import parallel_func +from ..utils import _validate_type, fill_doc, logger, verbose from .tfr import AverageTFR, _get_data diff --git a/mne/time_frequency/ar.py b/mne/time_frequency/ar.py index ab3110f23d2..0baccac31f0 100644 --- a/mne/time_frequency/ar.py +++ b/mne/time_frequency/ar.py @@ -6,9 +6,9 @@ import numpy as np from scipy import linalg +from .._fiff.pick import _picks_by_type, _picks_to_idx, pick_info from ..defaults import _handle_default -from .._fiff.pick import _picks_to_idx, _picks_by_type, pick_info -from ..utils import verbose, _apply_scaling_array +from ..utils import _apply_scaling_array, verbose def _yule_walker(X, order=1): diff --git a/mne/time_frequency/csd.py b/mne/time_frequency/csd.py index 1adf684950e..78ff9d95bcc 100644 --- a/mne/time_frequency/csd.py +++ b/mne/time_frequency/csd.py @@ -10,26 +10,26 @@ import numpy as np from scipy.fft import rfftfreq -from .tfr import _cwt_array, morlet, _get_nfft, EpochsTFR -from .._fiff.pick import pick_channels, _picks_to_idx +from .._fiff.pick import _picks_to_idx, pick_channels +from ..parallel import parallel_func +from ..time_frequency.multitaper import ( + _compute_mt_params, + _csd_from_mt, + _mt_spectra, + _psd_from_mt_adaptive, +) from ..utils import ( - logger, - verbose, - warn, - copy_function_doc_to_method_doc, ProgressBar, _check_fname, _import_h5io_funcs, _validate_type, + copy_function_doc_to_method_doc, + logger, + verbose, + warn, ) from ..viz.misc import plot_csd -from ..time_frequency.multitaper import ( - _compute_mt_params, - _mt_spectra, - _csd_from_mt, - _psd_from_mt_adaptive, -) -from ..parallel import parallel_func +from .tfr import EpochsTFR, _cwt_array, _get_nfft, morlet @verbose diff --git a/mne/time_frequency/multitaper.py b/mne/time_frequency/multitaper.py index 8810926f817..5cb42e7b9b7 100644 --- a/mne/time_frequency/multitaper.py +++ b/mne/time_frequency/multitaper.py @@ -9,9 +9,8 @@ from scipy.signal import get_window from scipy.signal.windows import dpss as sp_dpss - from ..parallel import parallel_func -from ..utils import warn, verbose, logger, _check_option +from ..utils import _check_option, logger, verbose, warn def dpss_windows(N, half_nbw, Kmax, *, sym=True, norm=None, low_bias=True): diff --git a/mne/time_frequency/psd.py b/mne/time_frequency/psd.py index 2c88e1adafe..79b269b645e 100644 --- a/mne/time_frequency/psd.py +++ b/mne/time_frequency/psd.py @@ -8,7 +8,7 @@ from scipy.signal import spectrogram from ..parallel import parallel_func -from ..utils import logger, verbose, _check_option, _ensure_int +from ..utils import _check_option, _ensure_int, logger, verbose # adapted from SciPy diff --git a/mne/time_frequency/spectrum.py b/mne/time_frequency/spectrum.py index 1fc2c6ce2bd..3d6acbc3914 100644 --- a/mne/time_frequency/spectrum.py +++ b/mne/time_frequency/spectrum.py @@ -10,6 +10,8 @@ import numpy as np +from .._fiff.meas_info import ContainsMixin, Info +from .._fiff.pick import _pick_data_channels, _picks_to_idx, pick_info from ..channels.channels import UpdateChannelsMixin from ..channels.layout import _merge_ch_data, find_layout from ..defaults import ( @@ -19,8 +21,6 @@ _handle_default, ) from ..html_templates import _get_html_template -from .._fiff.meas_info import ContainsMixin, Info -from .._fiff.pick import _pick_data_channels, _picks_to_idx, pick_info from ..utils import ( GetEpochsMixin, _build_data_frame, @@ -50,13 +50,13 @@ from ..viz.topomap import _make_head_outlines, _prepare_topomap_plot, plot_psds_topomap from ..viz.utils import ( _format_units_psd, + _get_plot_ch_type, _plot_psd, _prepare_sensor_names, plt_show, - _get_plot_ch_type, ) from .multitaper import psd_array_multitaper -from .psd import psd_array_welch, _check_nfft +from .psd import _check_nfft, psd_array_welch def _identity_function(x): @@ -658,8 +658,8 @@ def plot( """ # Must nest this _mpl_figure import because of the BACKEND global # stuff - from .multitaper import _psd_from_mt from ..viz._mpl_figure import _line_figure, _split_picks_by_type + from .multitaper import _psd_from_mt # arg checking ci = _check_ci(ci) diff --git a/mne/time_frequency/tests/test_ar.py b/mne/time_frequency/tests/test_ar.py index e5bcaa2b049..7d1d06d46dc 100644 --- a/mne/time_frequency/tests/test_ar.py +++ b/mne/time_frequency/tests/test_ar.py @@ -1,14 +1,13 @@ from pathlib import Path -import pytest import numpy as np -from numpy.testing import assert_array_almost_equal, assert_allclose +import pytest +from numpy.testing import assert_allclose, assert_array_almost_equal from scipy.signal import lfilter from mne import io from mne.time_frequency.ar import _yule_walker, fit_iir_model_raw - raw_fname = ( Path(__file__).parent.parent.parent / "io" / "tests" / "data" / "test_raw.fif" ) diff --git a/mne/time_frequency/tests/test_csd.py b/mne/time_frequency/tests/test_csd.py index 0c3aef6014c..6c306188699 100644 --- a/mne/time_frequency/tests/test_csd.py +++ b/mne/time_frequency/tests/test_csd.py @@ -1,29 +1,30 @@ +import pickle +from itertools import product +from os import path as op + import numpy as np import pytest +from numpy.testing import assert_allclose, assert_array_equal from pytest import raises -from numpy.testing import assert_array_equal, assert_allclose -from os import path as op -import pickle -from itertools import product import mne from mne.channels import equalize_channels -from mne.utils import sum_squared +from mne.proj import Projection from mne.time_frequency import ( - csd_fourier, - csd_multitaper, - csd_morlet, + CrossSpectralDensity, csd_array_fourier, - csd_array_multitaper, csd_array_morlet, - tfr_morlet, + csd_array_multitaper, + csd_fourier, + csd_morlet, + csd_multitaper, csd_tfr, - CrossSpectralDensity, - read_csd, pick_channels_csd, + read_csd, + tfr_morlet, ) from mne.time_frequency.csd import _sym_mat_to_vector, _vector_to_sym_mat -from mne.proj import Projection +from mne.utils import sum_squared base_dir = op.join(op.dirname(__file__), "..", "..", "io", "tests", "data") raw_fname = op.join(base_dir, "test_raw.fif") diff --git a/mne/time_frequency/tests/test_psd.py b/mne/time_frequency/tests/test_psd.py index b39fd431fd0..3631363083d 100644 --- a/mne/time_frequency/tests/test_psd.py +++ b/mne/time_frequency/tests/test_psd.py @@ -1,12 +1,12 @@ import numpy as np -from numpy.testing import assert_array_almost_equal, assert_allclose, assert_array_equal -from scipy.signal import welch import pytest +from numpy.testing import assert_allclose, assert_array_almost_equal, assert_array_equal +from scipy.signal import welch -from mne.utils import catch_logging -from mne.time_frequency import psd_array_welch, psd_array_multitaper +from mne.time_frequency import psd_array_multitaper, psd_array_welch from mne.time_frequency.multitaper import _psd_from_mt from mne.time_frequency.psd import _median_biases +from mne.utils import catch_logging def test_psd_nan(): diff --git a/mne/time_frequency/tests/test_spectrum.py b/mne/time_frequency/tests/test_spectrum.py index 6a296808b20..7aaa5b40ea6 100644 --- a/mne/time_frequency/tests/test_spectrum.py +++ b/mne/time_frequency/tests/test_spectrum.py @@ -1,17 +1,16 @@ from contextlib import nullcontext from functools import partial +import matplotlib.pyplot as plt import numpy as np import pytest -from numpy.testing import assert_array_equal, assert_allclose -import matplotlib.pyplot as plt +from numpy.testing import assert_allclose, assert_array_equal -from mne import create_info, make_fixed_length_epochs +from mne import Annotations, create_info, make_fixed_length_epochs from mne.io import RawArray -from mne import Annotations from mne.time_frequency import read_spectrum from mne.time_frequency.multitaper import _psd_from_mt -from mne.time_frequency.spectrum import SpectrumArray, EpochsSpectrumArray +from mne.time_frequency.spectrum import EpochsSpectrumArray, SpectrumArray def test_compute_psd_errors(raw): diff --git a/mne/time_frequency/tests/test_stft.py b/mne/time_frequency/tests/test_stft.py index 7609b5c45cb..4829d975aad 100644 --- a/mne/time_frequency/tests/test_stft.py +++ b/mne/time_frequency/tests/test_stft.py @@ -3,12 +3,12 @@ # # License : BSD-3-Clause -import pytest import numpy as np -from scipy import linalg +import pytest from numpy.testing import assert_almost_equal, assert_array_almost_equal +from scipy import linalg -from mne.time_frequency import stft, istft, stftfreq +from mne.time_frequency import istft, stft, stftfreq from mne.time_frequency._stft import stft_norm2 diff --git a/mne/time_frequency/tests/test_stockwell.py b/mne/time_frequency/tests/test_stockwell.py index 77f2e88383b..dcb202a959a 100644 --- a/mne/time_frequency/tests/test_stockwell.py +++ b/mne/time_frequency/tests/test_stockwell.py @@ -5,28 +5,26 @@ from pathlib import Path -import pytest import numpy as np +import pytest from numpy.testing import ( - assert_array_almost_equal, assert_allclose, - assert_equal, + assert_array_almost_equal, assert_array_less, + assert_equal, ) - from scipy import fftpack -from mne import read_events, Epochs, make_fixed_length_events +from mne import Epochs, make_fixed_length_events, read_events from mne.io import read_raw_fif +from mne.time_frequency import AverageTFR, tfr_array_stockwell from mne.time_frequency._stockwell import ( - tfr_stockwell, - _st, - _precompute_st_windows, _check_input_st, + _precompute_st_windows, + _st, _st_power_itc, + tfr_stockwell, ) - -from mne.time_frequency import AverageTFR, tfr_array_stockwell from mne.utils import _record_warnings base_dir = Path(__file__).parent.parent.parent / "io" / "tests" / "data" diff --git a/mne/time_frequency/tests/test_tfr.py b/mne/time_frequency/tests/test_tfr.py index b96cb11e567..58943b10e48 100644 --- a/mne/time_frequency/tests/test_tfr.py +++ b/mne/time_frequency/tests/test_tfr.py @@ -3,41 +3,41 @@ from itertools import product from pathlib import Path +import matplotlib.pyplot as plt import numpy as np -from numpy.testing import assert_array_equal, assert_equal, assert_allclose import pytest -import matplotlib.pyplot as plt +from numpy.testing import assert_allclose, assert_array_equal, assert_equal from scipy.signal import morlet2 import mne from mne import ( Epochs, - read_events, - pick_types, - create_info, EpochsArray, Info, Transform, + create_info, + pick_types, + read_events, ) from mne.io import read_raw_fif -from mne.utils import grand_average, catch_logging +from mne.tests.test_epochs import assert_metadata_equal +from mne.time_frequency import tfr_array_morlet, tfr_array_multitaper from mne.time_frequency.tfr import ( - morlet, - tfr_morlet, - _make_dpss, - tfr_multitaper, AverageTFR, - read_tfrs, - write_tfrs, + EpochsTFR, + _compute_tfr, + _make_dpss, combine_tfr, cwt, - _compute_tfr, - EpochsTFR, fwhm, + morlet, + read_tfrs, + tfr_morlet, + tfr_multitaper, + write_tfrs, ) -from mne.time_frequency import tfr_array_multitaper, tfr_array_morlet +from mne.utils import catch_logging, grand_average from mne.viz.utils import _fake_click, _fake_keypress, _fake_scroll -from mne.tests.test_epochs import assert_metadata_equal data_path = Path(__file__).parent.parent.parent / "io" / "tests" / "data" raw_fname = data_path / "test_raw.fif" diff --git a/mne/time_frequency/tfr.py b/mne/time_frequency/tfr.py index 83445a64690..09745e9a1b1 100644 --- a/mne/time_frequency/tfr.py +++ b/mne/time_frequency/tfr.py @@ -16,65 +16,64 @@ from scipy.fft import fft, ifft from scipy.signal import argrelmax -from .multitaper import dpss_windows - -from ..baseline import rescale, _check_baseline +from .._fiff.meas_info import ContainsMixin, Info +from .._fiff.pick import ( + _picks_to_idx, + channel_type, + pick_info, +) +from ..baseline import _check_baseline, rescale +from ..channels.channels import UpdateChannelsMixin +from ..channels.layout import _find_topomap_coords, _merge_ch_data, _pair_grad_sensors +from ..defaults import _BORDER_DEFAULT, _EXTRAPOLATE_DEFAULT, _INTERPOLATION_DEFAULT from ..filter import next_fast_len from ..parallel import parallel_func from ..utils import ( - logger, - verbose, - _time_mask, - _freq_mask, - check_fname, - sizeof_fmt, - GetEpochsMixin, ExtendedTimeMixin, - _prepare_read_metadata, - fill_doc, - _prepare_write_metadata, - _check_event_id, - _gen_events, + GetEpochsMixin, SizeMixin, - _is_numeric, - _check_option, - _validate_type, + _build_data_frame, _check_combine, - _check_pandas_installed, + _check_event_id, + _check_option, _check_pandas_index_arguments, + _check_pandas_installed, _check_time_format, _convert_times, - _build_data_frame, - warn, + _freq_mask, + _gen_events, _import_h5io_funcs, + _is_numeric, + _prepare_read_metadata, + _prepare_write_metadata, + _time_mask, + _validate_type, + check_fname, copy_function_doc_to_method_doc, + fill_doc, + logger, + sizeof_fmt, + verbose, + warn, ) -from ..channels.channels import UpdateChannelsMixin -from ..channels.layout import _merge_ch_data, _pair_grad_sensors, _find_topomap_coords -from ..defaults import _INTERPOLATION_DEFAULT, _EXTRAPOLATE_DEFAULT, _BORDER_DEFAULT -from .._fiff.pick import ( - pick_info, - _picks_to_idx, - channel_type, -) -from .._fiff.meas_info import Info, ContainsMixin -from ..viz.topo import _imshow_tfr, _plot_topo, _imshow_tfr_unified +from ..viz.topo import _imshow_tfr, _imshow_tfr_unified, _plot_topo from ..viz.topomap import ( - _set_contour_locator, - plot_topomap, + _add_colorbar, _get_pos_outlines, + _set_contour_locator, plot_tfr_topomap, - _add_colorbar, + plot_topomap, ) from ..viz.utils import ( - figure_nobar, - plt_show, - _setup_cmap, _prepare_joint_axes, - _setup_vmin_vmax, _set_title_multiple_electrodes, + _setup_cmap, + _setup_vmin_vmax, add_background_image, + figure_nobar, + plt_show, ) +from .multitaper import dpss_windows @fill_doc diff --git a/mne/transforms.py b/mne/transforms.py index 58f88450c56..f8a4c813025 100644 --- a/mne/transforms.py +++ b/mne/transforms.py @@ -5,8 +5,8 @@ # # License: BSD-3-Clause -import os import glob +import os from copy import deepcopy from pathlib import Path @@ -15,29 +15,28 @@ from scipy.spatial.distance import cdist from scipy.special import sph_harm -from .fixes import jit, _get_img_fdata from ._fiff.constants import FIFF from ._fiff.open import fiff_open from ._fiff.tag import read_tag from ._fiff.write import start_and_end_file, write_coord_trans from .defaults import _handle_default +from .fixes import _get_img_fdata, jit from .utils import ( - check_fname, - logger, - verbose, - _ensure_int, - _validate_type, - _path_like, - get_subjects_dir, - fill_doc, _check_fname, _check_option, + _ensure_int, + _import_nibabel, + _path_like, _require_version, + _validate_type, + check_fname, + fill_doc, + get_subjects_dir, + logger, + verbose, wrapped_stdout, - _import_nibabel, ) - # transformation from anterior/left/superior coordinate system to # right/anterior/superior: als_ras_trans = np.array([[0, -1, 0, 0], [1, 0, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]]) @@ -1783,16 +1782,16 @@ def _compute_volume_registration( nib = _import_nibabel("SDR morph") _require_version("dipy", "SDR morph", "0.10.1") with np.testing.suppress_warnings(): - from dipy.align.imaffine import AffineMap from dipy.align import ( + affine, affine_registration, center_of_mass, - translation, - rigid, - affine, imwarp, metrics, + rigid, + translation, ) + from dipy.align.imaffine import AffineMap # input validation _validate_type(moving, nib.spatialimages.SpatialImage, "moving") @@ -1927,9 +1926,9 @@ def apply_volume_registration( """ _require_version("dipy", "SDR morph", "0.10.1") _import_nibabel("SDR morph") - from nibabel.spatialimages import SpatialImage - from dipy.align.imwarp import DiffeomorphicMap from dipy.align.imaffine import AffineMap + from dipy.align.imwarp import DiffeomorphicMap + from nibabel.spatialimages import SpatialImage _validate_type(moving, SpatialImage, "moving") _validate_type(static, SpatialImage, "static") @@ -2001,9 +2000,9 @@ def apply_volume_registration_points( from .channels import compute_native_head_t, make_dig_montage _require_version("nibabel", "volume registration", "2.1.0") + from dipy.align.imwarp import DiffeomorphicMap from nibabel import MGHImage from nibabel.spatialimages import SpatialImage - from dipy.align.imwarp import DiffeomorphicMap _validate_type(moving, SpatialImage, "moving") _validate_type(static, SpatialImage, "static") diff --git a/mne/utils/__init__.pyi b/mne/utils/__init__.pyi index 8dfc4a6bdc4..b2c611f7d1c 100644 --- a/mne/utils/__init__.pyi +++ b/mne/utils/__init__.pyi @@ -183,202 +183,202 @@ __all__ = [ "wrapped_stdout", ] from ._bunch import Bunch, BunchConst, BunchConstNamed +from ._logging import ( + ClosingStringIO, + _get_call_line, + _parse_verbose, + _record_warnings, + _verbose_safe_false, + catch_logging, + filter_out_warnings, + logger, + set_log_file, + set_log_level, + use_log_level, + verbose, + warn, + wrapped_stdout, +) +from ._testing import ( + ArgvSetter, + SilenceStdout, + _click_ch_name, + _raw_annot, + _TempDir, + assert_and_remove_boundary_annot, + assert_dig_allclose, + assert_meg_snr, + assert_object_equal, + assert_snr, + assert_stcs_equal, + buggy_mkl_svd, + has_freesurfer, + has_mne_c, + requires_freesurfer, + requires_good_network, + requires_mne, + requires_mne_mark, + requires_openmeeg_mark, + run_command_if_main, +) from .check import ( - check_fname, - check_version, - check_random_state, - _check_fname, - _check_subject, - _check_pandas_installed, - _check_pandas_index_arguments, - _check_event_id, + _check_all_same_channel_names, _check_ch_locs, + _check_channels_spatial_filter, + _check_combine, _check_compensation_grade, + _check_depth, + _check_dict_keys, + _check_edflib_installed, + _check_eeglabio_installed, + _check_event_id, + _check_fname, + _check_freesurfer_home, + _check_head_radius, _check_if_nan, - _is_numeric, - _ensure_int, + _check_info_inv, _check_integer_or_list, + _check_on_missing, + _check_one_ch_type, + _check_option, + _check_pandas_index_arguments, + _check_pandas_installed, _check_preload, - _validate_type, + _check_pybv_installed, + _check_pymatreader_installed, + _check_qt_version, _check_range, - _check_info_inv, - _check_channels_spatial_filter, - _check_one_ch_type, _check_rank, - _check_option, - _check_depth, - _check_combine, - _path_like, + _check_sphere, _check_src_normal, _check_stc_units, - _check_qt_version, - _check_sphere, + _check_subject, _check_time_format, - _check_freesurfer_home, - _suggest, - _require_version, - _on_missing, - _check_on_missing, - int_like, - _safe_input, - _check_all_same_channel_names, - path_like, _ensure_events, - _check_eeglabio_installed, - _check_pybv_installed, - _check_edflib_installed, - _to_rgb, - _soft_import, - _check_dict_keys, - _check_pymatreader_installed, - _import_h5py, + _ensure_int, _import_h5io_funcs, + _import_h5py, _import_nibabel, _import_pymatreader_funcs, - _check_head_radius, + _is_numeric, + _on_missing, + _path_like, + _require_version, + _safe_input, + _soft_import, + _suggest, + _to_rgb, + _validate_type, + check_fname, + check_random_state, + check_version, + int_like, + path_like, ) from .config import ( - set_config, + _get_extra_data_path, + _get_numpy_libs, + _get_root_dir, + _get_stim_channel, get_config, get_config_path, + get_subjects_dir, set_cache_dir, + set_config, set_memmap_min_size, - get_subjects_dir, - _get_stim_channel, sys_info, - _get_extra_data_path, - _get_root_dir, - _get_numpy_libs, +) +from .dataframe import ( + _build_data_frame, + _convert_times, + _scale_dataframe_data, + _set_pandas_dtype, ) from .docs import ( - copy_function_doc_to_method_doc, + _doc_special_members, + copy_base_doc_to_subclass_doc, copy_doc, - linkcode_resolve, - open_docs, + copy_function_doc_to_method_doc, deprecated, - fill_doc, deprecated_alias, + fill_doc, legacy, - copy_base_doc_to_subclass_doc, - _doc_special_members, + linkcode_resolve, + open_docs, ) from .fetching import _url_to_local_path -from ._logging import ( - verbose, - logger, - set_log_level, - set_log_file, - use_log_level, - catch_logging, - warn, - filter_out_warnings, - wrapped_stdout, - _get_call_line, - _record_warnings, - ClosingStringIO, - _verbose_safe_false, - _parse_verbose, +from .linalg import ( + _get_blas_funcs, + _repeated_svd, + _svd_lwork, + _sym_mat_pow, + eigh, + sqrtm_sym, ) from .misc import ( - run_subprocess, - _pl, + _assert_no_instances, + _auto_weakref, _clean_names, - pformat, - _file_like, + _DefaultEventParser, _empty_hash, _explain_exception, + _file_like, _get_argvalues, - sizeof_fmt, - running_subprocess, - _DefaultEventParser, - _assert_no_instances, + _pl, _resource_path, + pformat, repr_html, - _auto_weakref, + run_subprocess, + running_subprocess, + sizeof_fmt, ) -from .progressbar import ProgressBar -from ._testing import ( - run_command_if_main, - requires_mne, - requires_good_network, - ArgvSetter, - SilenceStdout, - has_freesurfer, - has_mne_c, - _TempDir, - buggy_mkl_svd, - requires_freesurfer, - requires_mne_mark, - assert_object_equal, - assert_and_remove_boundary_annot, - _raw_annot, - assert_dig_allclose, - assert_meg_snr, - assert_snr, - assert_stcs_equal, - _click_ch_name, - requires_openmeeg_mark, +from .mixin import ( + ExtendedTimeMixin, + GetEpochsMixin, + SizeMixin, + TimeMixin, + _check_decim, + _prepare_read_metadata, + _prepare_write_metadata, ) from .numerics import ( - hashfunc, + _PCA, + _apply_scaling_array, + _apply_scaling_cov, + _arange_div, + _array_equal_nan, + _cal_to_julian, + _check_dt, _compute_row_norms, + _custom_lru_cache, + _dt_to_julian, + _dt_to_stamp, + _freq_mask, + _gen_events, + _get_inst_data, + _hashable_ndarray, + _julian_to_cal, + _julian_to_dt, + _mask_to_onsets_offsets, _reg_pinv, - random_permutation, _reject_data_segments, - compute_corr, - _get_inst_data, + _replace_md5, + _ReuseCycle, + _scaled_array, + _stamp_to_dt, + _time_mask, + _undo_scaling_array, + _undo_scaling_cov, array_split_idx, - sum_squared, - split_list, - _gen_events, + compute_corr, create_slices, - _time_mask, - _freq_mask, grand_average, + hashfunc, object_diff, object_hash, object_size, - _apply_scaling_cov, - _undo_scaling_cov, - _apply_scaling_array, - _undo_scaling_array, - _scaled_array, - _replace_md5, - _PCA, - _mask_to_onsets_offsets, - _array_equal_nan, - _julian_to_cal, - _cal_to_julian, - _dt_to_julian, - _julian_to_dt, - _dt_to_stamp, - _stamp_to_dt, - _check_dt, - _ReuseCycle, - _arange_div, - _hashable_ndarray, - _custom_lru_cache, -) -from .mixin import ( - SizeMixin, - GetEpochsMixin, - TimeMixin, - ExtendedTimeMixin, - _prepare_read_metadata, - _prepare_write_metadata, - _check_decim, -) -from .linalg import ( - _svd_lwork, - _repeated_svd, - _sym_mat_pow, - sqrtm_sym, - eigh, - _get_blas_funcs, -) -from .dataframe import ( - _set_pandas_dtype, - _scale_dataframe_data, - _convert_times, - _build_data_frame, + random_permutation, + split_list, + sum_squared, ) +from .progressbar import ProgressBar diff --git a/mne/utils/_bunch.py b/mne/utils/_bunch.py index 3ede4290111..9b9af35a7ad 100644 --- a/mne/utils/_bunch.py +++ b/mne/utils/_bunch.py @@ -7,7 +7,6 @@ from copy import deepcopy - ############################################################################### # Create a Bunch class that acts like a struct (mybunch.key = val) diff --git a/mne/utils/_logging.py b/mne/utils/_logging.py index 9cec6f4c7be..84c8bcb2e69 100644 --- a/mne/utils/_logging.py +++ b/mne/utils/_logging.py @@ -4,19 +4,19 @@ # License: BSD-3-Clause import contextlib -from decorator import FunctionMaker import importlib import inspect -from io import StringIO -import re -import sys import logging import os.path as op +import re +import sys import warnings +from io import StringIO from typing import Any, Callable, TypeVar -from .docs import fill_doc +from decorator import FunctionMaker +from .docs import fill_doc logger = logging.getLogger("mne") # one selection here used across mne-python logger.propagate = False # don't propagate (in case of multiple imports) @@ -220,8 +220,8 @@ def set_log_level(verbose=None, return_old_level=False, add_frames=None): def _parse_verbose(verbose): - from .config import get_config from .check import _check_option, _validate_type + from .config import get_config _validate_type(verbose, (bool, str, int, None), "verbose") if verbose is None: diff --git a/mne/utils/_testing.py b/mne/utils/_testing.py index da907bb52c9..4f1a2eaf9b5 100644 --- a/mne/utils/_testing.py +++ b/mne/utils/_testing.py @@ -3,21 +3,21 @@ # # License: BSD-3-Clause -from functools import wraps -import os import inspect -from io import StringIO -from shutil import rmtree +import os import sys import tempfile import traceback +from functools import wraps +from io import StringIO +from shutil import rmtree from unittest import SkipTest import numpy as np -from numpy.testing import assert_array_equal, assert_allclose +from numpy.testing import assert_allclose, assert_array_equal from scipy import linalg -from ._logging import warn, ClosingStringIO +from ._logging import ClosingStringIO, warn from .check import check_version from .misc import run_subprocess from .numerics import object_diff @@ -315,9 +315,9 @@ def _dig_sort_key(dig): def assert_dig_allclose(info_py, info_bin, limit=None): """Assert dig allclose.""" - from ..bem import fit_sphere_to_headshape from .._fiff.constants import FIFF from .._fiff.meas_info import Info + from ..bem import fit_sphere_to_headshape from ..channels.montage import DigMontage # test dig positions diff --git a/mne/utils/check.py b/mne/utils/check.py index d13f2d1a1f1..a26495106e4 100644 --- a/mne/utils/check.py +++ b/mne/utils/check.py @@ -3,23 +3,23 @@ # # License: BSD-3-Clause +import numbers +import operator +import os +import re from builtins import input # no-op here but facilitates testing from collections.abc import Sequence from difflib import get_close_matches from importlib import import_module from importlib.metadata import version -import operator -import os -from packaging.version import parse from pathlib import Path -import re -import numbers import numpy as np +from packaging.version import parse -from ..defaults import _handle_default, HEAD_SIZE_DEFAULT -from ..fixes import _median_complex, _compare_version -from ._logging import warn, logger, verbose, _record_warnings, _verbose_safe_false +from ..defaults import HEAD_SIZE_DEFAULT, _handle_default +from ..fixes import _compare_version, _median_complex +from ._logging import _record_warnings, _verbose_safe_false, logger, verbose, warn def _ensure_int(x, name="unknown", must_be="an int", *, extra=""): @@ -316,9 +316,9 @@ def _check_preload(inst, msg): def _check_compensation_grade(info1, info2, name1, name2="data", ch_names=None): """Ensure that objects have same compensation_grade.""" + from .._fiff.compensator import get_current_comp from .._fiff.meas_info import Info from .._fiff.pick import pick_channels, pick_info - from .._fiff.compensator import get_current_comp for t_info in (info1, info2): if t_info is None: @@ -754,9 +754,9 @@ def _check_rank(rank): def _check_one_ch_type(method, info, forward, data_cov=None, noise_cov=None): """Check number of sensor types and presence of noise covariance matrix.""" - from ..cov import make_ad_hoc_cov, Covariance + from .._fiff.pick import _contains_ch_type, pick_info + from ..cov import Covariance, make_ad_hoc_cov from ..time_frequency.csd import CrossSpectralDensity - from .._fiff.pick import pick_info, _contains_ch_type if isinstance(data_cov, CrossSpectralDensity): _validate_type(noise_cov, [None, CrossSpectralDensity], "noise_cov") @@ -946,7 +946,8 @@ def _check_qt_version(*, return_api=False, check_usable_display=True): from ..viz.backends._utils import _init_mne_qtapp try: - from qtpy import QtCore, API_NAME as api + from qtpy import API_NAME as api + from qtpy import QtCore except Exception: api = version = None else: @@ -969,7 +970,7 @@ def _check_qt_version(*, return_api=False, check_usable_display=True): def _check_sphere(sphere, info=None, sphere_units="m"): - from ..bem import fit_sphere_to_headshape, ConductorModel, get_fitting_dig + from ..bem import ConductorModel, fit_sphere_to_headshape, get_fitting_dig if sphere is None: sphere = HEAD_SIZE_DEFAULT diff --git a/mne/utils/config.py b/mne/utils/config.py index 47a26e8109d..aa4e0b9dd90 100644 --- a/mne/utils/config.py +++ b/mne/utils/config.py @@ -18,11 +18,10 @@ from importlib import import_module from pathlib import Path -from .check import _validate_type, _check_qt_version, _check_option, _check_fname +from ._logging import logger, warn +from .check import _check_fname, _check_option, _check_qt_version, _validate_type from .docs import fill_doc from .misc import _pl -from ._logging import warn, logger - _temp_home_dir = None diff --git a/mne/utils/dataframe.py b/mne/utils/dataframe.py index a6d262bf8ee..2c9cbad78af 100644 --- a/mne/utils/dataframe.py +++ b/mne/utils/dataframe.py @@ -7,8 +7,8 @@ import numpy as np -from ._logging import logger, verbose from ..defaults import _handle_default +from ._logging import logger, verbose @verbose @@ -74,6 +74,7 @@ def _build_data_frame( """Build DataFrame from MNE-object-derived data array.""" # private function; pandas already checked in calling function from pandas import DataFrame + from ..source_estimate import _BaseSourceEstimate # build DataFrame diff --git a/mne/utils/docs.py b/mne/utils/docs.py index ef311f30a71..9e72685e738 100644 --- a/mne/utils/docs.py +++ b/mne/utils/docs.py @@ -16,7 +16,6 @@ from ..defaults import HEAD_SIZE_DEFAULT from ._bunch import BunchConst - # # # WARNING # # # # This list must also be updated in doc/_templates/autosummary/class.rst if it # is changed here! diff --git a/mne/utils/misc.py b/mne/utils/misc.py index 79a23e9a0e6..470caf671f2 100644 --- a/mne/utils/misc.py +++ b/mne/utils/misc.py @@ -3,27 +3,27 @@ # # License: BSD-3-Clause -from contextlib import contextmanager, ExitStack import fnmatch import gc import hashlib import inspect -from math import log import os -from queue import Queue, Empty -from string import Formatter import subprocess import sys -from textwrap import dedent -from threading import Thread import traceback import weakref +from contextlib import ExitStack, contextmanager +from math import log +from queue import Empty, Queue +from string import Formatter +from textwrap import dedent +from threading import Thread import numpy as np from decorator import FunctionMaker -from .check import _check_option, _validate_type from ._logging import logger, verbose, warn +from .check import _check_option, _validate_type # TODO: remove try/except when our min version is py 3.9 try: diff --git a/mne/utils/mixin.py b/mne/utils/mixin.py index 8f4adc43d15..e9f30116454 100644 --- a/mne/utils/mixin.py +++ b/mne/utils/mixin.py @@ -3,17 +3,16 @@ # # License: BSD-3-Clause +import json +import logging from collections import OrderedDict from copy import deepcopy -import logging -import json import numpy as np +from ._logging import verbose, warn from .check import _check_pandas_installed, _check_preload, _validate_type -from ._logging import warn, verbose -from .numerics import object_size, object_hash, _time_mask - +from .numerics import _time_mask, object_hash, object_size logger = logging.getLogger("mne") # one selection here used across mne-python logger.propagate = False # don't propagate (in case of multiple imports) @@ -59,9 +58,9 @@ def __hash__(self): hash : int The hash """ - from ..io import BaseRaw from ..epochs import BaseEpochs from ..evoked import Evoked + from ..io import BaseRaw if isinstance(self, Evoked): return object_hash(dict(info=self.info, data=self.data)) @@ -672,9 +671,9 @@ def decimate(self, decim, offset=0, *, verbose=None): # if epochs have frequencies, they are not in time (EpochsTFR) # and so do not need to be checked whether they have been # appropriately filtered to avoid aliasing - from ..evoked import Evoked from ..epochs import BaseEpochs - from ..time_frequency import EpochsTFR, AverageTFR + from ..evoked import Evoked + from ..time_frequency import AverageTFR, EpochsTFR # This should be the list of classes that inherit _validate_type(self, (BaseEpochs, Evoked, EpochsTFR, AverageTFR), "inst") @@ -736,8 +735,8 @@ def shift_time(self, tshift, relative=True): def _update_first_last(self): """Update self.first and self.last (sample indices).""" - from ..evoked import Evoked from ..dipole import DipoleFixed + from ..evoked import Evoked if isinstance(self, (Evoked, DipoleFixed)): self.first = int(round(self.times[0] * self.info["sfreq"])) diff --git a/mne/utils/numerics.py b/mne/utils/numerics.py index 67a15d789e4..44cf8210771 100644 --- a/mne/utils/numerics.py +++ b/mne/utils/numerics.py @@ -19,18 +19,18 @@ import numpy as np from scipy import sparse -from ._logging import logger, warn, verbose -from .check import check_random_state, _ensure_int, _validate_type -from .misc import _empty_hash from ..fixes import ( _infer_dimension_, - svd_flip, - stable_cumsum, _safe_svd, - jit, has_numba, + jit, + stable_cumsum, + svd_flip, ) +from ._logging import logger, verbose, warn +from .check import _ensure_int, _validate_type, check_random_state from .docs import fill_doc +from .misc import _empty_hash def split_list(v, n, idx=False): @@ -209,8 +209,8 @@ def _gen_events(n_epochs): def _reject_data_segments(data, reject, flat, decim, info, tstep): """Reject data segments using peak-to-peak amplitude.""" - from ..epochs import _is_good from .._fiff.pick import channel_indices_by_type + from ..epochs import _is_good data_clean = np.empty_like(data) idx_by_type = channel_indices_by_type(info) @@ -251,9 +251,9 @@ def _reject_data_segments(data, reject, flat, decim, info, tstep): def _get_inst_data(inst): """Get data view from MNE object instance like Raw, Epochs or Evoked.""" - from ..io import BaseRaw from ..epochs import BaseEpochs from ..evoked import Evoked + from ..io import BaseRaw from ..time_frequency.tfr import _BaseTFR _validate_type(inst, (BaseRaw, BaseEpochs, Evoked, _BaseTFR), "Instance") @@ -576,9 +576,9 @@ def grand_average(all_inst, interpolate_bads=True, drop_bads=True): .. versionadded:: 0.11.0 """ # check if all elements in the given list are evoked data + from ..channels.channels import equalize_channels from ..evoked import Evoked from ..time_frequency import AverageTFR - from ..channels.channels import equalize_channels if not all_inst: raise ValueError("Please pass a list of Evoked or AverageTFR objects.") diff --git a/mne/utils/progressbar.py b/mne/utils/progressbar.py index a47c2e340d0..220a103a2b3 100644 --- a/mne/utils/progressbar.py +++ b/mne/utils/progressbar.py @@ -3,19 +3,19 @@ # # License: BSD-3-Clause -from collections.abc import Iterable +import logging import os import os.path as op -import logging import tempfile -from threading import Thread import time +from collections.abc import Iterable +from threading import Thread import numpy as np +from ._logging import logger from .check import _check_option from .config import get_config -from ._logging import logger class ProgressBar: diff --git a/mne/utils/spectrum.py b/mne/utils/spectrum.py index 0b0fffc9e5a..2ea3b058c58 100644 --- a/mne/utils/spectrum.py +++ b/mne/utils/spectrum.py @@ -1,4 +1,5 @@ from inspect import currentframe, getargvalues, signature + from ..utils import warn diff --git a/mne/utils/tests/test_bunch.py b/mne/utils/tests/test_bunch.py index 757fa8e3072..93d87387c55 100644 --- a/mne/utils/tests/test_bunch.py +++ b/mne/utils/tests/test_bunch.py @@ -4,8 +4,9 @@ # License: BSD-3-Clause import pickle + from mne.utils import BunchConstNamed -from mne.utils._bunch import NamedInt, NamedFloat +from mne.utils._bunch import NamedFloat, NamedInt def test_pickle(): diff --git a/mne/utils/tests/test_check.py b/mne/utils/tests/test_check.py index 9ad25d9843a..2856d9ea37b 100644 --- a/mne/utils/tests/test_check.py +++ b/mne/utils/tests/test_check.py @@ -6,32 +6,32 @@ import os import sys +from pathlib import Path import numpy as np import pytest -from pathlib import Path import mne -from mne import read_vectorview_selection, pick_channels_cov -from mne.datasets import testing +from mne import pick_channels_cov, read_vectorview_selection from mne._fiff.pick import _picks_to_idx +from mne.datasets import testing from mne.utils import ( - check_random_state, + Bunch, + _check_ch_locs, _check_fname, - check_fname, - _suggest, - _check_subject, _check_info_inv, _check_option, - Bunch, - check_version, - _path_like, - _validate_type, + _check_range, + _check_sphere, + _check_subject, _on_missing, + _path_like, _safe_input, - _check_ch_locs, - _check_sphere, - _check_range, + _suggest, + _validate_type, + check_fname, + check_random_state, + check_version, ) data_path = testing.data_path(download=False) diff --git a/mne/utils/tests/test_config.py b/mne/utils/tests/test_config.py index 07f555df157..d29aa43feda 100644 --- a/mne/utils/tests/test_config.py +++ b/mne/utils/tests/test_config.py @@ -1,17 +1,18 @@ import os import platform -import pytest from pathlib import Path +import pytest + from mne.utils import ( - set_config, + ClosingStringIO, + _get_stim_channel, get_config, get_config_path, + get_subjects_dir, + set_config, set_memmap_min_size, - _get_stim_channel, sys_info, - ClosingStringIO, - get_subjects_dir, ) diff --git a/mne/utils/tests/test_docs.py b/mne/utils/tests/test_docs.py index a89e266c53a..7e744202e95 100644 --- a/mne/utils/tests/test_docs.py +++ b/mne/utils/tests/test_docs.py @@ -1,16 +1,17 @@ +import webbrowser + import pytest -from mne import open_docs, grade_to_tris +from mne import grade_to_tris, open_docs from mne.utils import ( - copy_function_doc_to_method_doc, + catch_logging, copy_doc, - linkcode_resolve, + copy_function_doc_to_method_doc, deprecated, deprecated_alias, legacy, - catch_logging, + linkcode_resolve, ) -import webbrowser @pytest.mark.parametrize("obj", (grade_to_tris,)) diff --git a/mne/utils/tests/test_linalg.py b/mne/utils/tests/test_linalg.py index 45e05bba94f..3a470c3073b 100644 --- a/mne/utils/tests/test_linalg.py +++ b/mne/utils/tests/test_linalg.py @@ -4,11 +4,11 @@ # License: BSD-3-Clause import numpy as np +import pytest from numpy.testing import assert_allclose, assert_array_equal from scipy import linalg -import pytest -from mne.utils import _sym_mat_pow, _reg_pinv, _record_warnings +from mne.utils import _record_warnings, _reg_pinv, _sym_mat_pow @pytest.mark.parametrize("dtype", (np.float64, np.complex128)) # real, complex diff --git a/mne/utils/tests/test_logging.py b/mne/utils/tests/test_logging.py index 9225d3548f9..2b9b6a8aeee 100644 --- a/mne/utils/tests/test_logging.py +++ b/mne/utils/tests/test_logging.py @@ -6,19 +6,19 @@ import numpy as np import pytest -from mne import read_evokeds, Epochs, create_info -from mne.io import read_raw_fif, RawArray +from mne import Epochs, create_info, read_evokeds +from mne.io import RawArray, read_raw_fif from mne.utils import ( - warn, - set_log_level, - set_log_file, - filter_out_warnings, - verbose, _get_call_line, - use_log_level, catch_logging, - logger, check, + filter_out_warnings, + logger, + set_log_file, + set_log_level, + use_log_level, + verbose, + warn, ) from mne.utils._logging import _frame_info diff --git a/mne/utils/tests/test_misc.py b/mne/utils/tests/test_misc.py index 1309feafbec..aca5efa5fbc 100644 --- a/mne/utils/tests/test_misc.py +++ b/mne/utils/tests/test_misc.py @@ -1,12 +1,12 @@ -from contextlib import nullcontext import os import subprocess import sys +from contextlib import nullcontext import pytest import mne -from mne.utils import sizeof_fmt, run_subprocess, catch_logging +from mne.utils import catch_logging, run_subprocess, sizeof_fmt def test_sizeof_fmt(): diff --git a/mne/utils/tests/test_mixin.py b/mne/utils/tests/test_mixin.py index 23b6a950aa4..32c7abe6a32 100644 --- a/mne/utils/tests/test_mixin.py +++ b/mne/utils/tests/test_mixin.py @@ -2,8 +2,8 @@ # # License: BSD-3-Clause -from numpy.testing import assert_allclose import pytest +from numpy.testing import assert_allclose import mne from mne.datasets import testing diff --git a/mne/utils/tests/test_numerics.py b/mne/utils/tests/test_numerics.py index 8b87e19a68a..71b1a349cd1 100644 --- a/mne/utils/tests/test_numerics.py +++ b/mne/utils/tests/test_numerics.py @@ -4,45 +4,44 @@ from pathlib import Path import numpy as np -from numpy.testing import assert_array_equal, assert_allclose import pytest +from numpy.testing import assert_allclose, assert_array_equal from scipy import sparse -from mne import read_evokeds, read_cov, pick_types +from mne import pick_types, read_cov, read_evokeds from mne._fiff.pick import _picks_by_type from mne.epochs import make_fixed_length_epochs from mne.io import read_raw_fif from mne.time_frequency import tfr_morlet from mne.utils import ( - _get_inst_data, - hashfunc, - sum_squared, - compute_corr, - create_slices, - _time_mask, - _freq_mask, - random_permutation, - _reg_pinv, - object_size, - object_hash, - object_diff, - _apply_scaling_cov, - _undo_scaling_cov, - _apply_scaling_array, - _undo_scaling_array, _PCA, + _apply_scaling_array, + _apply_scaling_cov, _array_equal_nan, - _julian_to_cal, _cal_to_julian, + _custom_lru_cache, _dt_to_julian, + _freq_mask, + _get_inst_data, + _julian_to_cal, _julian_to_dt, - grand_average, + _reg_pinv, _ReuseCycle, + _time_mask, + _undo_scaling_array, + _undo_scaling_cov, + compute_corr, + create_slices, + grand_average, + hashfunc, numerics, - _custom_lru_cache, + object_diff, + object_hash, + object_size, + random_permutation, + sum_squared, ) -from mne.utils.numerics import _LRU_CACHES, _LRU_CACHE_MAXSIZES - +from mne.utils.numerics import _LRU_CACHE_MAXSIZES, _LRU_CACHES base_dir = Path(__file__).parent.parent.parent / "io" / "tests" / "data" fname_raw = base_dir / "test_raw.fif" diff --git a/mne/utils/tests/test_progressbar.py b/mne/utils/tests/test_progressbar.py index 6f39f45fcc1..4d2438bb7d5 100644 --- a/mne/utils/tests/test_progressbar.py +++ b/mne/utils/tests/test_progressbar.py @@ -5,11 +5,11 @@ from pathlib import Path import numpy as np -from numpy.testing import assert_array_equal import pytest +from numpy.testing import assert_array_equal from mne.parallel import parallel_func -from mne.utils import ProgressBar, array_split_idx, use_log_level, catch_logging +from mne.utils import ProgressBar, array_split_idx, catch_logging, use_log_level def test_progressbar(monkeypatch): diff --git a/mne/viz/_3d.py b/mne/viz/_3d.py index 680d52022b5..feb01f9c850 100644 --- a/mne/viz/_3d.py +++ b/mne/viz/_3d.py @@ -9,89 +9,89 @@ # # License: Simplified BSD -from collections import defaultdict import os import os.path as op import warnings +from collections import defaultdict from collections.abc import Iterable from dataclasses import dataclass from functools import partial from itertools import cycle -from typing import Optional from pathlib import Path +from typing import Optional import numpy as np -from scipy.stats import rankdata from scipy.spatial import ConvexHull, Delaunay from scipy.spatial.distance import cdist +from scipy.stats import rankdata -from ._dipole import _check_concat_dipoles, _plot_dipole_mri_outlines, _plot_dipole_3d -from ..defaults import DEFAULTS -from ..fixes import _crop_colorbar, _get_img_fdata +from .._fiff.constants import FIFF +from .._fiff.meas_info import Info, create_info, read_fiducials +from .._fiff.pick import ( + _FNIRS_CH_TYPES_SPLIT, + _MEG_CH_TYPES_SPLIT, + channel_type, + pick_info, + pick_types, +) +from .._fiff.tag import _loc_to_coil_trans from .._freesurfer import ( - _read_mri_info, _check_mri, _get_head_surface, _get_skull_surface, + _read_mri_info, read_freesurfer_lut, ) -from .._fiff.tag import _loc_to_coil_trans -from .._fiff.pick import ( - pick_types, - channel_type, - pick_info, - _FNIRS_CH_TYPES_SPLIT, - _MEG_CH_TYPES_SPLIT, -) -from .._fiff.constants import FIFF -from .._fiff.meas_info import read_fiducials, create_info, Info +from ..defaults import DEFAULTS +from ..fixes import _crop_colorbar, _get_img_fdata from ..surface import ( - get_meg_helmet_surf, - _read_mri_surface, + _CheckInside, _DistanceQuery, _project_onto_surface, + _read_mri_surface, _reorder_ccw, - _CheckInside, + get_meg_helmet_surf, ) from ..transforms import ( - apply_trans, - rot_to_quat, - combine_transforms, - _get_trans, - _ensure_trans, Transform, - rotation, - read_ras_mni_t, - _print_coord_trans, + _ensure_trans, _find_trans, - transform_surface_to, _frame_to_str, + _get_trans, _get_transforms_to_coord_frame, + _print_coord_trans, + apply_trans, + combine_transforms, + read_ras_mni_t, + rot_to_quat, + rotation, + transform_surface_to, ) from ..utils import ( - get_subjects_dir, - logger, + _check_option, _check_subject, - verbose, - warn, + _ensure_int, + _import_nibabel, + _pl, + _to_rgb, + _validate_type, check_version, fill_doc, - _pl, get_config, - _import_nibabel, - _ensure_int, - _validate_type, - _check_option, - _to_rgb, + get_subjects_dir, + logger, + verbose, + warn, ) +from ._dipole import _check_concat_dipoles, _plot_dipole_3d, _plot_dipole_mri_outlines +from .evoked_field import EvokedField from .utils import ( - _get_color_list, + _check_time_unit, _get_cmap, - plt_show, + _get_color_list, figure_nobar, - _check_time_unit, + plt_show, ) -from .evoked_field import EvokedField verbose_dec = verbose FIDUCIAL_ORDER = (FIFF.FIFFV_POINT_LPA, FIFF.FIFFV_POINT_NASION, FIFF.FIFFV_POINT_RPA) @@ -177,9 +177,10 @@ def plot_head_positions( fig : instance of matplotlib.figure.Figure The figure. """ + import matplotlib.pyplot as plt + from ..chpi import head_pos_to_trans_rot_t from ..preprocessing.maxwell import _check_destination - import matplotlib.pyplot as plt _check_option("mode", mode, ["traces", "field"]) dest_info = dict(dev_head_t=None) if info is None else info @@ -310,8 +311,8 @@ def plot_head_positions( else: # mode == 'field': from matplotlib.colors import Normalize - from mpl_toolkits.mplot3d.art3d import Line3DCollection from mpl_toolkits.mplot3d import Axes3D # noqa: F401, analysis:ignore + from mpl_toolkits.mplot3d.art3d import Line3DCollection fig, ax = plt.subplots( 1, subplot_kw=dict(projection="3d"), layout="constrained" @@ -633,9 +634,9 @@ def plot_alignment( .. versionadded:: 0.15 """ # Update the backend - from .backends.renderer import _get_renderer from ..bem import ConductorModel, _bem_find_surface, _ensure_bem_surfaces from ..source_space._source_space import _ensure_src + from .backends.renderer import _get_renderer meg, eeg, fnirs, warn_meg = _handle_sensor_types(meg, eeg, fnirs) _check_option("interaction", interaction, ["trackball", "terrain"]) @@ -1983,11 +1984,12 @@ def _plot_mpl_stc( ): """Plot source estimate using mpl.""" import matplotlib.pyplot as plt - from mpl_toolkits.mplot3d import Axes3D - from matplotlib.widgets import Slider import nibabel as nib + from matplotlib.widgets import Slider + from mpl_toolkits.mplot3d import Axes3D + from ..morph import _get_subject_sphere_tris - from ..source_space._source_space import _create_surf_spacing, _check_spacing + from ..source_space._source_space import _check_spacing, _create_surf_spacing if hemi not in ["lh", "rh"]: raise ValueError( @@ -2175,7 +2177,7 @@ def link_brains(brains, time=True, camera=False, colorbar=True, picking=False): def _check_volume(stc, src, surface, backend_name): - from ..source_estimate import _BaseSurfaceSourceEstimate, _BaseMixedSourceEstimate + from ..source_estimate import _BaseMixedSourceEstimate, _BaseSurfaceSourceEstimate from ..source_space import SourceSpaces if isinstance(stc, _BaseSurfaceSourceEstimate): @@ -2347,8 +2349,8 @@ def plot_source_estimates( - https://surfer.nmr.mgh.harvard.edu/fswiki/FreeSurferOccipitalFlattenedPatch - https://openwetware.org/wiki/Beauchamp:FreeSurfer """ # noqa: E501 - from .backends.renderer import _get_3d_backend, use_3d_backend from ..source_estimate import _BaseSourceEstimate, _check_stc_src + from .backends.renderer import _get_3d_backend, use_3d_backend _check_stc_src(stc, src) _validate_type(stc, _BaseSourceEstimate, "stc", "source estimate") @@ -2436,8 +2438,8 @@ def _plot_stc( add_data_kwargs, brain_kwargs, ): - from .backends.renderer import _get_3d_backend, get_brain_class from ..source_estimate import _BaseVolSourceEstimate + from .backends.renderer import _get_3d_backend, get_brain_class vec = stc._data_ndim == 3 subjects_dir = str(get_subjects_dir(subjects_dir=subjects_dir, raise_error=True)) @@ -2709,7 +2711,8 @@ def plot_volume_source_estimates( >>> fig = stc_vol_sample.plot(morph) # doctest: +SKIP """ # noqa: E501 import nibabel as nib - from matplotlib import pyplot as plt, colors + from matplotlib import colors + from matplotlib import pyplot as plt from ..morph import SourceMorph from ..source_estimate import VolSourceEstimate @@ -2718,8 +2721,8 @@ def plot_volume_source_estimates( if not check_version("nilearn", "0.4"): raise RuntimeError("This function requires nilearn >= 0.4") - from nilearn.plotting import plot_stat_map, plot_glass_brain 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] @@ -3050,8 +3053,8 @@ def plot_and_correct(*args, **kwargs): def _check_views(surf, views, hemi, stc=None, backend=None): - from ._brain.view import views_dicts from ..source_estimate import SourceEstimate + from ._brain.view import views_dicts _validate_type(views, (list, tuple, str), "views") views = [views] if isinstance(views, str) else list(views) diff --git a/mne/viz/_3d_overlay.py b/mne/viz/_3d_overlay.py index eff5d400035..8eb7c7313f7 100644 --- a/mne/viz/_3d_overlay.py +++ b/mne/viz/_3d_overlay.py @@ -22,9 +22,10 @@ def __init__(self, scalars, colormap, rng, opacity, name): self._name = name def to_colors(self): - from ._3d import _get_cmap from matplotlib.colors import Colormap, ListedColormap + from ._3d import _get_cmap + if isinstance(self._colormap, str): cmap = _get_cmap(self._colormap) elif isinstance(self._colormap, Colormap): diff --git a/mne/viz/__init__.pyi b/mne/viz/__init__.pyi index b709ebc2a05..dfebec1f5dc 100644 --- a/mne/viz/__init__.pyi +++ b/mne/viz/__init__.pyi @@ -86,91 +86,91 @@ __all__ = [ "use_3d_backend", "use_browser_backend", ] -from . import backends, _scraper, ui_events +from . import _scraper, backends, ui_events +from ._3d import ( + link_brains, + plot_alignment, + plot_brain_colorbar, + plot_dipole_locations, + plot_evoked_field, + plot_head_positions, + plot_source_estimates, + plot_sparse_source_estimates, + plot_vector_source_estimates, + plot_volume_source_estimates, + set_3d_options, + snapshot_brain_montage, +) +from ._brain import Brain +from ._figure import get_browser_backend, set_browser_backend, use_browser_backend +from ._proj import plot_projs_joint from .backends._abstract import Figure3D from .backends.renderer import ( - set_3d_backend, - get_3d_backend, - use_3d_backend, - set_3d_view, - set_3d_title, - create_3d_figure, close_3d_figure, close_all_3d_figures, + create_3d_figure, + get_3d_backend, get_brain_class, + set_3d_backend, + set_3d_title, + set_3d_view, + use_3d_backend, ) from .circle import circular_layout, plot_channel_labels_circle -from .epochs import plot_drop_log, plot_epochs, plot_epochs_psd, plot_epochs_image +from .epochs import plot_drop_log, plot_epochs, plot_epochs_image, plot_epochs_psd from .evoked import ( + plot_compare_evokeds, plot_evoked, plot_evoked_image, + plot_evoked_joint, + plot_evoked_topo, plot_evoked_white, plot_snr_estimate, - plot_evoked_topo, - plot_evoked_joint, - plot_compare_evokeds, ) from .evoked_field import EvokedField from .ica import ( - plot_ica_scores, - plot_ica_sources, - plot_ica_overlay, _plot_sources, + plot_ica_overlay, plot_ica_properties, + plot_ica_scores, + plot_ica_sources, ) from .misc import ( + _get_presser, + adjust_axes, + plot_bem, + plot_chpi_snr, plot_cov, plot_csd, - plot_bem, - plot_events, - plot_source_spectrogram, - _get_presser, plot_dipole_amplitudes, - plot_ideal_filter, + plot_events, plot_filter, - adjust_axes, - plot_chpi_snr, + plot_ideal_filter, + plot_source_spectrogram, ) from .montage import plot_montage -from .raw import plot_raw, plot_raw_psd, plot_raw_psd_topo, _RAW_CLIP_DEF -from .topo import plot_topo_image_epochs, iter_topography +from .raw import _RAW_CLIP_DEF, plot_raw, plot_raw_psd, plot_raw_psd_topo +from .topo import iter_topography, plot_topo_image_epochs from .topomap import ( - plot_evoked_topomap, - plot_projs_topomap, plot_arrowmap, - plot_ica_components, - plot_tfr_topomap, - plot_topomap, - plot_epochs_psd_topomap, - plot_layout, plot_bridged_electrodes, plot_ch_adjacency, + plot_epochs_psd_topomap, + plot_evoked_topomap, + plot_ica_components, + plot_layout, + plot_projs_topomap, plot_regression_weights, + plot_tfr_topomap, + plot_topomap, ) from .utils import ( - mne_analyze_colormap, - compare_fiff, ClickableImage, + _get_plot_ch_type, add_background_image, - plot_sensors, centers_to_edges, + compare_fiff, concatenate_images, - _get_plot_ch_type, -) -from ._3d import ( - plot_sparse_source_estimates, - plot_source_estimates, - plot_vector_source_estimates, - plot_evoked_field, - plot_dipole_locations, - snapshot_brain_montage, - plot_head_positions, - plot_alignment, - plot_brain_colorbar, - plot_volume_source_estimates, - link_brains, - set_3d_options, + mne_analyze_colormap, + plot_sensors, ) -from ._brain import Brain -from ._figure import get_browser_backend, set_browser_backend, use_browser_backend -from ._proj import plot_projs_joint diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 4c4aeb531ba..80c9d313924 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -7,89 +7,87 @@ # # License: Simplified BSD -from functools import partial -from io import BytesIO +import copy import os import os.path as op import time -import copy import traceback import warnings +from functools import partial +from io import BytesIO import numpy as np from scipy.interpolate import interp1d from scipy.sparse import csr_matrix from scipy.spatial.distance import cdist -from .colormap import calculate_lut -from .surface import _Surface -from .view import views_dicts, _lh_views_dict - -from ..utils import ( - _show_help_fig, - _get_color_list, - concatenate_images, - _generate_default_filename, - _save_ndarray_img, - safe_event, -) -from .._3d import ( - _process_clim, - _handle_time, - _check_views, - _handle_sensor_types, - _plot_sensors, - _plot_forward, -) -from .._3d_overlay import _LayeredMesh -from ...defaults import _handle_default, DEFAULTS +from ..._fiff.meas_info import Info +from ..._fiff.pick import pick_types from ..._freesurfer import ( - vertex_to_mni, - read_talxfm, - read_freesurfer_lut, + _estimate_talxfm_rigid, _get_head_surface, _get_skull_surface, - _estimate_talxfm_rigid, + read_freesurfer_lut, + read_talxfm, + vertex_to_mni, ) -from ..._fiff.pick import pick_types -from ..._fiff.meas_info import Info -from ...surface import mesh_edges, _mesh_borders, _marching_cubes, get_meg_helmet_surf +from ...defaults import DEFAULTS, _handle_default +from ...surface import _marching_cubes, _mesh_borders, get_meg_helmet_surf, mesh_edges from ...transforms import ( Transform, - apply_trans, _frame_to_str, _get_trans, _get_transforms_to_coord_frame, + apply_trans, ) from ...utils import ( - _check_option, - logger, - verbose, - fill_doc, - _validate_type, - use_log_level, Bunch, - _ReuseCycle, - warn, - get_subjects_dir, + _auto_weakref, _check_fname, - _to_rgb, + _check_option, _ensure_int, - _auto_weakref, _path_like, + _ReuseCycle, + _to_rgb, + _validate_type, + fill_doc, + get_subjects_dir, + logger, + use_log_level, + verbose, + warn, ) - +from .._3d import ( + _check_views, + _handle_sensor_types, + _handle_time, + _plot_forward, + _plot_sensors, + _process_clim, +) +from .._3d_overlay import _LayeredMesh from ..ui_events import ( - publish, - subscribe, - unsubscribe, - TimeChange, - PlaybackSpeed, ColormapRange, + PlaybackSpeed, + TimeChange, VertexSelect, - disable_ui_events, _get_event_channel, + disable_ui_events, + publish, + subscribe, + unsubscribe, ) +from ..utils import ( + _generate_default_filename, + _get_color_list, + _save_ndarray_img, + _show_help_fig, + concatenate_images, + safe_event, +) +from .colormap import calculate_lut +from .surface import _Surface +from .view import _lh_views_dict, views_dicts @fill_doc @@ -307,7 +305,7 @@ def __init__( show=True, block=False, ): - from ..backends.renderer import backend, _get_renderer + from ..backends.renderer import _get_renderer, backend _validate_type(subject, str, "subject") self._surf = surf @@ -987,8 +985,8 @@ def _set_label_mode(mode): self.mpl_canvas.update_plot() self._renderer._update() - from ...source_estimate import _get_allowed_label_modes from ...label import _read_annot_cands + from ...source_estimate import _get_allowed_label_modes dir_name = op.join(self._subjects_dir, self._subject, "label") cands = _read_annot_cands(dir_name, raise_error=False) @@ -1677,9 +1675,10 @@ def interaction(self, interaction): def _cortex_colormap(self, cortex): """Return the colormap corresponding to the cortex.""" - from .._3d import _get_cmap from matplotlib.colors import ListedColormap + from .._3d import _get_cmap + colormap_map = dict( classic=dict(colormap="Greys", vmin=-1, vmax=2), high_contrast=dict(colormap="Greys", vmin=-0.1, vmax=1.3), @@ -2053,8 +2052,8 @@ def remove_annotations(self): self._renderer._update() def _add_volume_data(self, hemi, src, volume_options): - from ..backends._pyvista import _hide_testing_actor from ...source_space import SourceSpaces + from ..backends._pyvista import _hide_testing_actor _validate_type(src, SourceSpaces, "src") _check_option("src.kind", src.kind, ("volume",)) @@ -4012,9 +4011,9 @@ def _iter_time(self, time_idx, callback): def _check_stc(self, hemi, array, vertices): from ...source_estimate import ( + _BaseMixedSourceEstimate, _BaseSourceEstimate, _BaseSurfaceSourceEstimate, - _BaseMixedSourceEstimate, _BaseVolSourceEstimate, ) diff --git a/mne/viz/_brain/_linkviewer.py b/mne/viz/_brain/_linkviewer.py index a69311db003..df2edb79bd2 100644 --- a/mne/viz/_brain/_linkviewer.py +++ b/mne/viz/_brain/_linkviewer.py @@ -4,6 +4,7 @@ # # License: Simplified BSD import numpy as np + from ...utils import warn from ..ui_events import link diff --git a/mne/viz/_brain/_scraper.py b/mne/viz/_brain/_scraper.py index 8c2aa7ed96f..08ad985e7b5 100644 --- a/mne/viz/_brain/_scraper.py +++ b/mne/viz/_brain/_scraper.py @@ -18,7 +18,8 @@ def __call__(self, block, block_vars, gallery_conf): # PyVista and matplotlib scrapers can just do the work if (not isinstance(brain, Brain)) or brain._closed: continue - from matplotlib import animation, pyplot as plt + from matplotlib import animation + from matplotlib import pyplot as plt from sphinx_gallery.scrapers import matplotlib_scraper img = brain.screenshot(time_viewer=True) diff --git a/mne/viz/_brain/surface.py b/mne/viz/_brain/surface.py index ce7bb9c974a..919d6f2aa09 100644 --- a/mne/viz/_brain/surface.py +++ b/mne/viz/_brain/surface.py @@ -9,8 +9,9 @@ from os import path as path import numpy as np -from ...utils import _check_option, get_subjects_dir, _check_fname, _validate_type -from ...surface import complete_surface_info, read_surface, read_curvature, _read_patch + +from ...surface import _read_patch, complete_surface_info, read_curvature, read_surface +from ...utils import _check_fname, _check_option, _validate_type, get_subjects_dir class _Surface: diff --git a/mne/viz/_brain/tests/test_brain.py b/mne/viz/_brain/tests/test_brain.py index e9ea19c748d..9f7f7c1cac5 100644 --- a/mne/viz/_brain/tests/test_brain.py +++ b/mne/viz/_brain/tests/test_brain.py @@ -12,39 +12,38 @@ from pathlib import Path from shutil import copyfile -import pytest import numpy as np +import pytest +from matplotlib import image +from matplotlib.lines import Line2D from numpy.testing import assert_allclose, assert_array_equal from mne import ( - read_source_estimate, - read_evokeds, - read_cov, - read_forward_solution, - pick_types_forward, - SourceEstimate, + Dipole, MixedSourceEstimate, - write_surface, + SourceEstimate, VolSourceEstimate, - vertex_to_mni, - Dipole, create_info, + pick_types_forward, + read_cov, + read_evokeds, + read_forward_solution, + read_source_estimate, + vertex_to_mni, + write_surface, ) from mne.channels import make_dig_montage +from mne.datasets import testing +from mne.io import read_info +from mne.label import read_label from mne.minimum_norm import apply_inverse, make_inverse_operator from mne.source_estimate import _BaseSourceEstimate from mne.source_space import read_source_spaces, setup_volume_source_space -from mne.datasets import testing -from mne.io import read_info from mne.utils import check_version -from mne.label import read_label -from mne.viz._brain import Brain, _LinkViewer, _BrainScraper, _LayeredMesh +from mne.viz import ui_events +from mne.viz._brain import Brain, _BrainScraper, _LayeredMesh, _LinkViewer from mne.viz._brain.colormap import calculate_lut from mne.viz.utils import _get_cmap -from mne.viz import ui_events - -from matplotlib import image -from matplotlib.lines import Line2D data_path = testing.data_path(download=False) subject = "sample" diff --git a/mne/viz/_brain/tests/test_notebook.py b/mne/viz/_brain/tests/test_notebook.py index 928f2eded53..f2da02bc467 100644 --- a/mne/viz/_brain/tests/test_notebook.py +++ b/mne/viz/_brain/tests/test_notebook.py @@ -5,6 +5,7 @@ # executed in a separate IPython kernel. import pytest + from mne.datasets import testing @@ -12,6 +13,7 @@ def test_notebook_alignment(renderer_notebook, brain_gc, nbexec): """Test plot alignment in a notebook.""" import pytest + import mne with pytest.MonkeyPatch().context() as mp: @@ -39,14 +41,16 @@ def test_notebook_alignment(renderer_notebook, brain_gc, nbexec): @testing.requires_testing_data def test_notebook_interactive(renderer_notebook, brain_gc, nbexec): """Test interactive modes.""" - from contextlib import contextmanager - from pathlib import Path import tempfile import time + from contextlib import contextmanager + from pathlib import Path + + import matplotlib.pyplot as plt import pytest - from numpy.testing import assert_allclose from ipywidgets import Button - import matplotlib.pyplot as plt + from numpy.testing import assert_allclose + import mne from mne.datasets import testing @@ -128,9 +132,10 @@ def interactive(on): @testing.requires_testing_data def test_notebook_button_counts(renderer_notebook, brain_gc, nbexec): """Test button counts.""" - import mne from ipywidgets import Button + import mne + mne.viz.set_3d_backend("notebook") rend = mne.viz.create_3d_figure(size=(100, 100), scene=False) fig = rend.scene() diff --git a/mne/viz/_dipole.py b/mne/viz/_dipole.py index 24fc4735f3c..809a7f45876 100644 --- a/mne/viz/_dipole.py +++ b/mne/viz/_dipole.py @@ -9,11 +9,11 @@ import numpy as np from scipy.spatial import ConvexHull -from .utils import plt_show, _validate_if_list_of_axes -from .._freesurfer import _get_head_surface, _estimate_talxfm_rigid +from .._freesurfer import _estimate_talxfm_rigid, _get_head_surface from ..surface import read_surface -from ..transforms import apply_trans, invert_transform, _get_trans -from ..utils import _validate_type, _check_option, get_subjects_dir +from ..transforms import _get_trans, apply_trans, invert_transform +from ..utils import _check_option, _validate_type, get_subjects_dir +from .utils import _validate_if_list_of_axes, plt_show def _check_concat_dipoles(dipole): @@ -41,9 +41,9 @@ def _plot_dipole_mri_outlines( surf, width, ): + import matplotlib.pyplot as plt from matplotlib.collections import LineCollection, PatchCollection from matplotlib.patches import Circle - import matplotlib.pyplot as plt extra = 'when mode is "outlines"' trans = _get_trans(trans, fro="head", to="mri")[0] diff --git a/mne/viz/_figure.py b/mne/viz/_figure.py index 738bf838ce3..f53e079c6b5 100644 --- a/mne/viz/_figure.py +++ b/mne/viz/_figure.py @@ -13,15 +13,21 @@ import numpy as np -from .backends._utils import VALID_BROWSE_BACKENDS -from .utils import _get_color_list, _setup_plot_projector, _show_browser - -from ..defaults import _handle_default -from ..filter import _overlap_add_filter, _iir_filter -from ..utils import logger, _validate_type, _check_option from .._fiff.pick import _DATA_CH_TYPES_SPLIT -from ..utils import verbose, get_config, set_config, _get_stim_channel +from ..defaults import _handle_default +from ..filter import _iir_filter, _overlap_add_filter from ..fixes import _compare_version +from ..utils import ( + _check_option, + _get_stim_channel, + _validate_type, + get_config, + logger, + set_config, + verbose, +) +from .backends._utils import VALID_BROWSE_BACKENDS +from .utils import _get_color_list, _setup_plot_projector, _show_browser MNE_BROWSER_BACKEND = None backend = None @@ -498,8 +504,8 @@ def _create_ica_properties_fig(self, idx): """Show ICA properties for the selected component.""" from mne.viz.ica import ( _create_properties_layout, - _prepare_data_ica_properties, _fast_plot_ica_properties, + _prepare_data_ica_properties, ) ch_name = self.mne.ch_names[idx] @@ -529,6 +535,7 @@ def _create_ica_properties_fig(self, idx): def _create_epoch_image_fig(self, pick): """Show epochs image for the selected channel.""" from matplotlib.gridspec import GridSpec + from mne.viz import plot_epochs_image ch_name = self.mne.ch_names[pick] @@ -663,6 +670,7 @@ def _get_browser(show, block, **kwargs): # Check mne-qt-browser compatibility if backend_name == "qt": import mne_qt_browser + from ..epochs import BaseEpochs is_ica = kwargs.get("ica", False) diff --git a/mne/viz/_mpl_figure.py b/mne/viz/_mpl_figure.py index c313bfe1edf..2149aa73d1f 100644 --- a/mne/viz/_mpl_figure.py +++ b/mne/viz/_mpl_figure.py @@ -47,30 +47,30 @@ from matplotlib import get_backend from matplotlib.figure import Figure -from ..fixes import _close_event from .._fiff.pick import ( _DATA_CH_TYPES_ORDER_DEFAULT, _DATA_CH_TYPES_SPLIT, - _FNIRS_CH_TYPES_SPLIT, _EYETRACK_CH_TYPES_SPLIT, + _FNIRS_CH_TYPES_SPLIT, _VALID_CHANNEL_TYPES, channel_indices_by_type, pick_types, ) -from ..utils import Bunch, _click_ch_name, logger, check_version +from ..fixes import _close_event +from ..utils import Bunch, _click_ch_name, check_version, logger from ._figure import BrowserBase from .utils import ( DraggableLine, _events_off, _fake_click, _fake_keypress, + _fake_scroll, _merge_annotations, _prop_kw, _set_window_title, _validate_if_list_of_axes, - plt_show, - _fake_scroll, plot_sensors, + plt_show, ) name = "matplotlib" @@ -791,6 +791,7 @@ def _keypress(self, event): def _buttonpress(self, event): """Handle mouse clicks.""" from matplotlib.collections import PolyCollection + from ..annotations import _sync_onset butterfly = self.mne.butterfly diff --git a/mne/viz/_proj.py b/mne/viz/_proj.py index 0f8f02a3089..0493d0ce8fb 100644 --- a/mne/viz/_proj.py +++ b/mne/viz/_proj.py @@ -8,12 +8,12 @@ import numpy as np +from .._fiff.pick import _picks_to_idx +from ..defaults import DEFAULTS +from ..utils import _pl, _validate_type, verbose, warn from .evoked import _plot_evoked from .topomap import _plot_projs_topomap -from .utils import plt_show, _check_type_projs -from ..defaults import DEFAULTS -from .._fiff.pick import _picks_to_idx -from ..utils import _validate_type, warn, _pl, verbose +from .utils import _check_type_projs, plt_show @verbose @@ -62,6 +62,7 @@ def plot_projs_joint( .. versionadded:: 1.1 """ import matplotlib.pyplot as plt + from ..evoked import Evoked _validate_type(evoked, Evoked, "evoked") diff --git a/mne/viz/backends/_abstract.py b/mne/viz/backends/_abstract.py index e924e7deae9..f6520244c3a 100644 --- a/mne/viz/backends/_abstract.py +++ b/mne/viz/backends/_abstract.py @@ -6,10 +6,10 @@ # # License: Simplified BSD -from abc import ABC, abstractmethod, abstractclassmethod import warnings +from abc import ABC, abstractclassmethod, abstractmethod -from ..ui_events import publish, TimeChange +from ..ui_events import TimeChange, publish class Figure3D(ABC): diff --git a/mne/viz/backends/_notebook.py b/mne/viz/backends/_notebook.py index 1c53c968b55..319d294a8a5 100644 --- a/mne/viz/backends/_notebook.py +++ b/mne/viz/backends/_notebook.py @@ -10,85 +10,83 @@ import re from contextlib import contextmanager, nullcontext -from IPython.display import display, clear_output +from ipyevents import Event +from IPython.display import clear_output, display from ipywidgets import ( - Widget, - HBox, - VBox, + HTML, + Accordion, + BoundedFloatText, Button, + Checkbox, Dropdown, + # non-object-based-abstraction-only widgets, deprecate + FloatSlider, + GridBox, + HBox, + IntProgress, IntSlider, IntText, - Text, - IntProgress, - Play, Label, - HTML, - Checkbox, - RadioButtons, - Accordion, - link, Layout, + Play, + RadioButtons, Select, - GridBox, - # non-object-based-abstraction-only widgets, deprecate - FloatSlider, - BoundedFloatText, + Text, + VBox, + Widget, jsdlink, + link, ) -from ipyevents import Event -from .renderer import _TimeInteraction +from ...utils import _soft_import, check_version from ._abstract import ( + _AbstractAction, _AbstractAppWindow, - _AbstractHBoxLayout, - _AbstractVBoxLayout, - _AbstractGridLayout, - _AbstractWidget, - _AbstractCanvas, - _AbstractPopup, - _AbstractLabel, + _AbstractBrainMplCanvas, _AbstractButton, - _AbstractSlider, + _AbstractCanvas, _AbstractCheckBox, - _AbstractSpinBox, _AbstractComboBox, - _AbstractRadioButtons, - _AbstractGroupBox, - _AbstractText, + _AbstractDialog, + _AbstractDock, _AbstractFileButton, + _AbstractGridLayout, + _AbstractGroupBox, + _AbstractHBoxLayout, + _AbstractKeyPress, + _AbstractLabel, + _AbstractLayout, + _AbstractMenuBar, + _AbstractMplCanvas, + _AbstractMplInterface, + _AbstractPlayback, _AbstractPlayMenu, + _AbstractPopup, _AbstractProgressBar, -) -from ._abstract import ( - _AbstractDock, - _AbstractToolBar, - _AbstractMenuBar, + _AbstractRadioButtons, + _AbstractSlider, + _AbstractSpinBox, _AbstractStatusBar, - _AbstractLayout, + _AbstractText, + _AbstractToolBar, + _AbstractVBoxLayout, _AbstractWdgt, - _AbstractWindow, - _AbstractMplCanvas, - _AbstractPlayback, - _AbstractBrainMplCanvas, - _AbstractMplInterface, + _AbstractWidget, _AbstractWidgetList, - _AbstractAction, - _AbstractDialog, - _AbstractKeyPress, + _AbstractWindow, ) -from ._pyvista import _PyVistaRenderer, Plotter from ._pyvista import ( - _close_3d_figure, # noqa: F401 + Plotter, _check_3d_figure, # noqa: F401 + _close_3d_figure, # noqa: F401 _close_all, # noqa: F401 - _set_3d_view, # noqa: F401 + _PyVistaRenderer, _set_3d_title, # noqa: F401 + _set_3d_view, # noqa: F401 _take_3d_screenshot, # noqa: F401 ) from ._utils import _notebook_vtk_works -from ...utils import check_version, _soft_import - +from .renderer import _TimeInteraction # dict values are icon names from: https://fontawesome.com/icons _ICON_LUT = dict( diff --git a/mne/viz/backends/_pyvista.py b/mne/viz/backends/_pyvista.py index 700ff9e6870..b09314462dc 100644 --- a/mne/viz/backends/_pyvista.py +++ b/mne/viz/backends/_pyvista.py @@ -11,37 +11,36 @@ # # License: Simplified BSD -from contextlib import contextmanager -from inspect import signature import platform import re import warnings +from contextlib import contextmanager +from inspect import signature import numpy as np -from ._abstract import _AbstractRenderer, Figure3D -from ._utils import ( - _get_colormap_from_array, - _alpha_blend_background, - ALLOWED_QUIVER_MODES, - _init_mne_qtapp, -) from ...fixes import _compare_version -from ...transforms import apply_trans, _cart_to_sph, _sph_to_cart +from ...transforms import _cart_to_sph, _sph_to_cart, apply_trans from ...utils import ( - copy_base_doc_to_subclass_doc, _check_option, _require_version, _validate_type, - warn, + copy_base_doc_to_subclass_doc, deprecated, + warn, +) +from ._abstract import Figure3D, _AbstractRenderer +from ._utils import ( + ALLOWED_QUIVER_MODES, + _alpha_blend_background, + _get_colormap_from_array, + _init_mne_qtapp, ) - with warnings.catch_warnings(): warnings.filterwarnings("ignore", category=DeprecationWarning) import pyvista - from pyvista import Plotter, PolyData, Line, close_all, UnstructuredGrid + from pyvista import Line, Plotter, PolyData, UnstructuredGrid, close_all from pyvistaqt import BackgroundPlotter try: @@ -49,36 +48,36 @@ except Exception: # PV < 0.40 from pyvista.plotting.plotting import _ALL_PLOTTERS -from vtkmodules.vtkCommonCore import vtkCommand, vtkLookupTable, VTK_UNSIGNED_CHAR +from vtkmodules.util.numpy_support import numpy_to_vtk +from vtkmodules.vtkCommonCore import VTK_UNSIGNED_CHAR, vtkCommand, vtkLookupTable from vtkmodules.vtkCommonDataModel import VTK_VERTEX, vtkPiecewiseFunction from vtkmodules.vtkCommonTransforms import vtkTransform from vtkmodules.vtkFiltersCore import vtkCellDataToPointData, vtkGlyph3D from vtkmodules.vtkFiltersGeneral import ( - vtkTransformPolyDataFilter, vtkMarchingContourFilter, + vtkTransformPolyDataFilter, ) from vtkmodules.vtkFiltersHybrid import vtkPolyDataSilhouette from vtkmodules.vtkFiltersSources import ( - vtkSphereSource, + vtkArrowSource, vtkConeSource, vtkCylinderSource, - vtkArrowSource, - vtkPlatonicSolidSource, vtkGlyphSource2D, + vtkPlatonicSolidSource, + vtkSphereSource, ) from vtkmodules.vtkImagingCore import vtkImageReslice from vtkmodules.vtkRenderingCore import ( - vtkMapper, vtkActor, vtkCellPicker, vtkColorTransferFunction, - vtkPolyDataMapper, - vtkVolume, vtkCoordinate, vtkDataSetMapper, + vtkMapper, + vtkPolyDataMapper, + vtkVolume, ) from vtkmodules.vtkRenderingVolumeOpenGL2 import vtkSmartVolumeMapper -from vtkmodules.util.numpy_support import numpy_to_vtk _FIGURES = dict() diff --git a/mne/viz/backends/_qt.py b/mne/viz/backends/_qt.py index 5f33920f3b4..7bb87537d10 100644 --- a/mne/viz/backends/_qt.py +++ b/mne/viz/backends/_qt.py @@ -6,121 +6,121 @@ # # License: Simplified BSD -from contextlib import contextmanager import os import platform import sys import weakref +from contextlib import contextmanager + +# importing anything from qtpy forces a Qt API choice as a side effect, which is then +# used by matplotlib and pyvistaqt +from qtpy import API_NAME # noqa: F401, isort: skip import pyvista -from pyvistaqt.plotting import FileDialog, MainWindow -from .renderer import _TimeInteraction +from matplotlib.backends.backend_qtagg import FigureCanvas from matplotlib.figure import Figure -from matplotlib.backends.backend_qt5agg import FigureCanvas - +from pyvistaqt.plotting import FileDialog, MainWindow from qtpy.QtCore import ( + QEvent, + QLibraryInfo, + QLocale, + QObject, Qt, QTimer, - QLocale, - QLibraryInfo, - QEvent, # non-object-based-abstraction-only, deprecate Signal, - QObject, ) -from qtpy.QtGui import QIcon, QCursor, QKeyEvent +from qtpy.QtGui import QCursor, QIcon, QKeyEvent from qtpy.QtWidgets import ( + QButtonGroup, + QCheckBox, QComboBox, + # non-object-based-abstraction-only, deprecate + QDockWidget, + QDoubleSpinBox, + QFileDialog, + QGridLayout, QGroupBox, QHBoxLayout, QLabel, - QSlider, - QDoubleSpinBox, - QVBoxLayout, - QWidget, - QSizePolicy, - QProgressBar, - QScrollArea, QLayout, - QCheckBox, - QButtonGroup, - QRadioButton, QLineEdit, - QGridLayout, - QFileDialog, - QPushButton, - QMessageBox, - # non-object-based-abstraction-only, deprecate - QDockWidget, - QToolButton, QMenuBar, + QMessageBox, + QProgressBar, + QPushButton, + QRadioButton, + QScrollArea, + QSizePolicy, + QSlider, QSpinBox, QStyle, QStyleOptionSlider, + QToolButton, + QVBoxLayout, + QWidget, ) -from ._pyvista import _PyVistaRenderer -from ._pyvista import ( - _close_3d_figure, # noqa: F401 - _check_3d_figure, # noqa: F401 - _close_all, # noqa: F401 - _set_3d_view, # noqa: F401 - _set_3d_title, # noqa: F401 - _take_3d_screenshot, # noqa: F401 - _is_mesa, # noqa: F401 -) +from ...fixes import _compare_version +from ...utils import _check_option, get_config +from ..utils import safe_event from ._abstract import ( + _AbstractAction, _AbstractAppWindow, - _AbstractHBoxLayout, - _AbstractVBoxLayout, - _AbstractGridLayout, - _AbstractWidget, - _AbstractCanvas, - _AbstractPopup, - _AbstractLabel, + _AbstractBrainMplCanvas, _AbstractButton, - _AbstractSlider, + _AbstractCanvas, _AbstractCheckBox, - _AbstractSpinBox, _AbstractComboBox, - _AbstractRadioButtons, - _AbstractGroupBox, - _AbstractText, + _AbstractDialog, + _AbstractDock, _AbstractFileButton, + _AbstractGridLayout, + _AbstractGroupBox, + _AbstractHBoxLayout, + _AbstractKeyPress, + _AbstractLabel, + _AbstractLayout, + _AbstractMenuBar, + _AbstractMplCanvas, + _AbstractMplInterface, + _AbstractPlayback, _AbstractPlayMenu, + _AbstractPopup, _AbstractProgressBar, -) -from ._abstract import ( - _AbstractDock, - _AbstractToolBar, - _AbstractMenuBar, + _AbstractRadioButtons, + _AbstractSlider, + _AbstractSpinBox, _AbstractStatusBar, - _AbstractLayout, + _AbstractText, + _AbstractToolBar, + _AbstractVBoxLayout, _AbstractWdgt, - _AbstractWindow, - _AbstractMplCanvas, - _AbstractPlayback, - _AbstractBrainMplCanvas, - _AbstractMplInterface, + _AbstractWidget, _AbstractWidgetList, - _AbstractAction, - _AbstractDialog, - _AbstractKeyPress, + _AbstractWindow, +) +from ._pyvista import ( + _check_3d_figure, # noqa: F401 + _close_3d_figure, # noqa: F401 + _close_all, # noqa: F401 + _is_mesa, # noqa: F401 + _PyVistaRenderer, + _set_3d_title, # noqa: F401 + _set_3d_view, # noqa: F401 + _take_3d_screenshot, # noqa: F401 ) from ._utils import ( + _init_mne_qtapp, + _qt_app_exec, + _qt_detect_theme, _qt_disable_paint, _qt_get_stylesheet, _qt_is_dark, - _qt_detect_theme, _qt_raise_window, - _init_mne_qtapp, - _qt_app_exec, _qt_safe_window, ) -from ..utils import safe_event -from ...utils import _check_option, get_config -from ...fixes import _compare_version - +from .renderer import _TimeInteraction # Adapted from matplotlib if ( @@ -1416,8 +1416,8 @@ def _playback_initialize(self, func, timeout, value, rng, time_widget, play_widg class _QtMplInterface(_AbstractMplInterface): def _mpl_initialize(self): - from qtpy import QtWidgets from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg + from qtpy import QtWidgets self.canvas = FigureCanvasQTAgg(self.fig) FigureCanvasQTAgg.setSizePolicy( diff --git a/mne/viz/backends/_utils.py b/mne/viz/backends/_utils.py index afb66f09213..b114e99e349 100644 --- a/mne/viz/backends/_utils.py +++ b/mne/viz/backends/_utils.py @@ -5,21 +5,21 @@ # Guillaume Favelier # # License: Simplified BSD -from ctypes import cdll, c_void_p, c_char_p import collections.abc -from colorsys import rgb_to_hls -from contextlib import contextmanager import functools import os import platform import signal import sys - +from colorsys import rgb_to_hls +from contextlib import contextmanager +from ctypes import c_char_p, c_void_p, cdll from pathlib import Path + import numpy as np from ...fixes import _compare_version -from ...utils import logger, warn, _validate_type, _check_qt_version +from ...utils import _check_qt_version, _validate_type, logger, warn from ..utils import _get_cmap VALID_BROWSE_BACKENDS = ( @@ -131,7 +131,7 @@ def _init_mne_qtapp(enable_icon=True, pg_app=False, splash=False): string. """ from qtpy.QtCore import Qt - from qtpy.QtGui import QIcon, QPixmap, QGuiApplication + from qtpy.QtGui import QGuiApplication, QIcon, QPixmap from qtpy.QtWidgets import QApplication, QSplashScreen app_name = "MNE-Python" diff --git a/mne/viz/backends/renderer.py b/mne/viz/backends/renderer.py index 960e32978f3..e0120e5ae70 100644 --- a/mne/viz/backends/renderer.py +++ b/mne/viz/backends/renderer.py @@ -7,25 +7,25 @@ # # License: Simplified BSD -from contextlib import contextmanager import importlib -from functools import partial import time +from contextlib import contextmanager +from functools import partial import numpy as np -from ._utils import VALID_3D_BACKENDS -from .._3d import _get_3d_option -from ..utils import safe_event from ...utils import ( - logger, - verbose, - get_config, + _auto_weakref, _check_option, - fill_doc, _validate_type, - _auto_weakref, + fill_doc, + get_config, + logger, + verbose, ) +from .._3d import _get_3d_option +from ..utils import safe_event +from ._utils import VALID_3D_BACKENDS MNE_3D_BACKEND = None MNE_3D_BACKEND_TESTING = False @@ -398,10 +398,10 @@ def _enable_time_interaction( playback_speed_range=[0.01, 0.1], ): from ..ui_events import ( + PlaybackSpeed, + TimeChange, publish, subscribe, - TimeChange, - PlaybackSpeed, ) self._fig = fig @@ -521,7 +521,7 @@ def _on_playback_speed(self, event): def _toggle_playback(self, value=None): """Toggle time playback.""" - from ..ui_events import publish, TimeChange + from ..ui_events import TimeChange, publish if value is None: self._playback = not self._playback @@ -538,7 +538,7 @@ def _toggle_playback(self, value=None): def _reset_time(self): """Reset time and playback speed to initial values.""" - from ..ui_events import publish, TimeChange, PlaybackSpeed + from ..ui_events import PlaybackSpeed, TimeChange, publish publish(self._fig, TimeChange(time=self._init_time)) publish(self._fig, PlaybackSpeed(speed=self._init_playback_speed)) @@ -553,7 +553,7 @@ def _play(self): raise def _advance(self): - from ..ui_events import publish, TimeChange + from ..ui_events import TimeChange, publish this_time = time.time() delta = this_time - self._last_tick diff --git a/mne/viz/backends/tests/_utils.py b/mne/viz/backends/tests/_utils.py index bac1b2a5c70..ca4961e784f 100644 --- a/mne/viz/backends/tests/_utils.py +++ b/mne/viz/backends/tests/_utils.py @@ -5,9 +5,10 @@ # # License: Simplified BSD -import pytest import warnings +import pytest + def has_pyvista(): """Check that PyVista is installed.""" diff --git a/mne/viz/backends/tests/test_abstract.py b/mne/viz/backends/tests/test_abstract.py index b5d72f03537..4130611a18a 100644 --- a/mne/viz/backends/tests/test_abstract.py +++ b/mne/viz/backends/tests/test_abstract.py @@ -117,10 +117,12 @@ def test_widget_abstraction_pyvistaqt(renderer_pyvistaqt): def test_widget_abstraction_notebook(renderer_notebook, nbexec): """Test the GUI widgets abstraction in notebook.""" from pathlib import Path + + from IPython import get_ipython + from mne.viz import set_3d_backend from mne.viz.backends.renderer import _get_backend from mne.viz.backends.tests.test_abstract import _do_widget_tests - from IPython import get_ipython set_3d_backend("notebook") backend = _get_backend() diff --git a/mne/viz/backends/tests/test_renderer.py b/mne/viz/backends/tests/test_renderer.py index eef1b7c459f..9de7862a597 100644 --- a/mne/viz/backends/tests/test_renderer.py +++ b/mne/viz/backends/tests/test_renderer.py @@ -9,14 +9,14 @@ import platform import sys -import pytest import numpy as np +import pytest from mne.utils import run_subprocess -from mne.viz import set_3d_backend, get_3d_backend, Figure3D +from mne.viz import Figure3D, get_3d_backend, set_3d_backend +from mne.viz.backends._utils import ALLOWED_QUIVER_MODES from mne.viz.backends.renderer import _get_renderer from mne.viz.backends.tests._utils import skips_if_not_pyvistaqt -from mne.viz.backends._utils import ALLOWED_QUIVER_MODES @pytest.mark.parametrize( diff --git a/mne/viz/backends/tests/test_utils.py b/mne/viz/backends/tests/test_utils.py index cfa0c65535f..b5ff72fc584 100644 --- a/mne/viz/backends/tests/test_utils.py +++ b/mne/viz/backends/tests/test_utils.py @@ -5,27 +5,27 @@ # # License: Simplified BSD +import platform from colorsys import rgb_to_hls from contextlib import nullcontext -import platform import numpy as np import pytest from mne import create_info from mne.io import RawArray +from mne.utils import _check_qt_version from mne.viz.backends._utils import ( - _get_colormap_from_array, _check_color, - _qt_is_dark, + _get_colormap_from_array, _pixmap_to_ndarray, + _qt_is_dark, ) -from mne.utils import _check_qt_version def test_get_colormap_from_array(): """Test setting a colormap.""" - from matplotlib.colors import ListedColormap, LinearSegmentedColormap + from matplotlib.colors import LinearSegmentedColormap, ListedColormap cmap = _get_colormap_from_array() assert isinstance(cmap, LinearSegmentedColormap) diff --git a/mne/viz/circle.py b/mne/viz/circle.py index 983eef69c5c..2877bebe382 100644 --- a/mne/viz/circle.py +++ b/mne/viz/circle.py @@ -7,13 +7,13 @@ # License: Simplified BSD -from itertools import cycle from functools import partial +from itertools import cycle import numpy as np -from .utils import plt_show, _get_cmap from ..utils import _validate_type +from .utils import _get_cmap, plt_show def circular_layout( @@ -154,9 +154,9 @@ def _plot_connectivity_circle( node_linewidth=2.0, show=True, ): - import matplotlib.pyplot as plt - import matplotlib.path as m_path import matplotlib.patches as m_patches + import matplotlib.path as m_path + import matplotlib.pyplot as plt from matplotlib.projections.polar import PolarAxes _validate_type(ax, (None, PolarAxes)) diff --git a/mne/viz/conftest.py b/mne/viz/conftest.py index ae4f6f5a979..0b46923ffc6 100644 --- a/mne/viz/conftest.py +++ b/mne/viz/conftest.py @@ -4,16 +4,16 @@ # # License: BSD-3-Clause -import pytest -import numpy as np import os.path as op -from mne import create_info, EvokedArray, events_from_annotations, Epochs +import numpy as np +import pytest + +from mne import Epochs, EvokedArray, create_info, events_from_annotations from mne.channels import make_standard_montage -from mne.datasets.testing import data_path, _pytest_param +from mne.datasets.testing import _pytest_param, data_path from mne.io import read_raw_nirx -from mne.preprocessing.nirs import optical_density, beer_lambert_law - +from mne.preprocessing.nirs import beer_lambert_law, optical_density fname_nirx = op.join( data_path(download=False), "NIRx", "nirscout", "nirx_15_2_recording_w_overlap" diff --git a/mne/viz/epochs.py b/mne/viz/epochs.py index 7bd1785ada9..5918d6d6aec 100644 --- a/mne/viz/epochs.py +++ b/mne/viz/epochs.py @@ -17,33 +17,32 @@ import numpy as np from scipy.ndimage import gaussian_filter1d -from .raw import _setup_channel_selections -from ..fixes import _sharex -from ..defaults import _handle_default -from ..utils import legacy, verbose, logger, warn, fill_doc, _check_option -from ..utils.spectrum import _split_psd_kwargs from .._fiff.meas_info import create_info - from .._fiff.pick import ( - _picks_to_idx, _DATA_CH_TYPES_SPLIT, _VALID_CHANNEL_TYPES, + _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 from .utils import ( - _setup_vmin_vmax, - plt_show, + DraggableColorbar, _check_cov, - _handle_precompute, _compute_scalings, - DraggableColorbar, - _setup_cmap, + _get_channel_plotting_order, _handle_decim, - _set_title_multiple_electrodes, + _handle_precompute, _make_combine_callable, - _set_window_title, _make_event_color_dict, - _get_channel_plotting_order, + _set_title_multiple_electrodes, + _set_window_title, + _setup_cmap, + _setup_vmin_vmax, _validate_type, + plt_show, ) @@ -430,7 +429,7 @@ def plot_epochs_image( def _validate_fig_and_axes(fig, axes, group_by, evoked, colorbar, clear=False): """Check user-provided fig/axes compatibility with plot_epochs_image.""" - from matplotlib.pyplot import figure, Axes, subplot2grid + from matplotlib.pyplot import Axes, figure, subplot2grid n_axes = 1 + int(evoked) + int(colorbar) ax_names = ("image", "evoked", "colorbar") @@ -711,6 +710,7 @@ def plot_drop_log( The figure. """ import matplotlib.pyplot as plt + from ..epochs import _drop_log_stats percent = _drop_log_stats(drop_log, ignore) diff --git a/mne/viz/evoked.py b/mne/viz/evoked.py index 5886bb26db3..88340295d78 100644 --- a/mne/viz/evoked.py +++ b/mne/viz/evoked.py @@ -17,66 +17,65 @@ import numpy as np -from ..fixes import _is_last_row from .._fiff.pick import ( - channel_type, - _VALID_CHANNEL_TYPES, - channel_indices_by_type, _DATA_CH_TYPES_SPLIT, _PICK_TYPES_DATA_DICT, + _VALID_CHANNEL_TYPES, _picks_to_idx, + channel_indices_by_type, + channel_type, pick_info, ) from ..defaults import _handle_default -from .utils import ( - _draw_proj_checkbox, - _check_delayed_ssp, - plt_show, - _process_times, - DraggableColorbar, - _setup_cmap, - _setup_vmin_vmax, - _check_cov, - _make_combine_callable, - _validate_if_list_of_axes, - _triage_rank_sss, - _get_color_list, - _setup_ax_spines, - _setup_plot_projector, - _prepare_joint_axes, - _check_option, - _set_title_multiple_electrodes, - _check_time_unit, - _plot_masked_image, - _trim_ticks, - _set_window_title, - _prop_kw, - _get_cmap, -) +from ..fixes import _is_last_row from ..utils import ( - logger, + _check_ch_locs, + _check_if_nan, _clean_names, - warn, + _is_numeric, _pl, - verbose, + _to_rgb, _validate_type, - _check_if_nan, - _check_ch_locs, fill_doc, - _is_numeric, - _to_rgb, + logger, + verbose, + warn, ) - from .topo import _plot_evoked_topo from .topomap import ( - _prepare_topomap_plot, - plot_topomap, - _get_pos_outlines, + _check_sphere, _draw_outlines, + _get_pos_outlines, + _make_head_outlines, _prepare_topomap, + _prepare_topomap_plot, _set_contour_locator, - _check_sphere, - _make_head_outlines, + plot_topomap, +) +from .utils import ( + DraggableColorbar, + _check_cov, + _check_delayed_ssp, + _check_option, + _check_time_unit, + _draw_proj_checkbox, + _get_cmap, + _get_color_list, + _make_combine_callable, + _plot_masked_image, + _prepare_joint_axes, + _process_times, + _prop_kw, + _set_title_multiple_electrodes, + _set_window_title, + _setup_ax_spines, + _setup_cmap, + _setup_plot_projector, + _setup_vmin_vmax, + _triage_rank_sss, + _trim_ticks, + _validate_if_list_of_axes, + plt_show, ) @@ -134,6 +133,7 @@ def _line_plot_onselect( ): """Draw topomaps from the selected area.""" import matplotlib.pyplot as plt + from ..channels.layout import _pair_grad_sensors ch_types = [type_ for type_ in ch_types if type_ in ("eeg", "grad", "mag")] @@ -607,7 +607,8 @@ def _plot_lines( highlight, ): """Plot data as butterfly plot.""" - from matplotlib import patheffects, pyplot as plt + from matplotlib import patheffects + from matplotlib import pyplot as plt from matplotlib.widgets import SpanSelector assert len(axes) == len(ch_types_used) @@ -1569,9 +1570,10 @@ def plot_evoked_white( covariance estimation and spatial whitening of MEG and EEG signals, vol. 108, 328-342, NeuroImage. """ - from ..cov import whiten_evoked, Covariance, _ensure_cov import matplotlib.pyplot as plt + from ..cov import Covariance, _ensure_cov, whiten_evoked + time_unit, times = _check_time_unit(time_unit, evoked.times) _validate_type(noise_cov, (list, tuple, Covariance, "path-like")) @@ -1768,6 +1770,7 @@ def plot_snr_estimate(evoked, inv, show=True, axes=None, verbose=None): .. versionadded:: 0.9.0 """ import matplotlib.pyplot as plt + from ..minimum_norm import estimate_snr snr, snr_est = estimate_snr(evoked, inv) @@ -2328,9 +2331,9 @@ def _evoked_sensor_legend(info, picks, ymin, ymax, show_sensors, ax, sphere): def _draw_colorbar_pce(ax, colors, cmap, colorbar_title, colorbar_ticks): """Draw colorbar for plot_compare_evokeds.""" - from mpl_toolkits.axes_grid1 import make_axes_locatable from matplotlib.colorbar import ColorbarBase from matplotlib.transforms import Bbox + from mpl_toolkits.axes_grid1 import make_axes_locatable # create colorbar axes orig_bbox = ax.get_position() @@ -2827,6 +2830,7 @@ def plot_compare_evokeds( +-------------+----------------+------------------------------------------+ """ import matplotlib.pyplot as plt + from ..evoked import Evoked, _check_evokeds_ch_names_times # build up evokeds into a dict, if it's not already @@ -3009,8 +3013,8 @@ def plot_compare_evokeds( if np.array(picks).ndim < 2: picks = [picks] # enables zipping w/ axes else: - from .topo import iter_topography from ..channels.layout import find_layout + from .topo import iter_topography fig = plt.figure(figsize=(18, 14), layout=None) # Not "constrained" for topo diff --git a/mne/viz/evoked_field.py b/mne/viz/evoked_field.py index 9e314a917ed..3757d2c00dd 100644 --- a/mne/viz/evoked_field.py +++ b/mne/viz/evoked_field.py @@ -3,34 +3,30 @@ author: Marijn van Vliet """ from functools import partial + import numpy as np from scipy.interpolate import interp1d - +from .._fiff.pick import pick_types +from ..defaults import DEFAULTS +from ..utils import ( + _auto_weakref, + _check_option, + _ensure_int, + _to_rgb, + _validate_type, + fill_doc, +) from ._3d_overlay import _LayeredMesh -from .utils import mne_analyze_colormap - from .ui_events import ( - publish, - subscribe, ColormapRange, Contours, TimeChange, disable_ui_events, + publish, + subscribe, ) - -from ..defaults import DEFAULTS - -from ..utils import ( - _ensure_int, - _validate_type, - _check_option, - _to_rgb, - _auto_weakref, - fill_doc, -) - -from .._fiff.pick import pick_types +from .utils import mne_analyze_colormap @fill_doc @@ -118,7 +114,7 @@ def __init__( time_viewer="auto", verbose=None, ): - from .backends.renderer import _get_renderer, _get_3d_backend + from .backends.renderer import _get_3d_backend, _get_renderer # Setup figure parameters self._evoked = evoked diff --git a/mne/viz/ica.py b/mne/viz/ica.py index d80ed9aec65..2b16c5f6837 100644 --- a/mne/viz/ica.py +++ b/mne/viz/ica.py @@ -7,34 +7,34 @@ # # License: Simplified BSD -from functools import partial import warnings +from functools import partial import numpy as np from scipy.stats import gaussian_kde -from .utils import ( - _make_event_color_dict, - _get_cmap, - plt_show, - _convert_psds, - _compute_scalings, - _handle_precompute, - _get_plot_ch_type, -) -from .topomap import _plot_ica_topomap -from .epochs import plot_epochs_image -from .evoked import _butterfly_on_button_press, _butterfly_onpick +from .._fiff.meas_info import create_info +from .._fiff.pick import _picks_to_idx, pick_types +from .._fiff.proj import _has_eeg_average_ref_proj +from ..defaults import DEFAULTS, _handle_default from ..utils import ( + _reject_data_segments, _validate_type, fill_doc, - _reject_data_segments, verbose, ) -from ..defaults import _handle_default, DEFAULTS -from .._fiff.meas_info import create_info -from .._fiff.pick import pick_types, _picks_to_idx -from .._fiff.proj import _has_eeg_average_ref_proj +from .epochs import plot_epochs_image +from .evoked import _butterfly_on_button_press, _butterfly_onpick +from .topomap import _plot_ica_topomap +from .utils import ( + _compute_scalings, + _convert_psds, + _get_cmap, + _get_plot_ch_type, + _handle_precompute, + _make_event_color_dict, + plt_show, +) @fill_doc @@ -114,9 +114,9 @@ def plot_ica_sources( .. versionadded:: 0.10.0 """ - from ..io import BaseRaw - from ..evoked import Evoked from ..epochs import BaseEpochs + from ..evoked import Evoked + from ..io import BaseRaw exclude = ica.exclude picks = _picks_to_idx(ica.n_components_, picks, picks_on="components") @@ -694,8 +694,8 @@ def _prepare_data_ica_properties(inst, ica, reject_by_annotation=True, reject="a data : array of shape (n_epochs, n_ica_sources, n_times) A view on epochs ICA sources data. """ - from ..io import BaseRaw, RawArray from ..epochs import BaseEpochs + from ..io import BaseRaw, RawArray _validate_type(inst, (BaseRaw, BaseEpochs), "inst", "Raw or Epochs") if isinstance(inst, BaseRaw): @@ -1069,8 +1069,8 @@ def plot_ica_overlay( The figure. """ # avoid circular imports - from ..io import BaseRaw from ..evoked import Evoked + from ..io import BaseRaw from ..preprocessing.ica import _check_start_stop if ica.current_fit == "unfitted": @@ -1268,9 +1268,9 @@ def _plot_sources( overview_mode=None, ): """Plot the ICA components as a RawArray or EpochsArray.""" + from ..epochs import BaseEpochs, EpochsArray + from ..io import BaseRaw, RawArray from ._figure import _get_browser - from ..epochs import EpochsArray, BaseEpochs - from ..io import RawArray, BaseRaw # handle defaults / check arg validity is_raw = isinstance(inst, BaseRaw) diff --git a/mne/viz/misc.py b/mne/viz/misc.py index c903244f9ff..3b20c9cb572 100644 --- a/mne/viz/misc.py +++ b/mne/viz/misc.py @@ -20,41 +20,41 @@ from pathlib import Path import numpy as np -from scipy.signal import freqz, group_delay, lfilter, filtfilt, sosfilt, sosfiltfilt +from scipy.signal import filtfilt, freqz, group_delay, lfilter, sosfilt, sosfiltfilt -from .._freesurfer import _check_mri, _reorient_image, _read_mri_info, _mri_orientation -from ..defaults import DEFAULTS -from ..fixes import _safe_svd -from ..rank import compute_rank -from ..surface import read_surface from .._fiff.constants import FIFF -from .._fiff.proj import make_projector from .._fiff.pick import ( _DATA_CH_TYPES_SPLIT, - pick_types, - pick_info, - pick_channels, _picks_by_type, + pick_channels, + pick_info, + pick_types, ) -from ..transforms import apply_trans, _frame_to_str +from .._fiff.proj import make_projector +from .._freesurfer import _check_mri, _mri_orientation, _read_mri_info, _reorient_image +from ..defaults import DEFAULTS +from ..filter import estimate_ringing_samples +from ..fixes import _safe_svd +from ..rank import compute_rank +from ..surface import read_surface +from ..transforms import _frame_to_str, apply_trans from ..utils import ( - logger, - verbose, - warn, _check_option, - get_subjects_dir, _mask_to_onsets_offsets, - _pl, _on_missing, + _pl, fill_doc, + get_subjects_dir, + logger, + verbose, + warn, ) -from ..filter import estimate_ringing_samples from .utils import ( + _figure_agg, _get_color_list, _prepare_trellis, - plt_show, - _figure_agg, _validate_type, + plt_show, ) @@ -145,6 +145,7 @@ def plot_cov( """ import matplotlib.pyplot as plt from matplotlib.colors import Normalize + from ..cov import Covariance info, C, ch_names, idx_names = _index_info_cov(info, cov, exclude) @@ -390,6 +391,7 @@ def _plot_mri_contours( """ import matplotlib.pyplot as plt from matplotlib import patheffects + from ..source_space._source_space import _ensure_src # For ease of plotting, we will do everything in voxel coordinates. @@ -678,7 +680,7 @@ def plot_bem( on top of the midpoint MRI slice with the BEM boundary drawn for that slice. """ - from ..source_space import read_source_spaces, SourceSpaces + from ..source_space import SourceSpaces, read_source_spaces subjects_dir = get_subjects_dir(subjects_dir, raise_error=True) mri_fname = _check_mri(mri, subject, subjects_dir) diff --git a/mne/viz/montage.py b/mne/viz/montage.py index 19cd8c12a1b..afce1ce8dcb 100644 --- a/mne/viz/montage.py +++ b/mne/viz/montage.py @@ -4,10 +4,10 @@ import numpy as np from scipy.spatial.distance import cdist -from .utils import plot_sensors from .._fiff._digitization import _get_fid_coords from .._fiff.meas_info import create_info -from ..utils import logger, _check_option, _validate_type, verbose +from ..utils import _check_option, _validate_type, logger, verbose +from .utils import plot_sensors @verbose diff --git a/mne/viz/raw.py b/mne/viz/raw.py index b06eaa361f2..ec5c95f57ab 100644 --- a/mne/viz/raw.py +++ b/mne/viz/raw.py @@ -10,19 +10,19 @@ import numpy as np +from .._fiff.pick import pick_channels, pick_types +from ..defaults import _handle_default from ..filter import create_filter -from .._fiff.pick import pick_types, pick_channels -from ..utils import legacy, verbose, _validate_type, _check_option, _get_stim_channel +from ..utils import _check_option, _get_stim_channel, _validate_type, legacy, verbose from ..utils.spectrum import _split_psd_kwargs -from ..defaults import _handle_default from .utils import ( + _check_cov, _compute_scalings, + _get_channel_plotting_order, _handle_decim, - _check_cov, - _shorten_path_from_middle, _handle_precompute, - _get_channel_plotting_order, _make_event_color_dict, + _shorten_path_from_middle, ) _RAW_CLIP_DEF = 1.5 @@ -230,9 +230,9 @@ def plot_raw( %(notes_2d_backend)s """ + from ..annotations import _annotations_starts_stops from ..io import BaseRaw from ._figure import _get_browser - from ..annotations import _annotations_starts_stops info = raw.info.copy() sfreq = info["sfreq"] @@ -559,10 +559,10 @@ def plot_raw_psd_topo( def _setup_channel_selections(raw, kind, order): """Get dictionary of channel groupings.""" from ..channels import ( - read_vectorview_selection, - _SELECTIONS, _EEG_SELECTIONS, + _SELECTIONS, _divide_to_regions, + read_vectorview_selection, ) _check_option("group_by", kind, ("position", "selection")) diff --git a/mne/viz/tests/test_3d.py b/mne/viz/tests/test_3d.py index f7993111543..8b02b8e228f 100644 --- a/mne/viz/tests/test_3d.py +++ b/mne/viz/tests/test_3d.py @@ -9,54 +9,53 @@ from pathlib import Path +import matplotlib.pyplot as plt import numpy as np -from numpy.testing import assert_array_equal, assert_allclose import pytest -import matplotlib.pyplot as plt from matplotlib.colors import Colormap from matplotlib.figure import Figure +from numpy.testing import assert_allclose, assert_array_equal from mne import ( - make_field_map, - read_evokeds, - read_trans, - read_dipole, + MixedSourceEstimate, SourceEstimate, + convert_forward_solution, + make_field_map, make_sphere_model, - use_coil_def, + pick_info, pick_types, - setup_volume_source_space, + read_dipole, + read_evokeds, read_forward_solution, - convert_forward_solution, - MixedSourceEstimate, - pick_info, + read_trans, + setup_volume_source_space, + use_coil_def, ) -from mne.source_estimate import _BaseVolSourceEstimate -from mne.io import read_raw_ctf, read_raw_bti, read_raw_kit, read_info, read_raw_nirx from mne._fiff._digitization import write_dig from mne._fiff.constants import FIFF +from mne.bem import read_bem_solution, read_bem_surfaces +from mne.datasets import testing +from mne.io import read_info, read_raw_bti, read_raw_ctf, read_raw_kit, read_raw_nirx from mne.minimum_norm import apply_inverse +from mne.source_estimate import _BaseVolSourceEstimate +from mne.source_space import read_source_spaces +from mne.transforms import Transform +from mne.utils import _record_warnings, catch_logging from mne.viz import ( - plot_sparse_source_estimates, - plot_source_estimates, - snapshot_brain_montage, - plot_head_positions, - plot_alignment, + Brain, + EvokedField, Figure3D, - plot_brain_colorbar, link_brains, mne_analyze_colormap, - Brain, - EvokedField, + plot_alignment, + plot_brain_colorbar, + plot_head_positions, + plot_source_estimates, + plot_sparse_source_estimates, + snapshot_brain_montage, ) -from mne.viz._3d import _process_clim, _linearize_map, _get_map_ticks +from mne.viz._3d import _get_map_ticks, _linearize_map, _process_clim from mne.viz.utils import _fake_click, _fake_keypress, _fake_scroll, _get_cmap -from mne.utils import catch_logging, _record_warnings -from mne.datasets import testing -from mne.source_space import read_source_spaces -from mne.transforms import Transform -from mne.bem import read_bem_solution, read_bem_surfaces - data_dir = testing.data_path(download=False) subjects_dir = data_dir / "subjects" @@ -208,9 +207,10 @@ def test_plot_evoked_field(renderer): def test_plot_evoked_field_notebook(renderer_notebook, nbexec): """Test plotting the evoked field inside a notebook.""" import pytest - from mne import read_evokeds, make_field_map + + from mne import make_field_map, read_evokeds from mne.datasets import testing - from mne.viz import set_3d_backend, Brain, EvokedField, Figure3D + from mne.viz import Brain, EvokedField, Figure3D, set_3d_backend set_3d_backend("notebook") diff --git a/mne/viz/tests/test_3d_mpl.py b/mne/viz/tests/test_3d_mpl.py index 2060de1ebbe..ae4f72d4dd3 100644 --- a/mne/viz/tests/test_3d_mpl.py +++ b/mne/viz/tests/test_3d_mpl.py @@ -13,6 +13,9 @@ import pytest from mne import ( + SourceEstimate, + VolSourceEstimate, + VolVectorSourceEstimate, compute_covariance, compute_source_morph, make_fixed_length_epochs, @@ -21,14 +24,11 @@ read_forward_solution, read_trans, setup_volume_source_space, - SourceEstimate, - VolSourceEstimate, - VolVectorSourceEstimate, ) from mne.datasets import testing from mne.io import read_raw_fif from mne.minimum_norm import apply_inverse, make_inverse_operator -from mne.utils import catch_logging, _record_warnings +from mne.utils import _record_warnings, catch_logging from mne.viz import plot_volume_source_estimates from mne.viz.utils import _fake_click, _fake_keypress diff --git a/mne/viz/tests/test_circle.py b/mne/viz/tests/test_circle.py index 7abd14568ec..f95aca2c8a5 100644 --- a/mne/viz/tests/test_circle.py +++ b/mne/viz/tests/test_circle.py @@ -5,8 +5,8 @@ # License: Simplified BSD -import pytest import matplotlib +import pytest from mne.viz import plot_channel_labels_circle diff --git a/mne/viz/tests/test_epochs.py b/mne/viz/tests/test_epochs.py index bfe5d07eebf..77f45ed1598 100644 --- a/mne/viz/tests/test_epochs.py +++ b/mne/viz/tests/test_epochs.py @@ -13,7 +13,7 @@ import numpy as np import pytest -from mne import Epochs, create_info, EpochsArray +from mne import Epochs, EpochsArray, create_info from mne.datasets import testing from mne.event import make_fixed_length_events from mne.viz import plot_drop_log diff --git a/mne/viz/tests/test_evoked.py b/mne/viz/tests/test_evoked.py index 644b2fb4e3e..c089b064d4a 100644 --- a/mne/viz/tests/test_evoked.py +++ b/mne/viz/tests/test_evoked.py @@ -11,30 +11,30 @@ from pathlib import Path +import matplotlib.pyplot as plt import numpy as np -from numpy.testing import assert_allclose import pytest -import matplotlib.pyplot as plt from matplotlib import gridspec from matplotlib.collections import PolyCollection from mpl_toolkits.axes_grid1.parasite_axes import HostAxes # spatial_colors +from numpy.testing import assert_allclose import mne from mne import ( - read_events, Epochs, - read_cov, compute_covariance, - make_fixed_length_events, compute_proj_evoked, + make_fixed_length_events, + read_cov, + read_events, ) +from mne._fiff.constants import FIFF +from mne.datasets import testing from mne.io import read_raw_fif +from mne.stats.parametric import _parametric_ci from mne.utils import catch_logging from mne.viz import plot_compare_evokeds, plot_evoked_white from mne.viz.utils import _fake_click, _get_cmap -from mne.datasets import testing -from mne._fiff.constants import FIFF -from mne.stats.parametric import _parametric_ci base_dir = Path(__file__).parent.parent.parent / "io" / "tests" / "data" evoked_fname = base_dir / "test-ave.fif" diff --git a/mne/viz/tests/test_figure.py b/mne/viz/tests/test_figure.py index 927d8c2f19c..19d4e163dd1 100644 --- a/mne/viz/tests/test_figure.py +++ b/mne/viz/tests/test_figure.py @@ -3,6 +3,7 @@ # License: Simplified BSD import pytest + from mne.viz._figure import _get_browser diff --git a/mne/viz/tests/test_ica.py b/mne/viz/tests/test_ica.py index 729a949e8e9..d01542f3da5 100644 --- a/mne/viz/tests/test_ica.py +++ b/mne/viz/tests/test_ica.py @@ -9,19 +9,19 @@ import matplotlib.pyplot as plt import numpy as np import pytest -from numpy.testing import assert_equal, assert_array_equal, assert_allclose +from numpy.testing import assert_allclose, assert_array_equal, assert_equal from mne import ( - read_events, - Epochs, - read_cov, - pick_types, Annotations, + Epochs, make_fixed_length_events, + pick_types, + read_cov, + read_events, ) from mne.io import read_raw_fif from mne.preprocessing import ICA, create_ecg_epochs, create_eog_epochs -from mne.utils import catch_logging, _record_warnings +from mne.utils import _record_warnings, catch_logging from mne.viz.ica import _create_properties_layout, plot_ica_properties from mne.viz.utils import _fake_click, _fake_keypress diff --git a/mne/viz/tests/test_misc.py b/mne/viz/tests/test_misc.py index cedceab21e1..fcc6d9e1566 100644 --- a/mne/viz/tests/test_misc.py +++ b/mne/viz/tests/test_misc.py @@ -9,37 +9,37 @@ from pathlib import Path +import matplotlib.pyplot as plt import numpy as np -from numpy.testing import assert_array_equal import pytest -import matplotlib.pyplot as plt +from numpy.testing import assert_array_equal from mne import ( - read_events, - read_cov, - read_source_spaces, - read_evokeds, - read_dipole, SourceEstimate, pick_events, + read_cov, + read_dipole, + read_events, + read_evokeds, + read_source_spaces, ) from mne.chpi import compute_chpi_snr from mne.datasets import testing from mne.filter import create_filter from mne.io import read_raw_fif from mne.minimum_norm import read_inverse_operator +from mne.time_frequency import CrossSpectralDensity from mne.viz import ( plot_bem, + plot_chpi_snr, + plot_csd, plot_events, - plot_source_spectrogram, - plot_snr_estimate, plot_filter, - plot_csd, - plot_chpi_snr, + plot_snr_estimate, + plot_source_spectrogram, ) from mne.viz.misc import _handle_event_colors from mne.viz.utils import _get_color_list -from mne.time_frequency import CrossSpectralDensity data_path = testing.data_path(download=False) subjects_dir = data_path / "subjects" diff --git a/mne/viz/tests/test_montage.py b/mne/viz/tests/test_montage.py index 7d77bd3d87f..bed0d212a6b 100644 --- a/mne/viz/tests/test_montage.py +++ b/mne/viz/tests/test_montage.py @@ -8,12 +8,11 @@ from pathlib import Path +import matplotlib.pyplot as plt import numpy as np - import pytest -import matplotlib.pyplot as plt -from mne.channels import read_dig_fif, make_dig_montage, make_standard_montage +from mne.channels import make_dig_montage, make_standard_montage, read_dig_fif p_dir = Path(__file__).parent.parent.parent / "io" / "kit" / "tests" / "data" elp = p_dir / "test_elp.txt" diff --git a/mne/viz/tests/test_proj.py b/mne/viz/tests/test_proj.py index 278128666d9..05f9207a75f 100644 --- a/mne/viz/tests/test_proj.py +++ b/mne/viz/tests/test_proj.py @@ -5,7 +5,7 @@ import numpy as np import pytest -from mne import read_evokeds, pick_types, compute_proj_evoked +from mne import compute_proj_evoked, pick_types, read_evokeds from mne.datasets import testing from mne.viz import plot_projs_joint diff --git a/mne/viz/tests/test_raw.py b/mne/viz/tests/test_raw.py index c413496ba27..a4c73e76075 100644 --- a/mne/viz/tests/test_raw.py +++ b/mne/viz/tests/test_raw.py @@ -7,23 +7,23 @@ from copy import deepcopy import matplotlib.pyplot as plt -from matplotlib import backend_bases import numpy as np import pytest +from matplotlib import backend_bases from numpy.testing import assert_allclose from mne import Annotations, create_info, pick_types +from mne._fiff.pick import _DATA_CH_TYPES_ORDER_DEFAULT, _PICK_TYPES_DATA_DICT from mne.annotations import _sync_onset from mne.datasets import testing from mne.io import RawArray -from mne._fiff.pick import _DATA_CH_TYPES_ORDER_DEFAULT, _PICK_TYPES_DATA_DICT from mne.utils import ( + _assert_no_instances, _dt_to_stamp, _record_warnings, + check_version, get_config, set_config, - _assert_no_instances, - check_version, ) from mne.viz import plot_raw, plot_sensors from mne.viz.utils import _fake_click, _fake_keypress diff --git a/mne/viz/tests/test_scraper.py b/mne/viz/tests/test_scraper.py index 4c7be550ac9..7a2c8e734b1 100644 --- a/mne/viz/tests/test_scraper.py +++ b/mne/viz/tests/test_scraper.py @@ -3,7 +3,9 @@ # License: Simplified BSD import os.path as op + import pytest + import mne diff --git a/mne/viz/tests/test_topo.py b/mne/viz/tests/test_topo.py index 4642d5375a8..da4fe116330 100644 --- a/mne/viz/tests/test_topo.py +++ b/mne/viz/tests/test_topo.py @@ -9,28 +9,26 @@ from collections import namedtuple from pathlib import Path -import numpy as np -import pytest import matplotlib import matplotlib.pyplot as plt +import numpy as np +import pytest -from mne import read_events, Epochs, read_cov, compute_proj_evoked +from mne import Epochs, compute_proj_evoked, read_cov, read_events from mne.channels import read_layout from mne.io import read_raw_fif from mne.time_frequency.tfr import AverageTFR from mne.utils import _record_warnings - from mne.viz import ( - plot_topo_image_epochs, _get_presser, mne_analyze_colormap, plot_evoked_topo, + plot_topo_image_epochs, ) from mne.viz.evoked import _line_plot_onselect +from mne.viz.topo import _imshow_tfr, _plot_update_evoked_topo_proj, iter_topography from mne.viz.utils import _fake_click -from mne.viz.topo import _plot_update_evoked_topo_proj, iter_topography, _imshow_tfr - base_dir = Path(__file__).parent.parent.parent / "io" / "tests" / "data" evoked_fname = base_dir / "test-ave.fif" raw_fname = base_dir / "test_raw.fif" diff --git a/mne/viz/tests/test_topomap.py b/mne/viz/tests/test_topomap.py index e20b1987dd1..972b07ce83c 100644 --- a/mne/viz/tests/test_topomap.py +++ b/mne/viz/tests/test_topomap.py @@ -9,55 +9,53 @@ from functools import partial from pathlib import Path -import numpy as np -from numpy.testing import assert_array_equal, assert_equal, assert_almost_equal -import pytest import matplotlib import matplotlib.pyplot as plt +import numpy as np +import pytest from matplotlib.patches import Circle +from numpy.testing import assert_almost_equal, assert_array_equal, assert_equal from mne import ( - read_evokeds, - read_proj, - make_fixed_length_events, Epochs, + EvokedArray, + Projection, compute_proj_evoked, + compute_proj_raw, + create_info, find_layout, + make_fixed_length_events, pick_types, - create_info, read_cov, - EvokedArray, - compute_proj_raw, - Projection, + read_evokeds, + read_proj, ) -from mne._fiff.proj import make_eeg_average_ref_proj -from mne.io import read_raw_fif, read_info, RawArray -from mne._fiff.constants import FIFF -from mne._fiff.pick import pick_info, channel_indices_by_type, _picks_to_idx from mne._fiff.compensator import get_current_comp +from mne._fiff.constants import FIFF +from mne._fiff.pick import _picks_to_idx, channel_indices_by_type, pick_info +from mne._fiff.proj import make_eeg_average_ref_proj from mne.channels import ( - read_layout, + find_ch_adjacency, make_dig_montage, make_standard_montage, - find_ch_adjacency, + read_layout, ) from mne.datasets import testing +from mne.io import RawArray, read_info, read_raw_fif from mne.preprocessing import compute_bridged_electrodes from mne.time_frequency.tfr import AverageTFR - from mne.viz import plot_evoked_topomap, plot_projs_topomap, topomap +from mne.viz.tests.test_raw import _proj_status from mne.viz.topomap import ( _get_pos_outlines, _onselect, - plot_topomap, plot_arrowmap, - plot_psds_topomap, plot_bridged_electrodes, plot_ch_adjacency, + plot_psds_topomap, + plot_topomap, ) -from mne.viz.utils import _find_peaks, _fake_click, _fake_keypress, _fake_scroll - -from mne.viz.tests.test_raw import _proj_status +from mne.viz.utils import _fake_click, _fake_keypress, _fake_scroll, _find_peaks data_dir = testing.data_path(download=False) subjects_dir = data_dir / "subjects" @@ -770,8 +768,7 @@ def test_plot_cov_topomap(): def test_plot_topomap_cnorm(): """Test colormap normalization.""" - from matplotlib.colors import TwoSlopeNorm - from matplotlib.colors import PowerNorm + from matplotlib.colors import PowerNorm, TwoSlopeNorm rng = np.random.default_rng(42) v = rng.uniform(low=-1, high=2.5, size=64) diff --git a/mne/viz/tests/test_utils.py b/mne/viz/tests/test_utils.py index c7f339c9997..5450e4d5789 100644 --- a/mne/viz/tests/test_utils.py +++ b/mne/viz/tests/test_utils.py @@ -4,30 +4,30 @@ from pathlib import Path +import matplotlib.pyplot as plt import numpy as np -from numpy.testing import assert_allclose import pytest -import matplotlib.pyplot as plt +from numpy.testing import assert_allclose +from mne import read_evokeds +from mne.epochs import Epochs +from mne.event import read_events +from mne.io import read_raw_fif +from mne.viz import ClickableImage, add_background_image, mne_analyze_colormap +from mne.viz.ui_events import ColormapRange, link, subscribe from mne.viz.utils import ( - compare_fiff, + _compute_scalings, _fake_click, _fake_keypress, _fake_scroll, - _compute_scalings, - _validate_if_list_of_axes, _get_color_list, + _make_event_color_dict, _setup_vmin_vmax, + _validate_if_list_of_axes, centers_to_edges, - _make_event_color_dict, + compare_fiff, concatenate_images, ) -from mne.viz.ui_events import link, subscribe, ColormapRange -from mne.viz import ClickableImage, add_background_image, mne_analyze_colormap -from mne.io import read_raw_fif -from mne.event import read_events -from mne.epochs import Epochs -from mne import read_evokeds base_dir = Path(__file__).parent.parent.parent / "io" / "tests" / "data" raw_fname = base_dir / "test_raw.fif" diff --git a/mne/viz/topo.py b/mne/viz/topo.py index 5a832c954a3..8e363d117e9 100644 --- a/mne/viz/topo.py +++ b/mne/viz/topo.py @@ -14,18 +14,18 @@ from scipy import ndimage from .._fiff.pick import channel_type, pick_types -from ..utils import _clean_names, _check_option, Bunch, fill_doc, _to_rgb from ..defaults import _handle_default +from ..utils import Bunch, _check_option, _clean_names, _to_rgb, fill_doc from .utils import ( + DraggableColorbar, + _check_cov, _check_delayed_ssp, _draw_proj_checkbox, + _plot_masked_image, + _setup_ax_spines, + _setup_vmin_vmax, add_background_image, plt_show, - _setup_vmin_vmax, - DraggableColorbar, - _setup_ax_spines, - _check_cov, - _plot_masked_image, ) @@ -141,7 +141,9 @@ def _iter_topography( If True, a single axis will be constructed. The former is useful for custom plotting, the latter for speed. """ - from matplotlib import pyplot as plt, collections + from matplotlib import collections + from matplotlib import pyplot as plt + from ..channels.layout import find_layout if fig is None: @@ -929,8 +931,9 @@ def _plot_evoked_topo( Images of evoked responses at sensor locations """ import matplotlib.pyplot as plt - from ..cov import whiten_evoked + from ..channels.layout import _merge_ch_data, _pair_grad_sensors, find_layout + from ..cov import whiten_evoked if type(evoked) not in (tuple, list): evoked = [evoked] diff --git a/mne/viz/topomap.py b/mne/viz/topomap.py index a90400c6421..a778c72dc1e 100644 --- a/mne/viz/topomap.py +++ b/mne/viz/topomap.py @@ -12,71 +12,74 @@ import copy import itertools +import warnings from functools import partial from numbers import Integral -import warnings import numpy as np from scipy.interpolate import ( CloughTocher2DInterpolator, - NearestNDInterpolator, LinearNDInterpolator, + NearestNDInterpolator, ) from scipy.sparse import csr_matrix from scipy.spatial import Delaunay, Voronoi from scipy.spatial.distance import pdist, squareform -from .ui_events import publish, subscribe, TimeChange -from ..baseline import rescale -from ..defaults import _INTERPOLATION_DEFAULT, _EXTRAPOLATE_DEFAULT, _BORDER_DEFAULT +from .._fiff.meas_info import Info, _simplify_info from .._fiff.pick import ( - pick_types, - _picks_by_type, - pick_info, - pick_channels, + _MEG_CH_TYPES_SPLIT, _pick_data_channels, + _picks_by_type, _picks_to_idx, - _MEG_CH_TYPES_SPLIT, + pick_channels, + pick_info, + pick_types, +) +from ..baseline import rescale +from ..defaults import ( + _BORDER_DEFAULT, + _EXTRAPOLATE_DEFAULT, + _INTERPOLATION_DEFAULT, + _handle_default, ) +from ..transforms import apply_trans, invert_transform from ..utils import ( + _check_option, + _check_sphere, _clean_names, + _is_numeric, _time_mask, - verbose, - logger, - fill_doc, _validate_type, - _check_sphere, - _check_option, - _is_numeric, - warn, - legacy, check_version, + fill_doc, + legacy, + logger, + verbose, + warn, ) from ..utils.spectrum import _split_psd_kwargs +from .ui_events import TimeChange, publish, subscribe from .utils import ( - _setup_vmin_vmax, - _prepare_trellis, - _check_delayed_ssp, - _draw_proj_checkbox, - figure_nobar, - plt_show, - _process_times, DraggableColorbar, - _get_cmap, - _validate_if_list_of_axes, - _setup_cmap, + _check_delayed_ssp, _check_time_unit, - _set_3d_axes_equal, _check_type_projs, + _draw_proj_checkbox, _format_units_psd, - _prepare_sensor_names, + _get_cmap, _get_plot_ch_type, + _prepare_sensor_names, + _prepare_trellis, + _process_times, + _set_3d_axes_equal, + _setup_cmap, + _setup_vmin_vmax, + _validate_if_list_of_axes, + figure_nobar, plot_sensors, + plt_show, ) -from ..defaults import _handle_default -from ..transforms import apply_trans, invert_transform -from .._fiff.meas_info import Info, _simplify_info - _fnirs_types = ("hbo", "hbr", "fnirs_cw_amplitude", "fnirs_od") @@ -112,7 +115,7 @@ def _adjust_meg_sphere(sphere, info, ch_type): def _prepare_topomap_plot(inst, ch_type, sphere=None): """Prepare topo plot.""" - from ..channels.layout import find_layout, _pair_grad_sensors, _find_topomap_coords + from ..channels.layout import _find_topomap_coords, _pair_grad_sensors, find_layout info = copy.deepcopy(inst if isinstance(inst, Info) else inst.info) sphere, clip_origin = _adjust_meg_sphere(sphere, info, ch_type) @@ -471,6 +474,7 @@ def _plot_projs_topomap( axes=None, ): import matplotlib.pyplot as plt + from ..channels.layout import _merge_ch_data sphere = _check_sphere(sphere, info) @@ -1193,6 +1197,7 @@ def _plot_topomap( ): from matplotlib.colors import Normalize from matplotlib.widgets import RectangleSelector + from ..channels.layout import ( _find_topomap_coords, _merge_ch_data, @@ -1405,6 +1410,7 @@ def _plot_ica_topomap( ): """Plot single ica map to axes.""" from matplotlib.axes import Axes + from ..channels.layout import _merge_ch_data if ica.info is None: @@ -1596,9 +1602,10 @@ def plot_ica_components( supplied). """ # noqa E501 from matplotlib.pyplot import Axes - from ..io import BaseRaw - from ..epochs import BaseEpochs + from ..channels.layout import _merge_ch_data + from ..epochs import BaseEpochs + from ..io import BaseRaw if ica.info is None: raise RuntimeError( @@ -1879,6 +1886,7 @@ def plot_tfr_topomap( The figure containing the topography. """ # noqa: E501 import matplotlib.pyplot as plt + from ..channels.layout import _merge_ch_data ch_type = _get_plot_ch_type(tfr, ch_type) @@ -2120,8 +2128,9 @@ def plot_evoked_topomap( import matplotlib.pyplot as plt from matplotlib.gridspec import GridSpec from matplotlib.widgets import Slider - from ..evoked import Evoked + from ..channels.layout import _merge_ch_data + from ..evoked import Evoked _validate_type(evoked, Evoked, "evoked") _validate_type(colorbar, bool, "colorbar") @@ -2943,6 +2952,7 @@ def _onselect( """Handle drawing average tfr over channels called from topomap.""" import matplotlib.pyplot as plt from matplotlib.collections import PathCollection + from ..channels.layout import _pair_grad_sensors ax = eclick.inaxes @@ -3243,7 +3253,8 @@ def _topomap_animation( See mne.evoked.Evoked.animate_topomap. """ - from matplotlib import pyplot as plt, animation + from matplotlib import animation + from matplotlib import pyplot as plt if ch_type is None: ch_type = _picks_by_type(evoked.info)[0][0] @@ -3577,6 +3588,7 @@ def plot_arrowmap( .. footbibliography:: """ from matplotlib import pyplot as plt + from ..forward import _map_meg_or_eeg_channels sphere = _check_sphere(sphere, info_from) @@ -3691,6 +3703,7 @@ def plot_bridged_electrodes( mne.preprocessing.compute_bridged_electrodes """ import matplotlib.pyplot as plt + from ..channels.layout import _find_topomap_coords if topomap_args is None: @@ -4029,6 +4042,7 @@ def plot_regression_weights( """ import matplotlib import matplotlib.pyplot as plt + from ..channels.layout import _merge_ch_data sphere = _check_sphere(sphere) diff --git a/mne/viz/ui_events.py b/mne/viz/ui_events.py index 78c1419ca2f..5648c90db9e 100644 --- a/mne/viz/ui_events.py +++ b/mne/viz/ui_events.py @@ -10,14 +10,14 @@ Authors: Marijn van Vliet """ import contextlib -from dataclasses import dataclass -from typing import Optional, List, Union -import weakref import re +import weakref +from dataclasses import dataclass +from typing import List, Optional, Union from matplotlib.colors import Colormap -from ..utils import warn, fill_doc, _validate_type, logger, verbose +from ..utils import _validate_type, fill_doc, logger, verbose, warn # Global dict {fig: channel} containing all currently active event channels. _event_channels = weakref.WeakKeyDictionary() @@ -225,6 +225,7 @@ def _get_event_channel(fig): channel. """ import matplotlib + from ._brain import Brain from .evoked_field import EvokedField @@ -460,8 +461,8 @@ def disable_ui_events(fig): def _cleanup_agg(): """Call close_event for Agg canvases to help our doc build.""" - import matplotlib.figure import matplotlib.backends.backend_agg + import matplotlib.figure for key in list(_event_channels): # we might remove keys as we go if isinstance(key, matplotlib.figure.Figure): diff --git a/mne/viz/utils.py b/mne/viz/utils.py index 08d4e69ec48..2855b9784c4 100644 --- a/mne/viz/utils.py +++ b/mne/viz/utils.py @@ -10,61 +10,60 @@ # Daniel McCloy # # License: Simplified BSD -from collections import defaultdict -from contextlib import contextmanager -from datetime import datetime -from inspect import signature import difflib -from functools import partial import math import os import sys import tempfile import traceback import webbrowser +from collections import defaultdict +from contextlib import contextmanager +from datetime import datetime +from functools import partial +from inspect import signature -from decorator import decorator import numpy as np +from decorator import decorator from scipy.signal import argrelmax -from ..defaults import _handle_default from .._fiff.constants import FIFF from .._fiff.meas_info import Info from .._fiff.open import show_fiff from .._fiff.pick import ( - channel_type, - channel_indices_by_type, - pick_channels, - _pick_data_channels, - _DATA_CH_TYPES_SPLIT, _DATA_CH_TYPES_ORDER_DEFAULT, + _DATA_CH_TYPES_SPLIT, _VALID_CHANNEL_TYPES, - pick_info, + _contains_ch_type, + _pick_data_channels, _picks_by_type, + channel_indices_by_type, + channel_type, + pick_channels, pick_channels_cov, - _contains_ch_type, + pick_info, ) -from .._fiff.proj import setup_proj, Projection +from .._fiff.proj import Projection, setup_proj +from ..defaults import _handle_default from ..rank import compute_rank +from ..transforms import apply_trans from ..utils import ( - verbose, - get_config, _check_ch_locs, + _check_decim, _check_option, - logger, - fill_doc, - _pl, _check_sphere, _ensure_int, - _validate_type, + _pl, _to_rgb, - warn, + _validate_type, check_version, - _check_decim, + fill_doc, + get_config, + logger, + verbose, + warn, ) -from .ui_events import publish, subscribe, ColormapRange -from ..transforms import apply_trans - +from .ui_events import ColormapRange, publish, subscribe _channel_type_prettyprint = { "eeg": "EEG channel", @@ -190,6 +189,7 @@ def _show_browser(show=True, block=True, fig=None, **kwargs): else: from qtpy.QtCore import Qt from qtpy.QtWidgets import QApplication + from .backends._utils import _qt_app_exec if fig is not None and os.getenv("_MNE_BROWSER_BACK", "").lower() == "true": @@ -436,6 +436,7 @@ def _prepare_trellis( sharey=False, ): from matplotlib.gridspec import GridSpec + from ._mpl_figure import _figure if n_cells == 1: @@ -635,7 +636,8 @@ def figure_nobar(*args, **kwargs): fig : instance of Figure The figure. """ - from matplotlib import rcParams, pyplot as plt + from matplotlib import pyplot as plt + from matplotlib import rcParams old_val = rcParams["toolbar"] try: @@ -1124,10 +1126,10 @@ def plot_sensors( ) # Avoid circular import from ..channels import ( - read_vectorview_selection, - _SELECTIONS, _EEG_SELECTIONS, + _SELECTIONS, _divide_to_regions, + read_vectorview_selection, ) if ch_groups == "position": @@ -1238,10 +1240,11 @@ def _plot_sensors( linewidth=2, ): """Plot sensors.""" - from matplotlib import rcParams import matplotlib.pyplot as plt + from matplotlib import rcParams from mpl_toolkits.mplot3d import Axes3D # noqa: F401 analysis:ignore - from .topomap import _get_pos_outlines, _draw_outlines + + from .topomap import _draw_outlines, _get_pos_outlines ch_names = [str(ch_name) for ch_name in ch_names] sphere = _check_sphere(sphere, info) @@ -1375,8 +1378,8 @@ def _compute_scalings(scalings, inst, remove_dc=False, duration=10): scalings : dict A scalings dictionary with updated values """ - from ..io import BaseRaw from ..epochs import BaseEpochs + from ..io import BaseRaw scalings = _handle_default("scalings_plot_raw", scalings) if not isinstance(inst, (BaseRaw, BaseEpochs)): @@ -2442,8 +2445,9 @@ def _plot_psd( ): # helper function for Spectrum.plot() from matplotlib.ticker import ScalarFormatter - from .evoked import _plot_lines + from ..stats import _ci + from .evoked import _plot_lines for key, ls in zip(["lowpass", "highpass", "line_freq"], ["--", "--", "-."]): if inst.info[key] is not None: diff --git a/pyproject.toml b/pyproject.toml index a9565e40d5a..0dc29069335 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -91,7 +91,7 @@ builtin = "clear,rare,informal,names,usage" skip = "doc/references.bib" [tool.ruff] -select = ["E", "F", "W", "D"] +select = ["E", "F", "W", "D", "I"] exclude = ["__init__.py", "constants.py", "resources.py"] ignore = [ "D100", # Missing docstring in public module diff --git a/tutorials/clinical/30_ecog.py b/tutorials/clinical/30_ecog.py index b97b44c1036..d3ee7c9268c 100644 --- a/tutorials/clinical/30_ecog.py +++ b/tutorials/clinical/30_ecog.py @@ -33,8 +33,8 @@ # %% -import numpy as np import matplotlib.pyplot as plt +import numpy as np from matplotlib import colormaps from mne_bids import BIDSPath, read_raw_bids diff --git a/tutorials/clinical/60_sleep.py b/tutorials/clinical/60_sleep.py index 55194922d22..a878b7747e7 100644 --- a/tutorials/clinical/60_sleep.py +++ b/tutorials/clinical/60_sleep.py @@ -33,19 +33,16 @@ # %% -import numpy as np import matplotlib.pyplot as plt - -import mne -from mne.datasets.sleep_physionet.age import fetch_data - +import numpy as np from sklearn.ensemble import RandomForestClassifier -from sklearn.metrics import accuracy_score -from sklearn.metrics import confusion_matrix -from sklearn.metrics import classification_report +from sklearn.metrics import accuracy_score, classification_report, confusion_matrix from sklearn.pipeline import make_pipeline from sklearn.preprocessing import FunctionTransformer +import mne +from mne.datasets.sleep_physionet.age import fetch_data + ############################################################################## # Load the data # ------------- diff --git a/tutorials/epochs/30_epochs_metadata.py b/tutorials/epochs/30_epochs_metadata.py index 8754a76e561..214178fc275 100644 --- a/tutorials/epochs/30_epochs_metadata.py +++ b/tutorials/epochs/30_epochs_metadata.py @@ -21,6 +21,7 @@ import numpy as np import pandas as pd + import mne kiloword_data_folder = mne.datasets.kiloword.data_path() diff --git a/tutorials/epochs/40_autogenerate_metadata.py b/tutorials/epochs/40_autogenerate_metadata.py index 01b9ed0d7ed..025277901d1 100644 --- a/tutorials/epochs/40_autogenerate_metadata.py +++ b/tutorials/epochs/40_autogenerate_metadata.py @@ -45,7 +45,9 @@ # %% from pathlib import Path + import matplotlib.pyplot as plt + import mne data_dir = Path(mne.datasets.erp_core.data_path()) diff --git a/tutorials/epochs/60_make_fixed_length_epochs.py b/tutorials/epochs/60_make_fixed_length_epochs.py index 9a6eace0ab9..a311842312a 100644 --- a/tutorials/epochs/60_make_fixed_length_epochs.py +++ b/tutorials/epochs/60_make_fixed_length_epochs.py @@ -30,11 +30,12 @@ # %% -import numpy as np import matplotlib.pyplot as plt +import numpy as np +from mne_connectivity import envelope_correlation + import mne from mne.preprocessing import compute_proj_ecg -from mne_connectivity import envelope_correlation sample_data_folder = mne.datasets.sample.data_path() sample_data_raw_file = sample_data_folder / "MEG" / "sample" / "sample_audvis_raw.fif" diff --git a/tutorials/evoked/30_eeg_erp.py b/tutorials/evoked/30_eeg_erp.py index 1915b99e183..c6fcae797af 100644 --- a/tutorials/evoked/30_eeg_erp.py +++ b/tutorials/evoked/30_eeg_erp.py @@ -19,9 +19,10 @@ # %% +import matplotlib.pyplot as plt import numpy as np import pandas as pd -import matplotlib.pyplot as plt + import mne root = mne.datasets.sample.data_path() / "MEG" / "sample" diff --git a/tutorials/forward/20_source_alignment.py b/tutorials/forward/20_source_alignment.py index 93b46375fb6..292e5346415 100644 --- a/tutorials/forward/20_source_alignment.py +++ b/tutorials/forward/20_source_alignment.py @@ -15,8 +15,8 @@ # %% -import numpy as np import nibabel as nib +import numpy as np from scipy import linalg import mne diff --git a/tutorials/forward/35_eeg_no_mri.py b/tutorials/forward/35_eeg_no_mri.py index 63ef7e016a9..504bc3b080b 100644 --- a/tutorials/forward/35_eeg_no_mri.py +++ b/tutorials/forward/35_eeg_no_mri.py @@ -24,11 +24,11 @@ # License: BSD-3-Clause import os.path as op + import numpy as np import mne -from mne.datasets import eegbci -from mne.datasets import fetch_fsaverage +from mne.datasets import eegbci, fetch_fsaverage # Download fsaverage files fs_dir = fetch_fsaverage(verbose=True) diff --git a/tutorials/forward/50_background_freesurfer_mne.py b/tutorials/forward/50_background_freesurfer_mne.py index 0150088de83..1afd6a5a278 100644 --- a/tutorials/forward/50_background_freesurfer_mne.py +++ b/tutorials/forward/50_background_freesurfer_mne.py @@ -18,14 +18,14 @@ # %% -import numpy as np -import nibabel -import matplotlib.pyplot as plt import matplotlib.patheffects as path_effects +import matplotlib.pyplot as plt +import nibabel +import numpy as np import mne -from mne.transforms import apply_trans from mne.io.constants import FIFF +from mne.transforms import apply_trans # %% # MRI coordinate frames diff --git a/tutorials/forward/80_fix_bem_in_blender.py b/tutorials/forward/80_fix_bem_in_blender.py index 570dd9a0419..74ea1c69349 100644 --- a/tutorials/forward/80_fix_bem_in_blender.py +++ b/tutorials/forward/80_fix_bem_in_blender.py @@ -28,6 +28,7 @@ import os import shutil + import mne data_path = mne.datasets.sample.data_path() diff --git a/tutorials/intro/10_overview.py b/tutorials/intro/10_overview.py index a0b5a65691e..e45e64085ba 100644 --- a/tutorials/intro/10_overview.py +++ b/tutorials/intro/10_overview.py @@ -17,6 +17,7 @@ # %% import numpy as np + import mne # %% diff --git a/tutorials/intro/20_events_from_raw.py b/tutorials/intro/20_events_from_raw.py index a9dea99f55a..e448368a209 100644 --- a/tutorials/intro/20_events_from_raw.py +++ b/tutorials/intro/20_events_from_raw.py @@ -29,6 +29,7 @@ # %% import numpy as np + import mne sample_data_folder = mne.datasets.sample.data_path() diff --git a/tutorials/intro/50_configure_mne.py b/tutorials/intro/50_configure_mne.py index 9c5d9d02f99..758726bed97 100644 --- a/tutorials/intro/50_configure_mne.py +++ b/tutorials/intro/50_configure_mne.py @@ -14,6 +14,7 @@ # %% import os + import mne # %% diff --git a/tutorials/intro/70_report.py b/tutorials/intro/70_report.py index 12f04772ce8..a7d3b02b2b3 100644 --- a/tutorials/intro/70_report.py +++ b/tutorials/intro/70_report.py @@ -23,11 +23,13 @@ # %% -from pathlib import Path import tempfile +from pathlib import Path + +import matplotlib.pyplot as plt import numpy as np import scipy.ndimage -import matplotlib.pyplot as plt + import mne data_path = Path(mne.datasets.sample.data_path(verbose=False)) diff --git a/tutorials/inverse/20_dipole_fit.py b/tutorials/inverse/20_dipole_fit.py index c81c16f3252..567c7d9b75e 100644 --- a/tutorials/inverse/20_dipole_fit.py +++ b/tutorials/inverse/20_dipole_fit.py @@ -13,17 +13,16 @@ # %% -import numpy as np import matplotlib.pyplot as plt +import numpy as np +from nilearn.datasets import load_mni152_template +from nilearn.plotting import plot_anat import mne -from mne.forward import make_forward_dipole from mne.evoked import combine_evoked +from mne.forward import make_forward_dipole from mne.simulation import simulate_evoked -from nilearn.plotting import plot_anat -from nilearn.datasets import load_mni152_template - data_path = mne.datasets.sample.data_path() subjects_dir = data_path / "subjects" fname_ave = data_path / "MEG" / "sample" / "sample_audvis-ave.fif" diff --git a/tutorials/inverse/30_mne_dspm_loreta.py b/tutorials/inverse/30_mne_dspm_loreta.py index 90d688eafc9..af3dc2a8e53 100644 --- a/tutorials/inverse/30_mne_dspm_loreta.py +++ b/tutorials/inverse/30_mne_dspm_loreta.py @@ -11,12 +11,12 @@ # %% -import numpy as np import matplotlib.pyplot as plt +import numpy as np import mne from mne.datasets import sample -from mne.minimum_norm import make_inverse_operator, apply_inverse +from mne.minimum_norm import apply_inverse, make_inverse_operator # %% # Process MEG data diff --git a/tutorials/inverse/35_dipole_orientations.py b/tutorials/inverse/35_dipole_orientations.py index bc5c74ca44b..bffc5ad1fc0 100644 --- a/tutorials/inverse/35_dipole_orientations.py +++ b/tutorials/inverse/35_dipole_orientations.py @@ -20,10 +20,11 @@ # --------- # Load everything we need to perform source localization on the sample dataset. -import mne import numpy as np + +import mne from mne.datasets import sample -from mne.minimum_norm import make_inverse_operator, apply_inverse +from mne.minimum_norm import apply_inverse, make_inverse_operator data_path = sample.data_path() meg_path = data_path / "MEG" / "sample" diff --git a/tutorials/inverse/40_mne_fixed_free.py b/tutorials/inverse/40_mne_fixed_free.py index c862c5898e4..c91815b513d 100644 --- a/tutorials/inverse/40_mne_fixed_free.py +++ b/tutorials/inverse/40_mne_fixed_free.py @@ -17,7 +17,7 @@ import mne from mne.datasets import sample -from mne.minimum_norm import make_inverse_operator, apply_inverse +from mne.minimum_norm import apply_inverse, make_inverse_operator print(__doc__) diff --git a/tutorials/inverse/50_beamformer_lcmv.py b/tutorials/inverse/50_beamformer_lcmv.py index 9ccc5371d74..9015949c250 100644 --- a/tutorials/inverse/50_beamformer_lcmv.py +++ b/tutorials/inverse/50_beamformer_lcmv.py @@ -16,9 +16,10 @@ # %% import matplotlib.pyplot as plt + import mne -from mne.datasets import sample, fetch_fsaverage -from mne.beamformer import make_lcmv, apply_lcmv +from mne.beamformer import apply_lcmv, make_lcmv +from mne.datasets import fetch_fsaverage, sample # %% # Introduction to beamformers diff --git a/tutorials/inverse/60_visualize_stc.py b/tutorials/inverse/60_visualize_stc.py index 3be86643c61..2d06089c846 100644 --- a/tutorials/inverse/60_visualize_stc.py +++ b/tutorials/inverse/60_visualize_stc.py @@ -14,13 +14,13 @@ # %% -import numpy as np import matplotlib.pyplot as plt +import numpy as np import mne -from mne.datasets import sample, fetch_hcp_mmp_parcellation -from mne.minimum_norm import apply_inverse, read_inverse_operator from mne import read_evokeds +from mne.datasets import fetch_hcp_mmp_parcellation, sample +from mne.minimum_norm import apply_inverse, read_inverse_operator data_path = sample.data_path() meg_path = data_path / "MEG" / "sample" diff --git a/tutorials/inverse/70_eeg_mri_coords.py b/tutorials/inverse/70_eeg_mri_coords.py index 11f19b916f4..0586b3084c1 100644 --- a/tutorials/inverse/70_eeg_mri_coords.py +++ b/tutorials/inverse/70_eeg_mri_coords.py @@ -16,8 +16,8 @@ # %% import nibabel -from nilearn.plotting import plot_glass_brain import numpy as np +from nilearn.plotting import plot_glass_brain import mne from mne.channels import compute_native_head_t, read_custom_montage diff --git a/tutorials/inverse/80_brainstorm_phantom_elekta.py b/tutorials/inverse/80_brainstorm_phantom_elekta.py index 95a2a8e8f59..2da04a19c0d 100644 --- a/tutorials/inverse/80_brainstorm_phantom_elekta.py +++ b/tutorials/inverse/80_brainstorm_phantom_elekta.py @@ -18,8 +18,8 @@ # %% -import numpy as np import matplotlib.pyplot as plt +import numpy as np import mne from mne import find_events, fit_dipole diff --git a/tutorials/inverse/85_brainstorm_phantom_ctf.py b/tutorials/inverse/85_brainstorm_phantom_ctf.py index 6c44477365a..857ff7397fe 100644 --- a/tutorials/inverse/85_brainstorm_phantom_ctf.py +++ b/tutorials/inverse/85_brainstorm_phantom_ctf.py @@ -23,8 +23,8 @@ import warnings -import numpy as np import matplotlib.pyplot as plt +import numpy as np import mne from mne import fit_dipole diff --git a/tutorials/inverse/90_phantom_4DBTi.py b/tutorials/inverse/90_phantom_4DBTi.py index 7c7bdd6c8b0..12b4643049f 100644 --- a/tutorials/inverse/90_phantom_4DBTi.py +++ b/tutorials/inverse/90_phantom_4DBTi.py @@ -19,9 +19,11 @@ # %% import os.path as op + import numpy as np -from mne.datasets import phantom_4dbti + import mne +from mne.datasets import phantom_4dbti # %% # Read data and compute a dipole fit at the peak of the evoked response diff --git a/tutorials/io/30_reading_fnirs_data.py b/tutorials/io/30_reading_fnirs_data.py index 789450ceb79..e87b0efa81b 100644 --- a/tutorials/io/30_reading_fnirs_data.py +++ b/tutorials/io/30_reading_fnirs_data.py @@ -159,6 +159,7 @@ import numpy as np import pandas as pd + import mne # sphinx_gallery_thumbnail_number = 2 diff --git a/tutorials/io/60_ctf_bst_auditory.py b/tutorials/io/60_ctf_bst_auditory.py index 01a65ef3234..20ef8d32534 100644 --- a/tutorials/io/60_ctf_bst_auditory.py +++ b/tutorials/io/60_ctf_bst_auditory.py @@ -29,14 +29,14 @@ # %% -import pandas as pd import numpy as np +import pandas as pd import mne from mne import combine_evoked -from mne.minimum_norm import apply_inverse from mne.datasets.brainstorm import bst_auditory from mne.io import read_raw_ctf +from mne.minimum_norm import apply_inverse # %% # To reduce memory consumption and running time, some of the steps are diff --git a/tutorials/io/70_reading_eyetracking_data.py b/tutorials/io/70_reading_eyetracking_data.py index a9b1b1ffa76..c800b918323 100644 --- a/tutorials/io/70_reading_eyetracking_data.py +++ b/tutorials/io/70_reading_eyetracking_data.py @@ -78,8 +78,8 @@ """ # %% -from mne.io import read_raw_eyelink from mne.datasets import misc +from mne.io import read_raw_eyelink # %% fpath = misc.data_path() / "eyetracking" / "eyelink" diff --git a/tutorials/machine-learning/30_strf.py b/tutorials/machine-learning/30_strf.py index 9cc53a7a2da..a1c184d4afb 100644 --- a/tutorials/machine-learning/30_strf.py +++ b/tutorials/machine-learning/30_strf.py @@ -21,16 +21,15 @@ # sphinx_gallery_thumbnail_number = 7 -import numpy as np import matplotlib.pyplot as plt +import numpy as np +from scipy.io import loadmat +from scipy.stats import multivariate_normal +from sklearn.preprocessing import scale import mne from mne.decoding import ReceptiveField, TimeDelayingRidge -from scipy.stats import multivariate_normal -from scipy.io import loadmat -from sklearn.preprocessing import scale - rng = np.random.RandomState(1337) # To make this example reproducible # %% diff --git a/tutorials/machine-learning/50_decoding.py b/tutorials/machine-learning/50_decoding.py index bd3b55e15e3..fe2addc87f3 100644 --- a/tutorials/machine-learning/50_decoding.py +++ b/tutorials/machine-learning/50_decoding.py @@ -25,24 +25,23 @@ # %% # sphinx_gallery_thumbnail_number = 6 -import numpy as np import matplotlib.pyplot as plt - +import numpy as np +from sklearn.linear_model import LogisticRegression from sklearn.pipeline import make_pipeline from sklearn.preprocessing import StandardScaler -from sklearn.linear_model import LogisticRegression import mne from mne.datasets import sample from mne.decoding import ( - SlidingEstimator, + CSP, GeneralizingEstimator, + LinearModel, Scaler, + SlidingEstimator, + Vectorizer, cross_val_multiscore, - LinearModel, get_coef, - Vectorizer, - CSP, ) data_path = sample.data_path() diff --git a/tutorials/preprocessing/10_preprocessing_overview.py b/tutorials/preprocessing/10_preprocessing_overview.py index 8baf4c6c33e..ce2c77e3d84 100644 --- a/tutorials/preprocessing/10_preprocessing_overview.py +++ b/tutorials/preprocessing/10_preprocessing_overview.py @@ -15,7 +15,9 @@ # %% import os + import numpy as np + import mne sample_data_folder = mne.datasets.sample.data_path() diff --git a/tutorials/preprocessing/15_handling_bad_channels.py b/tutorials/preprocessing/15_handling_bad_channels.py index 151c15bf0f7..7385506bf34 100644 --- a/tutorials/preprocessing/15_handling_bad_channels.py +++ b/tutorials/preprocessing/15_handling_bad_channels.py @@ -16,7 +16,9 @@ import os from copy import deepcopy + import numpy as np + import mne sample_data_folder = mne.datasets.sample.data_path() diff --git a/tutorials/preprocessing/20_rejecting_bad_data.py b/tutorials/preprocessing/20_rejecting_bad_data.py index 6f9b86a6040..99228eb37d7 100644 --- a/tutorials/preprocessing/20_rejecting_bad_data.py +++ b/tutorials/preprocessing/20_rejecting_bad_data.py @@ -20,6 +20,7 @@ # %% import os + import mne sample_data_folder = mne.datasets.sample.data_path() diff --git a/tutorials/preprocessing/25_background_filtering.py b/tutorials/preprocessing/25_background_filtering.py index 09e5db8173e..0f60e8f1eb3 100644 --- a/tutorials/preprocessing/25_background_filtering.py +++ b/tutorials/preprocessing/25_background_filtering.py @@ -140,16 +140,15 @@ # First let's import some useful tools for filtering, and set some default # values for our data that are reasonable for M/EEG. +import matplotlib.pyplot as plt import numpy as np from numpy.fft import fft, fftfreq from scipy import signal -import matplotlib.pyplot as plt +import mne from mne.time_frequency.tfr import morlet from mne.viz import plot_filter, plot_ideal_filter -import mne - sfreq = 1000.0 f_p = 40.0 # limits for plotting diff --git a/tutorials/preprocessing/30_filtering_resampling.py b/tutorials/preprocessing/30_filtering_resampling.py index 53b1f550fcc..13d5f199c6f 100644 --- a/tutorials/preprocessing/30_filtering_resampling.py +++ b/tutorials/preprocessing/30_filtering_resampling.py @@ -16,8 +16,10 @@ # %% import os -import numpy as np + import matplotlib.pyplot as plt +import numpy as np + import mne sample_data_folder = mne.datasets.sample.data_path() diff --git a/tutorials/preprocessing/35_artifact_correction_regression.py b/tutorials/preprocessing/35_artifact_correction_regression.py index 0e1be9da5ef..e328648a33c 100644 --- a/tutorials/preprocessing/35_artifact_correction_regression.py +++ b/tutorials/preprocessing/35_artifact_correction_regression.py @@ -40,6 +40,7 @@ # %% import numpy as np + import mne from mne.preprocessing import EOGRegression diff --git a/tutorials/preprocessing/40_artifact_correction_ica.py b/tutorials/preprocessing/40_artifact_correction_ica.py index 0c5a6d8a8be..72e61a69454 100644 --- a/tutorials/preprocessing/40_artifact_correction_ica.py +++ b/tutorials/preprocessing/40_artifact_correction_ica.py @@ -21,6 +21,7 @@ # %% import os + import mne from mne.preprocessing import ICA, corrmap, create_ecg_epochs, create_eog_epochs diff --git a/tutorials/preprocessing/45_projectors_background.py b/tutorials/preprocessing/45_projectors_background.py index a970def83a1..5c25fc798f3 100644 --- a/tutorials/preprocessing/45_projectors_background.py +++ b/tutorials/preprocessing/45_projectors_background.py @@ -17,10 +17,12 @@ # %% import os -import numpy as np + import matplotlib.pyplot as plt +import numpy as np from mpl_toolkits.mplot3d import Axes3D # noqa from scipy.linalg import svd + import mne diff --git a/tutorials/preprocessing/50_artifact_correction_ssp.py b/tutorials/preprocessing/50_artifact_correction_ssp.py index 55d18b276a6..6adcacfb4e0 100644 --- a/tutorials/preprocessing/50_artifact_correction_ssp.py +++ b/tutorials/preprocessing/50_artifact_correction_ssp.py @@ -18,14 +18,16 @@ # %% import os -import numpy as np + import matplotlib.pyplot as plt +import numpy as np + import mne from mne.preprocessing import ( - create_eog_epochs, - create_ecg_epochs, compute_proj_ecg, compute_proj_eog, + create_ecg_epochs, + create_eog_epochs, ) # %% diff --git a/tutorials/preprocessing/55_setting_eeg_reference.py b/tutorials/preprocessing/55_setting_eeg_reference.py index ac6e9131c8b..36254647700 100644 --- a/tutorials/preprocessing/55_setting_eeg_reference.py +++ b/tutorials/preprocessing/55_setting_eeg_reference.py @@ -16,6 +16,7 @@ # %% import os + import mne sample_data_folder = mne.datasets.sample.data_path() diff --git a/tutorials/preprocessing/59_head_positions.py b/tutorials/preprocessing/59_head_positions.py index 3605315a4d2..28c54fe3875 100644 --- a/tutorials/preprocessing/59_head_positions.py +++ b/tutorials/preprocessing/59_head_positions.py @@ -30,6 +30,7 @@ # %% from os import path as op + import mne data_path = op.join(mne.datasets.testing.data_path(verbose=True), "SSS") diff --git a/tutorials/preprocessing/60_maxwell_filtering_sss.py b/tutorials/preprocessing/60_maxwell_filtering_sss.py index a3659b1f765..c1453528975 100644 --- a/tutorials/preprocessing/60_maxwell_filtering_sss.py +++ b/tutorials/preprocessing/60_maxwell_filtering_sss.py @@ -15,10 +15,12 @@ # %% import os + import matplotlib.pyplot as plt -import seaborn as sns -import pandas as pd import numpy as np +import pandas as pd +import seaborn as sns + import mne from mne.preprocessing import find_bad_channels_maxwell diff --git a/tutorials/preprocessing/70_fnirs_processing.py b/tutorials/preprocessing/70_fnirs_processing.py index 886d99fc618..2e0f4f9fa0f 100644 --- a/tutorials/preprocessing/70_fnirs_processing.py +++ b/tutorials/preprocessing/70_fnirs_processing.py @@ -14,12 +14,12 @@ """ # %% -import numpy as np -import matplotlib.pyplot as plt from itertools import compress -import mne +import matplotlib.pyplot as plt +import numpy as np +import mne fnirs_data_folder = mne.datasets.fnirs_motor.data_path() fnirs_cw_amplitude_dir = fnirs_data_folder / "Participant-1" diff --git a/tutorials/raw/10_raw_overview.py b/tutorials/raw/10_raw_overview.py index 198cb5264ee..b059cba86dd 100644 --- a/tutorials/raw/10_raw_overview.py +++ b/tutorials/raw/10_raw_overview.py @@ -18,8 +18,10 @@ # %% import os -import numpy as np + import matplotlib.pyplot as plt +import numpy as np + import mne # %% diff --git a/tutorials/raw/20_event_arrays.py b/tutorials/raw/20_event_arrays.py index e13b1f361a7..4390634dfd8 100644 --- a/tutorials/raw/20_event_arrays.py +++ b/tutorials/raw/20_event_arrays.py @@ -16,7 +16,9 @@ # %% import os + import numpy as np + import mne sample_data_folder = mne.datasets.sample.data_path() diff --git a/tutorials/raw/30_annotate_raw.py b/tutorials/raw/30_annotate_raw.py index 79d0577ed6b..90387f0a5e8 100644 --- a/tutorials/raw/30_annotate_raw.py +++ b/tutorials/raw/30_annotate_raw.py @@ -18,6 +18,7 @@ import os from datetime import timedelta + import mne sample_data_folder = mne.datasets.sample.data_path() diff --git a/tutorials/simulation/70_point_spread.py b/tutorials/simulation/70_point_spread.py index c197add9833..777c8996d3c 100644 --- a/tutorials/simulation/70_point_spread.py +++ b/tutorials/simulation/70_point_spread.py @@ -16,9 +16,8 @@ import mne from mne.datasets import sample - -from mne.minimum_norm import read_inverse_operator, apply_inverse -from mne.simulation import simulate_stc, simulate_evoked +from mne.minimum_norm import apply_inverse, read_inverse_operator +from mne.simulation import simulate_evoked, simulate_stc # %% # First, we set some parameters. diff --git a/tutorials/simulation/80_dics.py b/tutorials/simulation/80_dics.py index 951671df1e4..61e6fedcc44 100644 --- a/tutorials/simulation/80_dics.py +++ b/tutorials/simulation/80_dics.py @@ -23,15 +23,15 @@ # We first import the required packages to run this tutorial and define a list # of filenames for various things we'll be using. import numpy as np -from scipy.signal import welch, coherence, unit_impulse from matplotlib import pyplot as plt +from scipy.signal import coherence, unit_impulse, welch import mne -from mne.simulation import simulate_raw, add_noise +from mne.beamformer import apply_dics_csd, make_dics from mne.datasets import sample -from mne.minimum_norm import make_inverse_operator, apply_inverse +from mne.minimum_norm import apply_inverse, make_inverse_operator +from mne.simulation import add_noise, simulate_raw from mne.time_frequency import csd_morlet -from mne.beamformer import make_dics, apply_dics_csd # We use the MEG and MRI setup from the MNE-sample dataset data_path = sample.data_path(download=False) diff --git a/tutorials/stats-sensor-space/10_background_stats.py b/tutorials/stats-sensor-space/10_background_stats.py index 412715b3042..1d0c88149a6 100644 --- a/tutorials/stats-sensor-space/10_background_stats.py +++ b/tutorials/stats-sensor-space/10_background_stats.py @@ -16,18 +16,18 @@ from functools import partial -import numpy as np -from scipy import stats import matplotlib.pyplot as plt +import numpy as np from mpl_toolkits.mplot3d import Axes3D # noqa: F401, analysis:ignore +from scipy import stats import mne from mne.stats import ( - ttest_1samp_no_p, bonferroni_correction, fdr_correction, - permutation_t_test, permutation_cluster_1samp_test, + permutation_t_test, + ttest_1samp_no_p, ) # %% diff --git a/tutorials/stats-sensor-space/20_erp_stats.py b/tutorials/stats-sensor-space/20_erp_stats.py index 504a323cbe6..08b5a583ffc 100644 --- a/tutorials/stats-sensor-space/20_erp_stats.py +++ b/tutorials/stats-sensor-space/20_erp_stats.py @@ -17,8 +17,8 @@ # %% -import numpy as np import matplotlib.pyplot as plt +import numpy as np from scipy.stats import ttest_ind import mne diff --git a/tutorials/stats-sensor-space/40_cluster_1samp_time_freq.py b/tutorials/stats-sensor-space/40_cluster_1samp_time_freq.py index cf49f48ddf4..e6ced1c8903 100644 --- a/tutorials/stats-sensor-space/40_cluster_1samp_time_freq.py +++ b/tutorials/stats-sensor-space/40_cluster_1samp_time_freq.py @@ -32,14 +32,14 @@ # %% -import numpy as np import matplotlib.pyplot as plt +import numpy as np import scipy.stats import mne -from mne.time_frequency import tfr_morlet -from mne.stats import permutation_cluster_1samp_test from mne.datasets import sample +from mne.stats import permutation_cluster_1samp_test +from mne.time_frequency import tfr_morlet # %% # Set parameters diff --git a/tutorials/stats-sensor-space/50_cluster_between_time_freq.py b/tutorials/stats-sensor-space/50_cluster_between_time_freq.py index 69bdbbc5d91..790de36a42c 100644 --- a/tutorials/stats-sensor-space/50_cluster_between_time_freq.py +++ b/tutorials/stats-sensor-space/50_cluster_between_time_freq.py @@ -25,13 +25,13 @@ # %% -import numpy as np import matplotlib.pyplot as plt +import numpy as np import mne -from mne.time_frequency import tfr_morlet -from mne.stats import permutation_cluster_test from mne.datasets import sample +from mne.stats import permutation_cluster_test +from mne.time_frequency import tfr_morlet print(__doc__) diff --git a/tutorials/stats-sensor-space/70_cluster_rmANOVA_time_freq.py b/tutorials/stats-sensor-space/70_cluster_rmANOVA_time_freq.py index a57112bedc4..f7b274d3ce4 100644 --- a/tutorials/stats-sensor-space/70_cluster_rmANOVA_time_freq.py +++ b/tutorials/stats-sensor-space/70_cluster_rmANOVA_time_freq.py @@ -29,13 +29,13 @@ # %% -import numpy as np import matplotlib.pyplot as plt +import numpy as np import mne -from mne.time_frequency import tfr_morlet -from mne.stats import f_threshold_mway_rm, f_mway_rm, fdr_correction from mne.datasets import sample +from mne.stats import f_mway_rm, f_threshold_mway_rm, fdr_correction +from mne.time_frequency import tfr_morlet print(__doc__) diff --git a/tutorials/stats-sensor-space/75_cluster_ftest_spatiotemporal.py b/tutorials/stats-sensor-space/75_cluster_ftest_spatiotemporal.py index 7a3234c5346..7c3e272a933 100644 --- a/tutorials/stats-sensor-space/75_cluster_ftest_spatiotemporal.py +++ b/tutorials/stats-sensor-space/75_cluster_ftest_spatiotemporal.py @@ -31,17 +31,17 @@ # %% -import numpy as np import matplotlib.pyplot as plt -from mpl_toolkits.axes_grid1 import make_axes_locatable +import numpy as np import scipy.stats +from mpl_toolkits.axes_grid1 import make_axes_locatable import mne -from mne.stats import spatio_temporal_cluster_test, combine_adjacency -from mne.datasets import sample from mne.channels import find_ch_adjacency -from mne.viz import plot_compare_evokeds +from mne.datasets import sample +from mne.stats import combine_adjacency, spatio_temporal_cluster_test from mne.time_frequency import tfr_morlet +from mne.viz import plot_compare_evokeds # %% # Set parameters diff --git a/tutorials/stats-source-space/20_cluster_1samp_spatiotemporal.py b/tutorials/stats-source-space/20_cluster_1samp_spatiotemporal.py index 4c766b5bbbc..d2210f56842 100644 --- a/tutorials/stats-source-space/20_cluster_1samp_spatiotemporal.py +++ b/tutorials/stats-source-space/20_cluster_1samp_spatiotemporal.py @@ -24,10 +24,10 @@ from scipy import stats as stats import mne +from mne.datasets import sample from mne.epochs import equalize_epoch_counts -from mne.stats import spatio_temporal_cluster_1samp_test, summarize_clusters_stc from mne.minimum_norm import apply_inverse, read_inverse_operator -from mne.datasets import sample +from mne.stats import spatio_temporal_cluster_1samp_test, summarize_clusters_stc # %% # Set parameters diff --git a/tutorials/stats-source-space/30_cluster_ftest_spatiotemporal.py b/tutorials/stats-source-space/30_cluster_ftest_spatiotemporal.py index 88c739411a3..118011c32f6 100644 --- a/tutorials/stats-source-space/30_cluster_ftest_spatiotemporal.py +++ b/tutorials/stats-source-space/30_cluster_ftest_spatiotemporal.py @@ -21,8 +21,8 @@ import mne from mne import spatial_src_adjacency -from mne.stats import spatio_temporal_cluster_test, summarize_clusters_stc from mne.datasets import sample +from mne.stats import spatio_temporal_cluster_test, summarize_clusters_stc print(__doc__) diff --git a/tutorials/stats-source-space/60_cluster_rmANOVA_spatiotemporal.py b/tutorials/stats-source-space/60_cluster_rmANOVA_spatiotemporal.py index 3c41afe8fc3..dd9d405dd13 100644 --- a/tutorials/stats-source-space/60_cluster_rmANOVA_spatiotemporal.py +++ b/tutorials/stats-source-space/60_cluster_rmANOVA_spatiotemporal.py @@ -23,21 +23,20 @@ # %% +import matplotlib.pyplot as plt import numpy as np from numpy.random import randn -import matplotlib.pyplot as plt import mne +from mne.datasets import sample +from mne.minimum_norm import apply_inverse, read_inverse_operator from mne.stats import ( - spatio_temporal_cluster_test, - f_threshold_mway_rm, f_mway_rm, + f_threshold_mway_rm, + spatio_temporal_cluster_test, summarize_clusters_stc, ) -from mne.minimum_norm import apply_inverse, read_inverse_operator -from mne.datasets import sample - print(__doc__) # %% diff --git a/tutorials/time-freq/50_ssvep.py b/tutorials/time-freq/50_ssvep.py index c5fa54ff1f1..611d6b19fe9 100644 --- a/tutorials/time-freq/50_ssvep.py +++ b/tutorials/time-freq/50_ssvep.py @@ -43,10 +43,11 @@ # %% import matplotlib.pyplot as plt -import mne import numpy as np from scipy.stats import ttest_rel +import mne + # %% # Data preprocessing # ------------------ diff --git a/tutorials/visualization/10_publication_figure.py b/tutorials/visualization/10_publication_figure.py index 9ca5cf03fc5..cc6d83b29f8 100644 --- a/tutorials/visualization/10_publication_figure.py +++ b/tutorials/visualization/10_publication_figure.py @@ -20,9 +20,9 @@ # ------- # We are importing everything we need for this example: -import numpy as np import matplotlib.pyplot as plt -from mpl_toolkits.axes_grid1 import make_axes_locatable, ImageGrid, inset_locator +import numpy as np +from mpl_toolkits.axes_grid1 import ImageGrid, inset_locator, make_axes_locatable import mne diff --git a/tutorials/visualization/20_ui_events.py b/tutorials/visualization/20_ui_events.py index 1c799a60956..014a8d2a057 100644 --- a/tutorials/visualization/20_ui_events.py +++ b/tutorials/visualization/20_ui_events.py @@ -19,9 +19,10 @@ # Author: Marijn van Vliet # # License: BSD-3-Clause -import mne import matplotlib.pyplot as plt -from mne.viz.ui_events import publish, subscribe, link, TimeChange + +import mne +from mne.viz.ui_events import TimeChange, link, publish, subscribe # Turn on interactivity plt.ion() From a5ce1cbf7b29fdfd964bd9eb36b10319d2b3025b Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Wed, 11 Oct 2023 20:35:41 +0300 Subject: [PATCH 007/405] MAINT: isort (#12102) --- .git-blame-ignore-revs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index bfbf8587d7d..3e511b1a194 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -1,3 +1,3 @@ e81ec528a42ac687f3d961ed5cf8e25f236925b0 # black -20737c0bd7c0dafee0a00caa1dba8ce573fd0f53 # move SetChannelsMixin between files 12395f9d9cf6ea3c72b225b62e052dd0d17d9889 # YAML indentation +d6d2f8c6a2ed4a0b27357da9ddf8e0cd14931b59 # isort From c7c8a2919734963726cd2d04fa9e01002b7f843c Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Wed, 11 Oct 2023 23:18:40 +0300 Subject: [PATCH 008/405] ENH: Add MEG sensor option to coreg (#12098) --- doc/changes/devel.rst | 2 + mne/gui/_coreg.py | 138 +++++++++++++++++++++++++----------- mne/gui/tests/test_coreg.py | 6 ++ mne/viz/_3d.py | 10 +-- 4 files changed, 112 insertions(+), 44 deletions(-) diff --git a/doc/changes/devel.rst b/doc/changes/devel.rst index b46c2a6fc60..912af2555ef 100644 --- a/doc/changes/devel.rst +++ b/doc/changes/devel.rst @@ -37,6 +37,7 @@ Enhancements - Add :class:`~mne.time_frequency.EpochsSpectrumArray` and :class:`~mne.time_frequency.SpectrumArray` to support creating power spectra from :class:`NumPy array ` data (:gh:`11803` by `Alex Rockhill`_) - Add support for writing forward solutions to HDF5 and convenience function :meth:`mne.Forward.save` (:gh:`12036` by `Eric Larson`_) - Refactored internals of :func:`mne.read_annotations` (:gh:`11964` by `Paul Roujansky`_) +- Add support for drawing MEG sensors in :ref:`mne coreg` (:gh:`12098` by `Eric Larson`_) - By default MNE-Python creates matplotlib figures with ``layout='constrained'`` rather than the default ``layout='tight'`` (:gh:`12050` by `Mathieu Scheltienne`_ and `Eric Larson`_) - Enhance :func:`~mne.viz.plot_evoked_field` with a GUI that has controls for time, colormap, and contour lines (:gh:`11942` by `Marijn van Vliet`_) - Add :class:`mne.viz.ui_events.UIEvent` linking for interactive colorbars, allowing users to link figures and change the colormap and limits interactively. This supports :func:`~mne.viz.plot_evoked_topomap`, :func:`~mne.viz.plot_ica_components`, :func:`~mne.viz.plot_tfr_topomap`, :func:`~mne.viz.plot_projs_topomap`, :meth:`~mne.Evoked.plot_image`, and :meth:`~mne.Epochs.plot_image` (:gh:`12057` by `Santeri Ruuskanen`_) @@ -60,6 +61,7 @@ Bugs - Fix bug with :meth:`~mne.viz.Brain.add_annotation` when reading an annotation from a file with both hemispheres shown (:gh:`11946` by `Marijn van Vliet`_) - Fix bug with axis clip box boundaries in :func:`mne.viz.plot_evoked_topo` and related functions (:gh:`11999` by `Eric Larson`_) - Fix bug with ``subject_info`` when loading data from and exporting to EDF file (:gh:`11952` by `Paul Roujansky`_) +- Fix rendering glitches when plotting Neuromag/TRIUX sensors in :func:`mne.viz.plot_alignment` and related functions (:gh:`12098` by `Eric Larson`_) - Fix bug with delayed checking of :class:`info["bads"] ` (:gh:`12038` by `Eric Larson`_) - Fix bug with :func:`mne.viz.plot_alignment` where ``sensor_colors`` were not handled properly on a per-channel-type basis (:gh:`12067` by `Eric Larson`_) - Fix handling of channel information in annotations when loading data from and exporting to EDF file (:gh:`11960` :gh:`12017` :gh:`12044` by `Paul Roujansky`_) diff --git a/mne/gui/_coreg.py b/mne/gui/_coreg.py index 3782303c58e..e07848db693 100644 --- a/mne/gui/_coreg.py +++ b/mne/gui/_coreg.py @@ -102,6 +102,10 @@ class CoregistrationUI(HasTraits): If True, display the head shape points. Defaults to True. eeg_channels : bool If True, display the EEG channels. Defaults to True. + meg_channels : bool + If True, display the MEG channels. Defaults to False. + fnirs_channels : bool + If True, display the fNIRS channels. Defaults to True. orient_glyphs : bool If True, orient the sensors towards the head surface. Default to False. scale_by_distance : bool @@ -153,6 +157,8 @@ class CoregistrationUI(HasTraits): _hpi_coils = Bool() _head_shape_points = Bool() _eeg_channels = Bool() + _meg_channels = Bool() + _fnirs_channels = Bool() _head_resolution = Bool() _head_opacity = Float() _helmet = Bool() @@ -177,6 +183,8 @@ def __init__( hpi_coils=None, head_shape_points=None, eeg_channels=None, + meg_channels=None, + fnirs_channels=None, orient_glyphs=None, scale_by_distance=None, mark_inside=None, @@ -231,6 +239,8 @@ def _get_default(var, val): hpi_coils=_get_default(hpi_coils, True), head_shape_points=_get_default(head_shape_points, True), eeg_channels=_get_default(eeg_channels, True), + meg_channels=_get_default(meg_channels, False), + fnirs_channels=_get_default(fnirs_channels, True), head_resolution=_get_default(head_resolution, True), head_opacity=_get_default(head_opacity, 0.8), helmet=False, @@ -303,6 +313,8 @@ def _get_default(var, val): self._set_hpi_coils(self._defaults["hpi_coils"]) self._set_head_shape_points(self._defaults["head_shape_points"]) self._set_eeg_channels(self._defaults["eeg_channels"]) + self._set_meg_channels(self._defaults["meg_channels"]) + self._set_fnirs_channels(self._defaults["fnirs_channels"]) self._set_head_resolution(self._defaults["head_resolution"]) self._set_helmet(self._defaults["helmet"]) self._set_grow_hair(self._defaults["grow_hair"]) @@ -351,7 +363,7 @@ def _get_default(var, val): True: dict(azimuth=90, elevation=90), # front False: dict(azimuth=180, elevation=90), } # left - self._renderer.set_camera(distance=None, **views[self._lock_fids]) + self._renderer.set_camera(distance="auto", **views[self._lock_fids]) self._redraw() # XXX: internal plotter/renderer should not be exposed if not self._immediate_redraw: @@ -482,6 +494,12 @@ def _set_head_shape_points(self, state): def _set_eeg_channels(self, state): self._eeg_channels = bool(state) + def _set_meg_channels(self, state): + self._meg_channels = bool(state) + + def _set_fnirs_channels(self, state): + self._fnirs_channels = bool(state) + def _set_head_resolution(self, state): self._head_resolution = bool(state) @@ -567,6 +585,8 @@ def _set_point_weight(self, weight, point): "hpi": "_set_hpi_coils", "hsp": "_set_head_shape_points", "eeg": "_set_eeg_channels", + "meg": "_set_meg_channels", + "fnirs": "_set_fnirs_channels", } if point in funcs.keys(): getattr(self, funcs[point])(weight > 0) @@ -611,6 +631,7 @@ def _lock_fids_changed(self, change=None): "save_mri_fids", # View options "helmet", + "meg", "head_opacity", "high_res_head", # Digitization source @@ -704,11 +725,11 @@ def _info_file_changed(self, change=None): @observe("_orient_glyphs") def _orient_glyphs_changed(self, change=None): - self._update_plot(["hpi", "hsp", "eeg"]) + self._update_plot(["hpi", "hsp", "sensors"]) @observe("_scale_by_distance") def _scale_by_distance_changed(self, change=None): - self._update_plot(["hpi", "hsp", "eeg"]) + self._update_plot(["hpi", "hsp", "sensors"]) @observe("_mark_inside") def _mark_inside_changed(self, change=None): @@ -724,7 +745,15 @@ def _head_shape_point_changed(self, change=None): @observe("_eeg_channels") def _eeg_channels_changed(self, change=None): - self._update_plot("eeg") + self._update_plot("sensors") + + @observe("_meg_channels") + def _meg_channels_changed(self, change=None): + self._update_plot("sensors") + + @observe("_fnirs_channels") + def _fnirs_channels_changed(self, change=None): + self._update_plot("sensors") @observe("_head_resolution") def _head_resolution_changed(self, change=None): @@ -825,6 +854,7 @@ def _configure_legend(self): mri_fids_legend_actor = self._renderer.legend(labels=labels) self._update_actor("mri_fids_legend", mri_fids_legend_actor) + @safe_event @verbose def _redraw(self, *, verbose=None): if not self._redraws_pending: @@ -834,7 +864,7 @@ def _redraw(self, *, verbose=None): mri_fids=self._add_mri_fiducials, hsp=self._add_head_shape_points, hpi=self._add_hpi_coils, - eeg=self._add_eeg_fnirs_channels, + sensors=self._add_channels, head_fids=self._add_head_fiducials, helmet=self._add_helmet, ) @@ -957,7 +987,7 @@ def _update_plot(self, changes="all", verbose=None): "mri_fids", # MRI first "hsp", "hpi", - "eeg", + "sensors", "head_fids", # then dig "helmet", ) @@ -1041,7 +1071,7 @@ def _follow_fiducial_view(self): kwargs = dict(front=(90.0, 90.0), left=(180, 90), right=(0.0, 90)) kwargs = dict(zip(("azimuth", "elevation"), kwargs[view[fid]])) if not self._lock_fids: - self._renderer.set_camera(distance=None, **kwargs) + self._renderer.set_camera(distance="auto", **kwargs) def _update_fiducials(self): fid = self._current_fiducial @@ -1145,7 +1175,13 @@ def _forward_widget_command( return ret def _set_sensors_visibility(self, state): - sensors = ["head_fiducials", "hpi_coils", "head_shape_points", "eeg_channels"] + sensors = [ + "head_fiducials", + "hpi_coils", + "head_shape_points", + "sensors", + "helmet", + ] for sensor in sensors: if sensor in self._actors and self._actors[sensor] is not None: actors = self._actors[sensor] @@ -1156,7 +1192,13 @@ def _set_sensors_visibility(self, state): def _update_actor(self, actor_name, actor): # XXX: internal plotter/renderer should not be exposed - self._renderer.plotter.remove_actor(self._actors.get(actor_name), render=False) + # Work around PyVista sequential update bug with iterable until > 0.42.3 is req + # https://github.com/pyvista/pyvista/pull/5046 + actors = self._actors.get(actor_name) or [] # convert None to list + if not isinstance(actors, list): + actors = [actors] + for this_actor in actors: + self._renderer.plotter.remove_actor(this_actor, render=False) self._actors[actor_name] = actor def _add_mri_fiducials(self): @@ -1216,35 +1258,44 @@ def _add_head_shape_points(self): hsp_actors = None self._update_actor("head_shape_points", hsp_actors) - def _add_eeg_fnirs_channels(self): + def _add_channels(self): + plot_types = dict(eeg=False, meg=False, fnirs=False) if self._eeg_channels: - eeg = ["original"] - picks = pick_types(self._info, eeg=(len(eeg) > 0), fnirs=True) - if len(picks) > 0: - actors = _plot_sensors( - self._renderer, - self._info, - self._to_cf_t, - picks, - meg=False, - eeg=eeg, - fnirs=["sources", "detectors"], - warn_meg=False, - head_surf=self._head_geo, - units="m", - sensor_opacity=self._defaults["sensor_opacity"], - orient_glyphs=self._orient_glyphs, - scale_by_distance=self._scale_by_distance, - surf=self._head_geo, - check_inside=self._check_inside, - nearest=self._nearest, - ) - sens_actors = sum(actors.values(), list()) - else: - sens_actors = None - else: - sens_actors = None - self._update_actor("eeg_channels", sens_actors) + plot_types["eeg"] = ["original"] + if self._meg_channels: + plot_types["meg"] = ["sensors"] + if self._fnirs_channels: + plot_types["fnirs"] = ["sources", "detectors"] + sens_actors = list() + # until opacity can be specified using a dict, we need to iterate + sensor_opacity = dict( + eeg=self._defaults["sensor_opacity"], + fnirs=self._defaults["sensor_opacity"], + meg=0.25, + ) + for ch_type, plot_type in plot_types.items(): + picks = pick_types(self._info, ref_meg=False, **{ch_type: True}) + if not (len(picks) and plot_type): + continue + logger.debug(f"Drawing {ch_type} sensors") + these_actors = _plot_sensors( + self._renderer, + self._info, + self._to_cf_t, + picks=picks, + warn_meg=False, + head_surf=self._head_geo, + units="m", + sensor_opacity=sensor_opacity[ch_type], + orient_glyphs=self._orient_glyphs, + scale_by_distance=self._scale_by_distance, + surf=self._head_geo, + check_inside=self._check_inside, + nearest=self._nearest, + **plot_types, + ) + sens_actors.extend(sum(these_actors.values(), list())) + self._update_actor("sensors", sens_actors) def _add_head_surface(self): bem = None @@ -1335,7 +1386,7 @@ def _fits_icp(self): def _fit_icp_real(self, *, update_head): with self._lock(params=True, fitting=True): self._current_icp_iterations = 0 - updates = ["hsp", "hpi", "eeg", "head_fids", "helmet"] + updates = ["hsp", "hpi", "sensors", "head_fids", "helmet"] if update_head: updates.insert(0, "head") @@ -1533,7 +1584,7 @@ def _configure_dock(self): collapse = True # collapsible and collapsed else: collapse = None # not collapsible - self._renderer._dock_initialize(name="Input", area="left", max_width="350px") + self._renderer._dock_initialize(name="Input", area="left", max_width="375px") mri_subject_layout = self._renderer._dock_add_group_box( name="MRI Subject", collapse=collapse, @@ -1706,6 +1757,13 @@ def _configure_dock(self): tooltip="Enable/Disable MEG helmet", layout=view_options_layout, ) + self._widgets["meg"] = self._renderer._dock_add_check_box( + name="Show MEG sensors", + value=self._helmet, + callback=self._set_meg_channels, + tooltip="Enable/Disable MEG sensors", + layout=view_options_layout, + ) self._widgets["high_res_head"] = self._renderer._dock_add_check_box( name="Show high-resolution head", value=self._head_resolution, @@ -1725,7 +1783,7 @@ def _configure_dock(self): self._renderer._dock_add_stretch() self._renderer._dock_initialize( - name="Parameters", area="right", max_width="350px" + name="Parameters", area="right", max_width="375px" ) mri_scaling_layout = self._renderer._dock_add_group_box( name="MRI Scaling", diff --git a/mne/gui/tests/test_coreg.py b/mne/gui/tests/test_coreg.py index 1805755d0ff..666ec7d28a0 100644 --- a/mne/gui/tests/test_coreg.py +++ b/mne/gui/tests/test_coreg.py @@ -252,6 +252,12 @@ def test_coreg_gui_pyvista_basic(tmp_path, monkeypatch, renderer_interactive_pyv coreg._redraw(verbose="debug") log = log.getvalue() assert "Drawing helmet" in log + assert not coreg._meg_channels + coreg._set_meg_channels(True) + assert coreg._meg_channels + with catch_logging() as log: + coreg._redraw(verbose="debug") + assert "Drawing meg sensors" in log.getvalue() assert coreg._orient_glyphs assert coreg._scale_by_distance assert coreg._mark_inside diff --git a/mne/viz/_3d.py b/mne/viz/_3d.py index feb01f9c850..14edb396edf 100644 --- a/mne/viz/_3d.py +++ b/mne/viz/_3d.py @@ -1598,7 +1598,7 @@ def _sensor_shape(coil): except ImportError: # scipy < 1.8 from scipy.spatial.qhull import QhullError id_ = coil["type"] & 0xFFFF - pad = True + z_value = 0 # Square figure eight if id_ in ( FIFF.FIFFV_COIL_NM_122, @@ -1624,6 +1624,8 @@ def _sensor_shape(coil): tris = np.concatenate( (_make_tris_fan(4), _make_tris_fan(4)[:, ::-1] + 4), axis=0 ) + # Offset for visibility (using heuristic for sanely named Neuromag coils) + z_value = 0.001 * (1 + coil["chname"].endswith("2")) # Square elif id_ in ( FIFF.FIFFV_COIL_POINT_MAGNETOMETER, @@ -1694,11 +1696,11 @@ def _sensor_shape(coil): rr_rot = rrs @ u tris = Delaunay(rr_rot[:, :2]).simplices tris = np.concatenate((tris, tris[:, ::-1])) - pad = False + z_value = None # Go from (x,y) -> (x,y,z) - if pad: - rrs = np.pad(rrs, ((0, 0), (0, 1)), mode="constant") + if z_value is not None: + rrs = np.pad(rrs, ((0, 0), (0, 1)), mode="constant", constant_values=z_value) assert rrs.ndim == 2 and rrs.shape[1] == 3 return rrs, tris From a5eb54846d0d5efb6f2519490d23569516c17d6d Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Thu, 12 Oct 2023 21:31:29 +0300 Subject: [PATCH 009/405] BUG: Fix constrained layout in psd.plot (#12103) --- doc/changes/devel.rst | 2 +- mne/viz/_mpl_figure.py | 43 ++++++------------------- mne/viz/epochs.py | 18 ++++++++--- mne/viz/evoked.py | 2 +- mne/viz/topomap.py | 9 ++---- tutorials/epochs/20_visualize_epochs.py | 1 - tutorials/evoked/20_visualize_evoked.py | 1 + tutorials/raw/40_visualize_raw.py | 1 - 8 files changed, 29 insertions(+), 48 deletions(-) diff --git a/doc/changes/devel.rst b/doc/changes/devel.rst index 912af2555ef..6474a846f35 100644 --- a/doc/changes/devel.rst +++ b/doc/changes/devel.rst @@ -38,7 +38,7 @@ Enhancements - Add support for writing forward solutions to HDF5 and convenience function :meth:`mne.Forward.save` (:gh:`12036` by `Eric Larson`_) - Refactored internals of :func:`mne.read_annotations` (:gh:`11964` by `Paul Roujansky`_) - Add support for drawing MEG sensors in :ref:`mne coreg` (:gh:`12098` by `Eric Larson`_) -- By default MNE-Python creates matplotlib figures with ``layout='constrained'`` rather than the default ``layout='tight'`` (:gh:`12050` by `Mathieu Scheltienne`_ and `Eric Larson`_) +- By default MNE-Python creates matplotlib figures with ``layout='constrained'`` rather than the default ``layout='tight'`` (:gh:`12050`, :gh:`12103` by `Mathieu Scheltienne`_ and `Eric Larson`_) - Enhance :func:`~mne.viz.plot_evoked_field` with a GUI that has controls for time, colormap, and contour lines (:gh:`11942` by `Marijn van Vliet`_) - Add :class:`mne.viz.ui_events.UIEvent` linking for interactive colorbars, allowing users to link figures and change the colormap and limits interactively. This supports :func:`~mne.viz.plot_evoked_topomap`, :func:`~mne.viz.plot_ica_components`, :func:`~mne.viz.plot_tfr_topomap`, :func:`~mne.viz.plot_projs_topomap`, :meth:`~mne.Evoked.plot_image`, and :meth:`~mne.Epochs.plot_image` (:gh:`12057` by `Santeri Ruuskanen`_) diff --git a/mne/viz/_mpl_figure.py b/mne/viz/_mpl_figure.py index 2149aa73d1f..b0a059c97cf 100644 --- a/mne/viz/_mpl_figure.py +++ b/mne/viz/_mpl_figure.py @@ -2331,38 +2331,16 @@ def _get_scale_bar_texts(self): class MNELineFigure(MNEFigure): """Interactive figure for non-scrolling line plots.""" - def __init__(self, inst, n_axes, figsize, *, layout=None, **kwargs): - super().__init__(figsize=figsize, inst=inst, layout=layout, **kwargs) - - # AXES: default margins (inches) - l_margin = 0.8 - r_margin = 0.2 - b_margin = 0.65 - t_margin = 0.35 - # AXES: default margins (figure-relative coordinates) - left = self._inch_to_rel(l_margin) - right = 1 - self._inch_to_rel(r_margin) - bottom = self._inch_to_rel(b_margin, horiz=False) - top = 1 - self._inch_to_rel(t_margin, horiz=False) - # AXES: make subplots - axes = [self.add_subplot(n_axes, 1, 1)] - for ix in range(1, n_axes): - axes.append(self.add_subplot(n_axes, 1, ix + 1, sharex=axes[0])) - self.subplotpars.update( - left=left, bottom=bottom, top=top, right=right, hspace=0.4 - ) - # save useful things - self.mne.ax_list = axes - - def _resize(self, event): - """Handle resize event.""" - old_width, old_height = self.mne.fig_size_px - new_width, new_height = self._get_size_px() - new_margins = _calc_new_margins( - self, old_width, old_height, new_width, new_height + def __init__(self, inst, n_axes, figsize, *, layout="constrained", **kwargs): + super().__init__( + figsize=figsize, + inst=inst, + layout=layout, + sharex=True, + **kwargs, ) - self.subplots_adjust(**new_margins) - self.mne.fig_size_px = (new_width, new_height) + for ix in range(n_axes): + self.add_subplot(n_axes, 1, ix + 1) def _close_all(): @@ -2426,11 +2404,10 @@ def _line_figure(inst, axes=None, picks=None, **kwargs): FigureClass=MNELineFigure, figsize=figsize, n_axes=n_axes, - layout=None, **kwargs, ) fig.mne.fig_size_px = fig._get_size_px() # can't do in __init__ - axes = fig.mne.ax_list + axes = fig.axes return fig, axes diff --git a/mne/viz/epochs.py b/mne/viz/epochs.py index 5918d6d6aec..613f3a2b62a 100644 --- a/mne/viz/epochs.py +++ b/mne/viz/epochs.py @@ -302,9 +302,11 @@ def plot_epochs_image( # check for compatible `fig` / `axes`; instantiate figs if needed; add # fig(s) and axes into group_by + needs_colorbar = colorbar and (axes is not None or fig is not None) group_by = _validate_fig_and_axes( - fig, axes, group_by, evoked, colorbar, clear=clear + fig, axes, group_by, evoked, colorbar=needs_colorbar, clear=clear ) + del fig, axes, needs_colorbar, clear # prepare images in advance to get consistent vmin/vmax. # At the same time, create a subsetted epochs object for each group @@ -649,20 +651,26 @@ def _plot_epochs_image( ax["evoked"].xaxis.set_major_locator(loc) ax["evoked"].yaxis.set_major_locator(AutoLocator()) + fig = ax_im.get_figure() + # draw the colorbar if colorbar: from matplotlib.pyplot import colorbar as cbar - this_colorbar = cbar(im, cax=ax["colorbar"]) - this_colorbar.ax.set_ylabel(unit, rotation=270, labelpad=12) + if "colorbar" in ax: # axes supplied by user + this_colorbar = cbar(im, cax=ax["colorbar"]) + this_colorbar.ax.set_ylabel(unit, rotation=270, labelpad=12) + else: # we created them + this_colorbar = fig.colorbar(im, ax=ax_im) + this_colorbar.ax.set_title(unit) if cmap[1]: ax_im.CB = DraggableColorbar( this_colorbar, im, kind="epochs_image", ch_type=unit ) # finish - plt_show(show) - return ax_im.get_figure() + plt_show(show, fig=fig) + return fig def plot_drop_log( diff --git a/mne/viz/evoked.py b/mne/viz/evoked.py index 88340295d78..6abcbcc0d1d 100644 --- a/mne/viz/evoked.py +++ b/mne/viz/evoked.py @@ -2027,7 +2027,7 @@ def plot_evoked_joint( if topomap_args.get("colorbar", True): from matplotlib import ticker - cbar = fig.colorbar(map_ax[0].images[0], ax=map_ax, cax=cbar_ax) + cbar = fig.colorbar(map_ax[0].images[0], ax=map_ax, cax=cbar_ax, shrink=0.8) cbar.ax.grid(False) # auto-removal deprecated as of 2021/10/05 if isinstance(contours, (list, np.ndarray)): cbar.set_ticks(contours) diff --git a/mne/viz/topomap.py b/mne/viz/topomap.py index a778c72dc1e..8629525ac4d 100644 --- a/mne/viz/topomap.py +++ b/mne/viz/topomap.py @@ -304,15 +304,13 @@ def _add_colorbar( im, cmap, *, - side="right", title=None, format=None, - size="5%", kind=None, ch_type=None, ): """Add a colorbar to an axis.""" - cbar = ax.figure.colorbar(im, format=format) + cbar = ax.figure.colorbar(im, format=format, shrink=0.6) if cmap is not None and cmap[1]: ax.CB = DraggableColorbar(cbar, im, kind, ch_type) cax = cbar.ax @@ -1712,7 +1710,6 @@ def plot_ica_components( im, cmap, title="AU", - side="right", format=cbar_fmt, kind="ica_comp_topomap", ch_type=ch_type, @@ -2564,7 +2561,7 @@ def _plot_topomap_multi_cbar( ) if colorbar: - cbar, cax = _add_colorbar(ax, im, cmap, title=None, size="10%", format=cbar_fmt) + cbar, cax = _add_colorbar(ax, im, cmap, title=None, format=cbar_fmt) cbar.set_ticks(_vlim) if unit is not None: cbar.ax.set_ylabel(unit, fontsize=8) @@ -3744,7 +3741,7 @@ def plot_bridged_electrodes( if title is not None: im.axes.set_title(title) if colorbar: - cax = fig.colorbar(im) + cax = fig.colorbar(im, shrink=0.6) cax.set_label(r"Electrical Distance ($\mu$$V^2$)") return fig diff --git a/tutorials/epochs/20_visualize_epochs.py b/tutorials/epochs/20_visualize_epochs.py index ef16211c84a..4a1606afed5 100644 --- a/tutorials/epochs/20_visualize_epochs.py +++ b/tutorials/epochs/20_visualize_epochs.py @@ -12,7 +12,6 @@ We'll start by importing the modules we need, loading the continuous (raw) sample data, and cropping it to save memory: """ - # %% import mne diff --git a/tutorials/evoked/20_visualize_evoked.py b/tutorials/evoked/20_visualize_evoked.py index 180f995d820..e07b1e2e601 100644 --- a/tutorials/evoked/20_visualize_evoked.py +++ b/tutorials/evoked/20_visualize_evoked.py @@ -10,6 +10,7 @@ As usual we'll start by importing the modules we need: """ + # %% import numpy as np diff --git a/tutorials/raw/40_visualize_raw.py b/tutorials/raw/40_visualize_raw.py index 8ce101a2ecf..28fbfa8fa9e 100644 --- a/tutorials/raw/40_visualize_raw.py +++ b/tutorials/raw/40_visualize_raw.py @@ -13,7 +13,6 @@ :ref:`example data `, and cropping the `~mne.io.Raw` object to just 60 seconds before loading it into RAM to save memory: """ - # %% import os From c9d20060d74b8f384aedfeb02c122ca7c31986fa Mon Sep 17 00:00:00 2001 From: Scott Huberty <52462026+scott-huberty@users.noreply.github.com> Date: Thu, 12 Oct 2023 11:47:56 -0700 Subject: [PATCH 010/405] ENH: eyetracking plot_heatmap function (#11798) Co-authored-by: Eric Larson Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Daniel McCloy Co-authored-by: Mathieu Scheltienne --- doc/api/visualization.rst | 15 ++ doc/documentation/datasets.rst | 28 +++- .../visualization/eyetracking_plot_heatmap.py | 89 ++++++++++ mne/datasets/config.py | 8 +- mne/utils/docs.py | 8 + mne/viz/eyetracking/__init__.py | 5 + mne/viz/eyetracking/heatmap.py | 155 ++++++++++++++++++ mne/viz/eyetracking/tests/__init__.py | 0 mne/viz/eyetracking/tests/test_heatmap.py | 35 ++++ .../preprocessing/90_eyetracking_data.py | 28 +++- 10 files changed, 357 insertions(+), 14 deletions(-) create mode 100644 examples/visualization/eyetracking_plot_heatmap.py create mode 100644 mne/viz/eyetracking/__init__.py create mode 100644 mne/viz/eyetracking/heatmap.py create mode 100644 mne/viz/eyetracking/tests/__init__.py create mode 100644 mne/viz/eyetracking/tests/test_heatmap.py diff --git a/doc/api/visualization.rst b/doc/api/visualization.rst index 03a2d22a529..359f1373129 100644 --- a/doc/api/visualization.rst +++ b/doc/api/visualization.rst @@ -88,6 +88,21 @@ Visualization get_browser_backend use_browser_backend +Eyetracking +----------- + +.. currentmodule:: mne.viz.eyetracking + +:py:mod:`mne.viz.eyetracking`: + +.. automodule:: mne.viz.eyetracking + :no-members: + :no-inherited-members: +.. autosummary:: + :toctree: generated/ + + plot_gaze + UI Events --------- diff --git a/doc/documentation/datasets.rst b/doc/documentation/datasets.rst index 3946ef64be5..348da772e90 100644 --- a/doc/documentation/datasets.rst +++ b/doc/documentation/datasets.rst @@ -481,16 +481,34 @@ EYELINK ======= :func:`mne.datasets.eyelink.data_path` -A small example dataset from a pupillary light reflex experiment. Both EEG (EGI) and -eye-tracking (SR Research EyeLink; ASCII format) data were recorded and stored in -separate files. 1 participant fixated on the screen while short light flashes appeared. -Event onsets were recorded by a photodiode attached to the screen and were -sent to both the EEG and eye-tracking systems. +Two small example datasets of eye-tracking data from SR Research EyeLink. + +EEG-Eyetracking +^^^^^^^^^^^^^^^ +:func:`mne.datasets.eyelink.data_path`. Data exists at ``/eeg-et/``. + +Contains both EEG (EGI) and eye-tracking (ASCII format) data recorded from a +pupillary light reflex experiment, stored in separate files. 1 participant fixated +on the screen while short light flashes appeared. Event onsets were recorded by a +photodiode attached to the screen and were sent to both the EEG and eye-tracking +systems. .. topic:: Examples * :ref:`tut-eyetrack` +Freeviewing +^^^^^^^^^^^ +:func:`mne.datasets.eyelink.data_path`. Data exists at ``/freeviewing/``. + +Contains eye-tracking data (ASCII format) from 1 participant who was free-viewing a +video of a natural scene. In some videos, the natural scene was pixelated such that +the people in the scene were unrecognizable. + +.. topic:: Examples + + * :ref:`tut-eyetrack-heatmap` + References ========== diff --git a/examples/visualization/eyetracking_plot_heatmap.py b/examples/visualization/eyetracking_plot_heatmap.py new file mode 100644 index 00000000000..00c9fee6611 --- /dev/null +++ b/examples/visualization/eyetracking_plot_heatmap.py @@ -0,0 +1,89 @@ +# -*- coding: utf-8 -*- +""" +.. _tut-eyetrack-heatmap: + +============================================= +Plotting eye-tracking heatmaps in MNE-Python +============================================= + +This tutorial covers plotting eye-tracking position data as a heatmap. + +.. seealso:: + + :ref:`tut-importing-eyetracking-data` + :ref:`tut-eyetrack` + +""" + +# %% +# Data loading +# ------------ +# +# As usual we start by importing the modules we need and loading some +# :ref:`example data `: eye-tracking data recorded from SR research's +# ``'.asc'`` file format. + + +import matplotlib.pyplot as plt + +import mne +from mne.viz.eyetracking import plot_gaze + +task_fpath = mne.datasets.eyelink.data_path() / "freeviewing" +et_fpath = task_fpath / "sub-01_task-freeview_eyetrack.asc" +stim_fpath = task_fpath / "stim" / "naturalistic.png" + +raw = mne.io.read_raw_eyelink(et_fpath) + +# %% +# Process and epoch the data +# -------------------------- +# +# First we will interpolate missing data during blinks and epoch the data. + +mne.preprocessing.eyetracking.interpolate_blinks(raw, interpolate_gaze=True) +raw.annotations.rename({"dvns": "natural"}) # more intuitive +event_ids = {"natural": 1} +events, event_dict = mne.events_from_annotations(raw, event_id=event_ids) + +epochs = mne.Epochs( + raw, events=events, event_id=event_dict, tmin=0, tmax=20, baseline=None +) + + +# %% +# Plot a heatmap of the eye-tracking data +# --------------------------------------- +# +# To make a heatmap of the eye-tracking data, we can use the function +# :func:`~mne.viz.eyetracking.plot_gaze`. We will need to define the dimensions of our +# canvas; for this file, the eye position data are reported in pixels, so we'll use the +# screen resolution of the participant screen (1920x1080) as the width and height. We +# can also use the sigma parameter to smooth the plot. + +px_width, px_height = 1920, 1080 +cmap = plt.get_cmap("viridis") +plot_gaze(epochs["natural"], width=px_width, height=px_height, cmap=cmap, sigma=50) + +# %% +# Overlaying plots with images +# ---------------------------- +# +# We can use matplotlib to plot gaze heatmaps on top of stimuli images. We'll +# customize a :class:`~matplotlib.colors.Colormap` to make some values of the heatmap +# completely transparent. We'll then use the ``vlim`` parameter to force the heatmap to +# start at a value greater than the darkest value in our previous heatmap, which will +# make the darkest colors of the heatmap transparent. + +cmap.set_under("k", alpha=0) # make the lowest values transparent +ax = plt.subplot() +ax.imshow(plt.imread(stim_fpath)) +plot_gaze( + epochs["natural"], + width=px_width, + height=px_height, + vlim=(0.0003, None), + sigma=50, + cmap=cmap, + axes=ax, +) diff --git a/mne/datasets/config.py b/mne/datasets/config.py index 76ea0934e39..2cf4d50a0ff 100644 --- a/mne/datasets/config.py +++ b/mne/datasets/config.py @@ -345,9 +345,9 @@ # eyelink dataset MNE_DATASETS["eyelink"] = dict( - archive_name="eeg-eyetrack_data.zip", - hash="md5:c4fc788fe01737e08e9086c90cab642d", - url=("https://osf.io/63fjm/download?version=1"), - folder_name="eyelink-example-data", + archive_name="MNE-eyelink-data.zip", + hash="md5:68a6323ef17d655f1a659c3290ee1c3f", + url=("https://osf.io/xsu4g/download?version=1"), + folder_name="MNE-eyelink-data", config_key="MNE_DATASETS_EYELINK_PATH", ) diff --git a/mne/utils/docs.py b/mne/utils/docs.py index 9e72685e738..40308a9074d 100644 --- a/mne/utils/docs.py +++ b/mne/utils/docs.py @@ -743,6 +743,14 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): ``pos_lims``, as the surface plot must show the magnitude. """ +docdict[ + "cmap" +] = """ +cmap : matplotlib colormap | str | None + The :class:`~matplotlib.colors.Colormap` to use. Defaults to ``None``, which + will use the matplotlib default colormap. +""" + docdict[ "cmap_topomap" ] = """ diff --git a/mne/viz/eyetracking/__init__.py b/mne/viz/eyetracking/__init__.py new file mode 100644 index 00000000000..7de13fd8900 --- /dev/null +++ b/mne/viz/eyetracking/__init__.py @@ -0,0 +1,5 @@ +"""Eye-tracking visualization routines.""" +# +# License: BSD-3-Clause + +from .heatmap import plot_gaze diff --git a/mne/viz/eyetracking/heatmap.py b/mne/viz/eyetracking/heatmap.py new file mode 100644 index 00000000000..d3ff4756d8d --- /dev/null +++ b/mne/viz/eyetracking/heatmap.py @@ -0,0 +1,155 @@ +# Authors: Scott Huberty +# +# License: BSD-3-Clause + +import numpy as np +from scipy.ndimage import gaussian_filter + +from ...utils import _ensure_int, _validate_type, fill_doc, logger +from ..utils import plt_show + + +@fill_doc +def plot_gaze( + epochs, + width, + height, + *, + sigma=25, + cmap=None, + alpha=1.0, + vlim=(None, None), + axes=None, + show=True, +): + """Plot a heatmap of eyetracking gaze data. + + Parameters + ---------- + epochs : instance of Epochs + The :class:`~mne.Epochs` object containing eyegaze channels. + width : int + The width dimension of the plot canvas. For example, if the eyegaze data units + are pixels, and the participant screen resolution was 1920x1080, then the width + should be 1920. + height : int + The height dimension of the plot canvas. For example, if the eyegaze data units + are pixels, and the participant screen resolution was 1920x1080, then the height + should be 1080. + sigma : float | None + The amount of Gaussian smoothing applied to the heatmap data (standard + deviation in pixels). If ``None``, no smoothing is applied. Default is 25. + %(cmap)s + alpha : float + The opacity of the heatmap (default is 1). + %(vlim_plot_topomap)s + %(axes_plot_topomap)s + %(show)s + + Returns + ------- + fig : instance of Figure + The resulting figure object for the heatmap plot. + + Notes + ----- + .. versionadded:: 1.6 + """ + from mne import BaseEpochs + from mne._fiff.pick import _picks_to_idx + + _validate_type(epochs, BaseEpochs, "epochs") + _validate_type(alpha, "numeric", "alpha") + _validate_type(sigma, ("numeric", None), "sigma") + width = _ensure_int(width, "width") + height = _ensure_int(height, "height") + + pos_picks = _picks_to_idx(epochs.info, "eyegaze") + gaze_data = epochs.get_data(picks=pos_picks) + gaze_ch_loc = np.array([epochs.info["chs"][idx]["loc"] for idx in pos_picks]) + x_data = gaze_data[:, np.where(gaze_ch_loc[:, 4] == -1)[0], :] + y_data = gaze_data[:, np.where(gaze_ch_loc[:, 4] == 1)[0], :] + + if x_data.shape[1] > 1: # binocular recording. Average across eyes + logger.info("Detected binocular recording. Averaging positions across eyes.") + x_data = np.nanmean(x_data, axis=1) # shape (n_epochs, n_samples) + y_data = np.nanmean(y_data, axis=1) + canvas = np.vstack((x_data.flatten(), y_data.flatten())) # shape (2, n_samples) + + # Create 2D histogram + # Bin into image-like format + hist, _, _ = np.histogram2d( + canvas[1, :], + canvas[0, :], + bins=(height, width), + range=[[0, height], [0, width]], + ) + # Convert density from samples to seconds + hist /= epochs.info["sfreq"] + # Smooth the heatmap + if sigma: + hist = gaussian_filter(hist, sigma=sigma) + + return _plot_heatmap_array( + hist, + width=width, + height=height, + cmap=cmap, + alpha=alpha, + vmin=vlim[0], + vmax=vlim[1], + axes=axes, + show=show, + ) + + +def _plot_heatmap_array( + data, + width, + height, + cmap=None, + alpha=None, + vmin=None, + vmax=None, + axes=None, + show=True, +): + """Plot a heatmap of eyetracking gaze data from a numpy array.""" + import matplotlib.pyplot as plt + + # Prepare axes + if axes is not None: + from matplotlib.axes import Axes + + _validate_type(axes, Axes, "axes") + ax = axes + fig = ax.get_figure() + else: + fig, ax = plt.subplots(constrained_layout=True) + + ax.set_title("Gaze heatmap") + ax.set_xlabel("X position") + ax.set_ylabel("Y position") + + # Prepare the heatmap + alphas = 1 if alpha is None else alpha + vmin = np.nanmin(data) if vmin is None else vmin + vmax = np.nanmax(data) if vmax is None else vmax + extent = [0, width, height, 0] # origin is the top left of the screen + + # Plot heatmap + im = ax.imshow( + data, + aspect="equal", + cmap=cmap, + alpha=alphas, + extent=extent, + origin="upper", + vmin=vmin, + vmax=vmax, + ) + + # Prepare the colorbar + fig.colorbar(im, ax=ax, shrink=0.6, label="Dwell time (seconds)") + plt_show(show) + return fig diff --git a/mne/viz/eyetracking/tests/__init__.py b/mne/viz/eyetracking/tests/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/mne/viz/eyetracking/tests/test_heatmap.py b/mne/viz/eyetracking/tests/test_heatmap.py new file mode 100644 index 00000000000..99103c552b1 --- /dev/null +++ b/mne/viz/eyetracking/tests/test_heatmap.py @@ -0,0 +1,35 @@ +# Authors: Scott Huberty +# +# License: Simplified BSD + +import matplotlib.pyplot as plt +import numpy as np +import pytest + +import mne + + +@pytest.mark.parametrize("axes", [None, True]) +def test_plot_heatmap(axes): + """Test plot_gaze.""" + # Create a toy epochs instance + info = info = mne.create_info( + ch_names=["xpos", "ypos"], sfreq=100, ch_types="eyegaze" + ) + # simulate a steady fixation at the center of the screen + width, height = (1920, 1080) + shape = (1, 100) # x or y, time + data = np.vstack([np.full(shape, width / 2), np.full(shape, height / 2)]) + epochs = mne.EpochsArray(data[None, ...], info) + epochs.info["chs"][0]["loc"][4] = -1 + epochs.info["chs"][1]["loc"][4] = 1 + + if axes: + axes = plt.subplot() + fig = mne.viz.eyetracking.plot_gaze( + epochs, width=width, height=height, axes=axes, cmap="Greys", sigma=None + ) + img = fig.axes[0].images[0].get_array() + # We simulated a 2D histogram where only the central pixel (960, 540) was active + assert img.T[width // 2, height // 2] == 1 # central pixel is active + assert np.sum(img) == 1 # only the central pixel should be active diff --git a/tutorials/preprocessing/90_eyetracking_data.py b/tutorials/preprocessing/90_eyetracking_data.py index 2e71f3d00b4..a788988bae0 100644 --- a/tutorials/preprocessing/90_eyetracking_data.py +++ b/tutorials/preprocessing/90_eyetracking_data.py @@ -31,9 +31,10 @@ import mne from mne.datasets.eyelink import data_path from mne.preprocessing.eyetracking import read_eyelink_calibration +from mne.viz.eyetracking import plot_gaze -et_fpath = data_path() / "sub-01_task-plr_eyetrack.asc" -eeg_fpath = data_path() / "sub-01_task-plr_eeg.mff" +et_fpath = data_path() / "eeg-et" / "sub-01_task-plr_eyetrack.asc" +eeg_fpath = data_path() / "eeg-et" / "sub-01_task-plr_eeg.mff" raw_et = mne.io.read_raw_eyelink(et_fpath, create_annotations=["blinks"]) raw_eeg = mne.io.read_raw_egi(eeg_fpath, preload=True, verbose="warning") @@ -123,7 +124,9 @@ # window 50 ms before and 200 ms after the blink, so that the noisy data surrounding # the blink is also interpolated. -mne.preprocessing.eyetracking.interpolate_blinks(raw_et, buffer=(0.05, 0.2)) +mne.preprocessing.eyetracking.interpolate_blinks( + raw_et, buffer=(0.05, 0.2), interpolate_gaze=True +) # %% # .. important:: By default, :func:`~mne.preprocessing.eyetracking.interpolate_blinks`, @@ -176,6 +179,7 @@ ) # Add EEG channels to the eye-tracking raw object raw_et.add_channels([raw_eeg], force_update_info=True) +del raw_eeg # free up some memory # Define a few channel groups of interest and plot the data frontal = ["E19", "E11", "E4", "E12", "E5"] @@ -194,11 +198,25 @@ # Now let's extract epochs around our flash events. We should see a clear pupil # constriction response to the flashes. -epochs = mne.Epochs(raw_et, events=et_events, event_id=event_dict, tmin=-0.3, tmax=3) +# Skip baseline correction for now. We will apply baseline correction later. +epochs = mne.Epochs( + raw_et, events=et_events, event_id=event_dict, tmin=-0.3, tmax=3, baseline=None +) +del raw_et # free up some memory epochs[:8].plot(events=et_events, event_id=event_dict, order=picks_idx) +# %% +# For this experiment, the participant was instructed to fixate on a crosshair in the +# center of the screen. Let's plot the gaze position data to confirm that the +# participant primarily kept their gaze fixated at the center of the screen. + +plot_gaze(epochs, width=1920, height=1080) + +# %% +# .. seealso:: :ref:`tut-eyetrack-heatmap` + # %% # Finally, let's plot the evoked responses to the light flashes to get a sense of the # average pupillary light response, and the associated ERP in the EEG data. -epochs.average().plot(picks=occipital + pupil) +epochs.apply_baseline().average().plot(picks=occipital + pupil) From 875df8285e25ca4964566f2b900b808be65845d7 Mon Sep 17 00:00:00 2001 From: Daniel McCloy Date: Fri, 13 Oct 2023 19:39:18 -0500 Subject: [PATCH 011/405] restore original location of API stubs (#12110) Co-authored-by: Mathieu Scheltienne Co-authored-by: Scott Huberty <52462026+scott-huberty@users.noreply.github.com> --- doc/api/covariance.rst | 2 +- doc/api/creating_from_arrays.rst | 2 +- doc/api/datasets.rst | 2 +- doc/api/decoding.rst | 4 ++-- doc/api/events.rst | 6 +++--- doc/api/export.rst | 2 +- doc/api/file_io.rst | 4 ++-- doc/api/forward.rst | 6 +++--- doc/api/inverse.rst | 10 +++++----- doc/api/logging.rst | 6 +++--- doc/api/most_used_classes.rst | 2 +- doc/api/mri.rst | 2 +- doc/api/preprocessing.rst | 22 +++++++++++----------- doc/api/reading_raw_data.rst | 6 +++--- doc/api/report.rst | 2 +- doc/api/sensor_space.rst | 4 ++-- doc/api/simulation.rst | 4 ++-- doc/api/source_space.rst | 2 +- doc/api/statistics.rst | 8 ++++---- doc/api/time_frequency.rst | 8 ++++---- doc/api/visualization.rst | 6 +++--- 21 files changed, 55 insertions(+), 55 deletions(-) diff --git a/doc/api/covariance.rst b/doc/api/covariance.rst index 1de751f21a8..b5449186b27 100644 --- a/doc/api/covariance.rst +++ b/doc/api/covariance.rst @@ -5,7 +5,7 @@ Covariance computation .. currentmodule:: mne .. autosummary:: - :toctree: generated/ + :toctree: ../generated/ Covariance compute_covariance diff --git a/doc/api/creating_from_arrays.rst b/doc/api/creating_from_arrays.rst index f580cadfc78..f5771f15ef8 100644 --- a/doc/api/creating_from_arrays.rst +++ b/doc/api/creating_from_arrays.rst @@ -5,7 +5,7 @@ Creating data objects from arrays .. currentmodule:: mne .. autosummary:: - :toctree: generated/ + :toctree: ../generated/ EvokedArray EpochsArray diff --git a/doc/api/datasets.rst b/doc/api/datasets.rst index c3d94c49006..b46e3e387a9 100644 --- a/doc/api/datasets.rst +++ b/doc/api/datasets.rst @@ -11,7 +11,7 @@ Datasets :no-inherited-members: .. autosummary:: - :toctree: generated/ + :toctree: ../generated/ fetch_dataset has_dataset diff --git a/doc/api/decoding.rst b/doc/api/decoding.rst index e539629c8c0..c844afc470a 100644 --- a/doc/api/decoding.rst +++ b/doc/api/decoding.rst @@ -11,7 +11,7 @@ Decoding :no-inherited-members: .. autosummary:: - :toctree: generated/ + :toctree: ../generated/ CSP EMS @@ -33,7 +33,7 @@ Decoding Functions that assist with decoding and model fitting: .. autosummary:: - :toctree: generated/ + :toctree: ../generated/ compute_ems cross_val_multiscore diff --git a/doc/api/events.rst b/doc/api/events.rst index 4e227ed60f1..f9447741a09 100644 --- a/doc/api/events.rst +++ b/doc/api/events.rst @@ -5,7 +5,7 @@ Events .. currentmodule:: mne .. autosummary:: - :toctree: generated/ + :toctree: ../generated/ Annotations AcqParserFIF @@ -35,7 +35,7 @@ Events .. currentmodule:: mne.event .. autosummary:: - :toctree: generated/ + :toctree: ../generated/ define_target_events match_event_names @@ -50,7 +50,7 @@ Events .. currentmodule:: mne.epochs .. autosummary:: - :toctree: generated/ + :toctree: ../generated/ average_movements combine_event_ids diff --git a/doc/api/export.rst b/doc/api/export.rst index 32f58f230bf..7c3bfc3f868 100644 --- a/doc/api/export.rst +++ b/doc/api/export.rst @@ -11,7 +11,7 @@ Exporting .. currentmodule:: mne.export .. autosummary:: - :toctree: generated/ + :toctree: ../generated/ export_epochs export_evokeds diff --git a/doc/api/file_io.rst b/doc/api/file_io.rst index fdb2022d134..3b43de6ce64 100644 --- a/doc/api/file_io.rst +++ b/doc/api/file_io.rst @@ -4,7 +4,7 @@ File I/O .. currentmodule:: mne .. autosummary:: - :toctree: generated + :toctree: ../generated/ channel_type channel_indices_by_type @@ -60,7 +60,7 @@ File I/O Base class: .. autosummary:: - :toctree: generated + :toctree: ../generated/ :template: autosummary/class_no_members.rst BaseEpochs \ No newline at end of file diff --git a/doc/api/forward.rst b/doc/api/forward.rst index 7e554195aa6..5abcd5178fc 100644 --- a/doc/api/forward.rst +++ b/doc/api/forward.rst @@ -5,14 +5,14 @@ Forward Modeling .. currentmodule:: mne .. autosummary:: - :toctree: generated/ + :toctree: ../generated/ :template: autosummary/class_no_inherited_members.rst Forward SourceSpaces .. autosummary:: - :toctree: generated/ + :toctree: ../generated/ add_source_space_distances apply_forward @@ -55,7 +55,7 @@ Forward Modeling .. currentmodule:: mne.bem .. autosummary:: - :toctree: generated/ + :toctree: ../generated/ ConductorModel fit_sphere_to_headshape diff --git a/doc/api/inverse.rst b/doc/api/inverse.rst index 3be9d088925..754244c17fe 100644 --- a/doc/api/inverse.rst +++ b/doc/api/inverse.rst @@ -11,7 +11,7 @@ Inverse Solutions .. currentmodule:: mne.minimum_norm .. autosummary:: - :toctree: generated/ + :toctree: ../generated/ InverseOperator apply_inverse @@ -43,7 +43,7 @@ Inverse Solutions .. currentmodule:: mne.inverse_sparse .. autosummary:: - :toctree: generated/ + :toctree: ../generated/ mixed_norm tf_mixed_norm @@ -59,7 +59,7 @@ Inverse Solutions .. currentmodule:: mne.beamformer .. autosummary:: - :toctree: generated/ + :toctree: ../generated/ Beamformer read_beamformer @@ -80,7 +80,7 @@ Inverse Solutions .. currentmodule:: mne .. autosummary:: - :toctree: generated/ + :toctree: ../generated/ Dipole DipoleFixed @@ -95,6 +95,6 @@ Inverse Solutions .. currentmodule:: mne.dipole .. autosummary:: - :toctree: generated/ + :toctree: ../generated/ get_phantom_dipoles diff --git a/doc/api/logging.rst b/doc/api/logging.rst index 453f9544994..64cc21759a7 100644 --- a/doc/api/logging.rst +++ b/doc/api/logging.rst @@ -5,7 +5,7 @@ Logging and Configuration .. currentmodule:: mne .. autosummary:: - :toctree: generated/ + :toctree: ../generated/ get_config_path get_config @@ -28,7 +28,7 @@ Logging and Configuration :no-inherited-members: .. autosummary:: - :toctree: generated/ + :toctree: ../generated/ deprecated warn @@ -42,7 +42,7 @@ Logging and Configuration :no-inherited-members: .. autosummary:: - :toctree: generated/ + :toctree: ../generated/ get_cuda_memory init_cuda diff --git a/doc/api/most_used_classes.rst b/doc/api/most_used_classes.rst index 2e4bc1d7dd0..705b4e845b3 100644 --- a/doc/api/most_used_classes.rst +++ b/doc/api/most_used_classes.rst @@ -4,7 +4,7 @@ Most-used classes .. currentmodule:: mne .. autosummary:: - :toctree: generated/ + :toctree: ../generated/ io.Raw Epochs diff --git a/doc/api/mri.rst b/doc/api/mri.rst index 2b3a82a8b28..6cd38cbbeb4 100644 --- a/doc/api/mri.rst +++ b/doc/api/mri.rst @@ -16,7 +16,7 @@ See also: - :func:`mne-gui-addons:mne_gui_addons.locate_ieeg`. .. autosummary:: - :toctree: generated/ + :toctree: ../generated/ coreg.get_mni_fiducials coreg.estimate_head_mri_t diff --git a/doc/api/preprocessing.rst b/doc/api/preprocessing.rst index cbfda1ac49b..54d4bfa2999 100644 --- a/doc/api/preprocessing.rst +++ b/doc/api/preprocessing.rst @@ -7,13 +7,13 @@ Projections: .. currentmodule:: mne .. autosummary:: - :toctree: generated/ + :toctree: ../generated/ :template: autosummary/class_no_inherited_members.rst Projection .. autosummary:: - :toctree: generated/ + :toctree: ../generated/ compute_proj_epochs compute_proj_evoked @@ -30,7 +30,7 @@ Projections: :no-inherited-members: .. autosummary:: - :toctree: generated/ + :toctree: ../generated/ Layout DigMontage @@ -72,7 +72,7 @@ Projections: :no-inherited-members: .. autosummary:: - :toctree: generated/ + :toctree: ../generated/ ICA Xdawn @@ -124,7 +124,7 @@ Projections: :no-inherited-members: .. autosummary:: - :toctree: generated/ + :toctree: ../generated/ optical_density beer_lambert_law @@ -142,7 +142,7 @@ Projections: :no-inherited-members: .. autosummary:: - :toctree: generated/ + :toctree: ../generated/ project_sensors_onto_brain make_montage_volume @@ -157,7 +157,7 @@ Projections: :no-inherited-members: .. autosummary:: - :toctree: generated/ + :toctree: ../generated/ Calibration read_eyelink_calibration @@ -169,7 +169,7 @@ EEG referencing: .. currentmodule:: mne .. autosummary:: - :toctree: generated/ + :toctree: ../generated/ add_reference_channels set_bipolar_reference @@ -184,7 +184,7 @@ EEG referencing: :no-inherited-members: .. autosummary:: - :toctree: generated/ + :toctree: ../generated/ construct_iir_filter create_filter @@ -202,7 +202,7 @@ EEG referencing: :no-inherited-members: .. autosummary:: - :toctree: generated/ + :toctree: ../generated/ compute_chpi_amplitudes compute_chpi_snr @@ -226,7 +226,7 @@ EEG referencing: :no-inherited-members: .. autosummary:: - :toctree: generated/ + :toctree: ../generated/ Transform quat_to_rot diff --git a/doc/api/reading_raw_data.rst b/doc/api/reading_raw_data.rst index 0a4d7cef5e6..1b8ebae2abf 100644 --- a/doc/api/reading_raw_data.rst +++ b/doc/api/reading_raw_data.rst @@ -10,7 +10,7 @@ Reading raw data :no-inherited-members: .. autosummary:: - :toctree: generated/ + :toctree: ../generated/ anonymize_info read_raw @@ -44,7 +44,7 @@ Reading raw data Base class: .. autosummary:: - :toctree: generated + :toctree: ../generated/ :template: autosummary/class_no_members.rst BaseRaw @@ -58,6 +58,6 @@ Base class: :no-inherited-members: .. autosummary:: - :toctree: generated/ + :toctree: ../generated/ read_mrk diff --git a/doc/api/report.rst b/doc/api/report.rst index 5104f68adc6..eab37eae542 100644 --- a/doc/api/report.rst +++ b/doc/api/report.rst @@ -7,7 +7,7 @@ MNE-Report .. currentmodule:: mne .. autosummary:: - :toctree: generated/ + :toctree: ../generated/ Report open_report diff --git a/doc/api/sensor_space.rst b/doc/api/sensor_space.rst index b4bbda60053..8121f63f3d5 100644 --- a/doc/api/sensor_space.rst +++ b/doc/api/sensor_space.rst @@ -5,7 +5,7 @@ Sensor Space Data .. currentmodule:: mne .. autosummary:: - :toctree: generated/ + :toctree: ../generated/ combine_evoked concatenate_raws @@ -33,6 +33,6 @@ Sensor Space Data .. currentmodule:: mne.baseline .. autosummary:: - :toctree: generated/ + :toctree: ../generated/ rescale diff --git a/doc/api/simulation.rst b/doc/api/simulation.rst index e055729ae48..3416e05ed72 100644 --- a/doc/api/simulation.rst +++ b/doc/api/simulation.rst @@ -11,7 +11,7 @@ Simulation .. currentmodule:: mne.simulation .. autosummary:: - :toctree: generated/ + :toctree: ../generated/ add_chpi add_ecg @@ -33,7 +33,7 @@ Simulation .. currentmodule:: mne.simulation.metrics .. autosummary:: - :toctree: generated/ + :toctree: ../generated/ cosine_score region_localization_error diff --git a/doc/api/source_space.rst b/doc/api/source_space.rst index ef6c8861bc2..cb4aee084f2 100644 --- a/doc/api/source_space.rst +++ b/doc/api/source_space.rst @@ -5,7 +5,7 @@ Source Space Data .. currentmodule:: mne .. autosummary:: - :toctree: generated/ + :toctree: ../generated/ BiHemiLabel Label diff --git a/doc/api/statistics.rst b/doc/api/statistics.rst index 2b8313b1f11..cf50b459545 100644 --- a/doc/api/statistics.rst +++ b/doc/api/statistics.rst @@ -16,7 +16,7 @@ Parametric statistics (see :mod:`scipy.stats` and :mod:`statsmodels` for more options): .. autosummary:: - :toctree: generated/ + :toctree: ../generated/ ttest_1samp_no_p ttest_ind_no_p @@ -29,7 +29,7 @@ options): Mass-univariate multiple comparison correction: .. autosummary:: - :toctree: generated/ + :toctree: ../generated/ bonferroni_correction fdr_correction @@ -37,7 +37,7 @@ Mass-univariate multiple comparison correction: Non-parametric (clustering) resampling methods: .. autosummary:: - :toctree: generated/ + :toctree: ../generated/ combine_adjacency permutation_cluster_test @@ -53,7 +53,7 @@ Compute ``adjacency`` matrices for cluster-level statistics: .. currentmodule:: mne .. autosummary:: - :toctree: generated/ + :toctree: ../generated/ channels.find_ch_adjacency channels.read_ch_adjacency diff --git a/doc/api/time_frequency.rst b/doc/api/time_frequency.rst index 63601691414..f8948909491 100644 --- a/doc/api/time_frequency.rst +++ b/doc/api/time_frequency.rst @@ -11,7 +11,7 @@ Time-Frequency .. currentmodule:: mne.time_frequency .. autosummary:: - :toctree: generated/ + :toctree: ../generated/ AverageTFR EpochsTFR @@ -24,7 +24,7 @@ Time-Frequency Functions that operate on mne-python objects: .. autosummary:: - :toctree: generated/ + :toctree: ../generated/ csd_tfr csd_fourier @@ -43,7 +43,7 @@ Functions that operate on mne-python objects: Functions that operate on ``np.ndarray`` objects: .. autosummary:: - :toctree: generated/ + :toctree: ../generated/ csd_array_fourier csd_array_multitaper @@ -70,7 +70,7 @@ Functions that operate on ``np.ndarray`` objects: .. currentmodule:: mne.time_frequency.tfr .. autosummary:: - :toctree: generated/ + :toctree: ../generated/ cwt morlet diff --git a/doc/api/visualization.rst b/doc/api/visualization.rst index 359f1373129..280ed51f590 100644 --- a/doc/api/visualization.rst +++ b/doc/api/visualization.rst @@ -11,7 +11,7 @@ Visualization :no-inherited-members: .. autosummary:: - :toctree: generated/ + :toctree: ../generated/ Brain ClickableImage @@ -99,7 +99,7 @@ Eyetracking :no-members: :no-inherited-members: .. autosummary:: - :toctree: generated/ + :toctree: ../generated/ plot_gaze @@ -115,7 +115,7 @@ UI Events :no-inherited-members: .. autosummary:: - :toctree: generated/ + :toctree: ../generated/ subscribe unsubscribe From 4beb8dde7588c3153ee0a240b5e363dc987c95f1 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Wed, 18 Oct 2023 05:12:58 +0300 Subject: [PATCH 012/405] MAINT: Ignore SciPy using deprecated API (#12115) --- mne/conftest.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/mne/conftest.py b/mne/conftest.py index 908e7f53dba..e4174e7deb9 100644 --- a/mne/conftest.py +++ b/mne/conftest.py @@ -172,6 +172,13 @@ def pytest_configure(config): ignore:(\n|.)*numpy\.distutils` is deprecated since NumPy(\n|.)*:DeprecationWarning ignore:datetime\.utcfromtimestamp.*is deprecated:DeprecationWarning ignore:The numpy\.array_api submodule is still experimental.*:UserWarning + # numpy 2.0 <-> SciPy + ignore:numpy\.core\._multiarray_umath.*:DeprecationWarning + ignore:numpy\.core\.numeric is deprecated.*:DeprecationWarning + ignore:numpy\.core\.multiarray is deprecated.*:DeprecationWarning + # TODO: Should actually fix these two + ignore:scipy.signal.morlet2 is deprecated in SciPy.*:DeprecationWarning + ignore:The `needs_threshold` and `needs_proba`.*:FutureWarning # tqdm (Fedora) ignore:.*'tqdm_asyncio' object has no attribute 'last_print_t':pytest.PytestUnraisableExceptionWarning # Until mne-qt-browser > 0.5.2 is released From b45837b5e1e4606e25701b85e525caf2f87dd523 Mon Sep 17 00:00:00 2001 From: Mathieu Scheltienne Date: Thu, 19 Oct 2023 02:24:53 +0200 Subject: [PATCH 013/405] Better error message when a bad method type is provided (#12113) Co-authored-by: Daniel McCloy --- doc/changes/devel.rst | 1 + mne/channels/channels.py | 104 ++++++++++++----------- mne/channels/tests/test_interpolation.py | 32 +++++-- 3 files changed, 79 insertions(+), 58 deletions(-) diff --git a/doc/changes/devel.rst b/doc/changes/devel.rst index 6474a846f35..a30901a5efd 100644 --- a/doc/changes/devel.rst +++ b/doc/changes/devel.rst @@ -72,6 +72,7 @@ Bugs - Add support for non-ASCII characters in Annotations, Evoked comments, etc when saving to FIFF format (:gh:`12080` by `Daniel McCloy`_) - Correctly handle passing ``"eyegaze"`` or ``"pupil"`` to :meth:`mne.io.Raw.pick` (:gh:`12019` by `Scott Huberty`_) - Fix bug with :func:`~mne.viz.plot_raw` where changing ``MNE_BROWSER_BACKEND`` via :func:`~mne.set_config` would have no effect within a Python session (:gh:`12078` by `Santeri Ruuskanen`_) +- Improve handling of ``method`` argument in the channel interpolation function to support :class:`str` and raise helpful error messages (:gh:`12113` by `Mathieu Scheltienne`_) API changes ~~~~~~~~~~~ diff --git a/mne/channels/channels.py b/mne/channels/channels.py index 2fb73fa5654..6e975e1cade 100644 --- a/mne/channels/channels.py +++ b/mne/channels/channels.py @@ -41,6 +41,7 @@ _pick_data_channels, _picks_by_type, _picks_to_idx, + _second_rules, channel_indices_by_type, channel_type, pick_channels, @@ -828,24 +829,25 @@ def interpolate_bads( origin fit. .. versionadded:: 0.17 - method : dict | None + method : dict | str | None Method to use for each channel type. - All channel types support "nan". - The key ``"eeg"`` has two additional options: - - ``"spline"`` (default) - Use spherical spline interpolation. - - ``"MNE"`` - Use minimum-norm projection to a sphere and back. - This is the method used for MEG channels. + - ``"meg"`` channels support ``"MNE"`` (default) and ``"nan"`` + - ``"eeg"`` channels support ``"spline"`` (default), ``"MNE"`` and ``"nan"`` + - ``"fnirs"`` channels support ``"nearest"`` (default) and ``"nan"`` - The default value for ``"meg"`` is ``"MNE"``, and the default value - for ``"fnirs"`` is ``"nearest"``. - - The default (None) is thus an alias for:: + None is an alias for:: method=dict(meg="MNE", eeg="spline", fnirs="nearest") + If a :class:`str` is provided, the method will be applied to all channel + types supported and available in the instance. The method ``"nan"`` will + replace the channel data with ``np.nan``. + + .. warning:: + Be careful when using ``method="nan"``; the default value + ``reset_bads=True`` may not be what you want. + .. versionadded:: 0.21 exclude : list | tuple The channels to exclude from interpolation. If excluded a bad @@ -859,11 +861,9 @@ def interpolate_bads( Notes ----- - .. versionadded:: 0.9.0 + The ``"MNE"`` method uses minimum-norm projection to a sphere and back. - .. warning:: - Be careful when using ``method="nan"``; the default value - ``reset_bads=True`` may not be what you want. + .. versionadded:: 0.9.0 """ from .interpolation import ( _interpolate_bads_eeg, @@ -872,49 +872,53 @@ def interpolate_bads( ) _check_preload(self, "interpolation") + _validate_type(method, (dict, str, None), "method") method = _handle_default("interpolation_method", method) + ch_types = self.get_channel_types(unique=True) + # figure out if we have "mag" for "meg", "hbo" for "fnirs", ... to filter the + # "method" dictionary and keep only keys that correspond to existing channels. + for ch_type in ("meg", "fnirs"): + for sub_ch_type in _second_rules[ch_type][1].values(): + if sub_ch_type in ch_types: + ch_types.remove(sub_ch_type) + if ch_type not in ch_types: + ch_types.append(ch_type) + keys2delete = set(method) - set(ch_types) + for key in keys2delete: + del method[key] + valids = { + "eeg": ("spline", "MNE", "nan"), + "meg": ("MNE", "nan"), + "fnirs": ("nearest", "nan"), + } for key in method: _check_option("method[key]", key, ("meg", "eeg", "fnirs")) - _check_option( - "method['eeg']", - method["eeg"], - ( - "spline", - "MNE", - "nan", - ), - ) - _check_option( - "method['meg']", - method["meg"], - ( - "MNE", - "nan", - ), - ) - _check_option( - "method['fnirs']", - method["fnirs"], - ( - "nearest", - "nan", - ), - ) - - if len(self.info["bads"]) == 0: + _check_option(f"method['{key}']", method[key], valids[key]) + logger.info("Setting channel interpolation method to %s.", method) + idx = _picks_to_idx(self.info, list(method), exclude=(), allow_empty=True) + if idx.size == 0 or len(pick_info(self.info, idx)["bads"]) == 0: warn("No bad channels to interpolate. Doing nothing...") return self - logger.info("Interpolating bad channels") + logger.info("Interpolating bad channels.") origin = _check_origin(origin, self.info) - if method["eeg"] == "spline": + if method.get("eeg", "") == "spline": _interpolate_bads_eeg(self, origin=origin, exclude=exclude) eeg_mne = False + elif "eeg" not in method: + eeg_mne = False else: eeg_mne = True - _interpolate_bads_meeg( - self, mode=mode, origin=origin, eeg=eeg_mne, exclude=exclude, method=method - ) - _interpolate_bads_nirs(self, exclude=exclude, method=method["fnirs"]) + if "meg" in method or eeg_mne: + _interpolate_bads_meeg( + self, + mode=mode, + origin=origin, + eeg=eeg_mne, + exclude=exclude, + method=method, + ) + if "fnirs" in method: + _interpolate_bads_nirs(self, exclude=exclude, method=method["fnirs"]) if reset_bads is True: if "nan" in method.values(): @@ -1010,7 +1014,7 @@ class _BuiltinChannelAdjacency: _ft_neighbor_url_t = string.Template( - "https://github.com/fieldtrip/fieldtrip/raw/master/" "template/neighbours/$fname" + "https://github.com/fieldtrip/fieldtrip/raw/master/template/neighbours/$fname" ) _BUILTIN_CHANNEL_ADJACENCIES = [ diff --git a/mne/channels/tests/test_interpolation.py b/mne/channels/tests/test_interpolation.py index 1eabb64de91..58cb6a1e669 100644 --- a/mne/channels/tests/test_interpolation.py +++ b/mne/channels/tests/test_interpolation.py @@ -5,11 +5,12 @@ import pytest from numpy.testing import assert_allclose, assert_array_equal -from mne import Epochs, io, pick_channels, pick_types, read_events +from mne import Epochs, pick_channels, pick_types, read_events +from mne._fiff.constants import FIFF from mne._fiff.proj import _has_eeg_average_ref_proj from mne.channels.interpolation import _make_interpolation_matrix from mne.datasets import testing -from mne.io import read_raw_nirx +from mne.io import RawArray, read_raw_ctf, read_raw_fif, read_raw_nirx from mne.preprocessing.nirs import ( beer_lambert_law, optical_density, @@ -30,7 +31,7 @@ def _load_data(kind): """Load data.""" # It is more memory efficient to load data in a separate # function so it's loaded on-demand - raw = io.read_raw_fif(raw_fname) + raw = read_raw_fif(raw_fname) events = read_events(event_name) # subselect channels for speed if kind == "eeg": @@ -86,7 +87,7 @@ def test_interpolation_eeg(offset, avg_proj, ctol, atol, method): # Offsetting the coordinate frame should have no effect on the output for inst in (raw, epochs_eeg): for ch in inst.info["chs"]: - if ch["kind"] == io.constants.FIFF.FIFFV_EEG_CH: + if ch["kind"] == FIFF.FIFFV_EEG_CH: ch["loc"][:3] += offset ch["loc"][3:6] += offset for d in inst.info["dig"]: @@ -167,7 +168,7 @@ def test_interpolation_eeg(offset, avg_proj, ctol, atol, method): epochs_eeg.preload = True # check that interpolation changes the data in raw - raw_eeg = io.RawArray(data=epochs_eeg._data[0], info=epochs_eeg.info) + raw_eeg = RawArray(data=epochs_eeg._data[0], info=epochs_eeg.info) raw_before = raw_eeg._data[bads_idx] raw_after = raw_eeg.interpolate_bads(**kw)._data[bads_idx] assert not np.all(raw_before == raw_after) @@ -220,7 +221,7 @@ def test_interpolation_meg(): pick = pick_channels(epochs_meg.info["ch_names"], epochs_meg.info["bads"]) # MEG -- raw - raw_meg = io.RawArray(data=epochs_meg._data[0], info=epochs_meg.info) + raw_meg = RawArray(data=epochs_meg._data[0], info=epochs_meg.info) raw_meg.info["bads"] = ["MEG 0141"] data1 = raw_meg[pick, :][0][0] @@ -269,7 +270,7 @@ def test_interpolate_meg_ctf(): tol = 0.05 # assert the new interpol correlates at least .05 "better" bad = "MLC22-2622" # select a good channel to test the interpolation - raw = io.read_raw_fif(raw_fname_ctf).crop(0, 1.0).load_data() # 3 secs + raw = read_raw_fif(raw_fname_ctf).crop(0, 1.0).load_data() # 3 secs raw.apply_gradient_compensation(3) # Show that we have to exclude ref_meg for interpolating CTF MEG-channels @@ -296,7 +297,7 @@ def test_interpolate_meg_ctf(): def test_interpolation_ctf_comp(): """Test interpolation with compensated CTF data.""" raw_fname = testing_path / "CTF" / "somMDYO-18av.ds" - raw = io.read_raw_ctf(raw_fname, preload=True) + raw = read_raw_ctf(raw_fname, preload=True) raw.info["bads"] = [raw.ch_names[5], raw.ch_names[-5]] raw.interpolate_bads(mode="fast", origin=(0.0, 0.0, 0.04)) assert raw.info["bads"] == [] @@ -348,3 +349,18 @@ def test_nan_interpolation(raw): raw.drop_channels(ch_to_interp) good_chs = raw.get_data() assert np.isfinite(good_chs).all() + + +@testing.requires_testing_data +def test_method_str(): + """Test method argument types.""" + raw = read_raw_fif( + testing_path / "MEG" / "sample" / "sample_audvis_trunc_raw.fif", + preload=False, + ) + raw.crop(0, 1).pick(("meg", "eeg"), exclude=()).load_data() + raw.copy().interpolate_bads(method="MNE") + with pytest.raises(ValueError, match="Invalid value for the"): + raw.interpolate_bads(method="spline") + raw.pick("eeg", exclude=()) + raw.interpolate_bads(method="spline") From f555d5f7c7389a6a3960e83253828b60fc816ce1 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Thu, 19 Oct 2023 14:04:15 -0400 Subject: [PATCH 014/405] BUG: Fix bug with weakref (#12119) --- mne/report/report.py | 2 +- mne/viz/topomap.py | 1 + mne/viz/utils.py | 8 +++++++- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/mne/report/report.py b/mne/report/report.py index 5abee10e1eb..d1138e1e610 100644 --- a/mne/report/report.py +++ b/mne/report/report.py @@ -414,7 +414,7 @@ def _fig_to_img(fig, *, image_format="png", own_figure=True): dpi = fig.get_dpi() logger.debug( f"Saving figure with dimension {fig.get_size_inches()} inches with " - f"{{dpi}} dpi" + f"{dpi} dpi" ) # https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html diff --git a/mne/viz/topomap.py b/mne/viz/topomap.py index 8629525ac4d..11d8ebce8a0 100644 --- a/mne/viz/topomap.py +++ b/mne/viz/topomap.py @@ -2494,6 +2494,7 @@ def _on_time_change( def _on_colormap_range(event, kwargs): """Handle updating colormap range.""" + logger.debug(f"Updating colormap range to {event.fmin}, {event.fmax}") kwargs.update(vlim=(event.fmin, event.fmax), cmap=event.cmap) diff --git a/mne/viz/utils.py b/mne/viz/utils.py index 2855b9784c4..c81bdf354c2 100644 --- a/mne/viz/utils.py +++ b/mne/viz/utils.py @@ -48,6 +48,7 @@ from ..rank import compute_rank from ..transforms import apply_trans from ..utils import ( + _auto_weakref, _check_ch_locs, _check_decim, _check_option, @@ -1493,7 +1494,12 @@ def __init__(self, cbar, mappable, kind, ch_type): self.index = self.cycle.index(mappable.get_cmap().name) self.lims = (self.cbar.norm.vmin, self.cbar.norm.vmax) self.connect() - subscribe(self.fig, "colormap_range", self._on_colormap_range) + + @_auto_weakref + def _on_colormap_range(event): + return self._on_colormap_range(event) + + subscribe(self.fig, "colormap_range", _on_colormap_range) def connect(self): """Connect to all the events we need.""" From c54d229913c5fe1bdb7a76b70930f66a89601c95 Mon Sep 17 00:00:00 2001 From: Mathieu Scheltienne Date: Mon, 23 Oct 2023 14:44:28 +0200 Subject: [PATCH 015/405] Fix combination of DIN events channels with EGI reader (#12122) --- doc/changes/devel.rst | 1 + mne/conftest.py | 1 + mne/io/egi/egimff.py | 12 +++++++++++- mne/io/egi/tests/test_egi.py | 6 +++--- 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/doc/changes/devel.rst b/doc/changes/devel.rst index a30901a5efd..5457e844f6c 100644 --- a/doc/changes/devel.rst +++ b/doc/changes/devel.rst @@ -73,6 +73,7 @@ Bugs - Correctly handle passing ``"eyegaze"`` or ``"pupil"`` to :meth:`mne.io.Raw.pick` (:gh:`12019` by `Scott Huberty`_) - Fix bug with :func:`~mne.viz.plot_raw` where changing ``MNE_BROWSER_BACKEND`` via :func:`~mne.set_config` would have no effect within a Python session (:gh:`12078` by `Santeri Ruuskanen`_) - Improve handling of ``method`` argument in the channel interpolation function to support :class:`str` and raise helpful error messages (:gh:`12113` by `Mathieu Scheltienne`_) +- Fix combination of ``DIN`` event channels into a single synthetic trigger channel ``STI 014`` by the MFF reader of :func:`mne.io.read_raw_egi` (:gh:`12122` by `Mathieu Scheltienne`_) API changes ~~~~~~~~~~~ diff --git a/mne/conftest.py b/mne/conftest.py index e4174e7deb9..1357a6a7c4a 100644 --- a/mne/conftest.py +++ b/mne/conftest.py @@ -176,6 +176,7 @@ def pytest_configure(config): 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 # TODO: Should actually fix these two ignore:scipy.signal.morlet2 is deprecated in SciPy.*:DeprecationWarning ignore:The `needs_threshold` and `needs_proba`.*:FutureWarning diff --git a/mne/io/egi/egimff.py b/mne/io/egi/egimff.py index b584acf9abd..fee53f2e589 100644 --- a/mne/io/egi/egimff.py +++ b/mne/io/egi/egimff.py @@ -507,7 +507,17 @@ def __init__( " Excluding events {%s} ..." % ", ".join([k for i, k in enumerate(event_codes) if i not in include_]) ) - events_ids = np.arange(len(include_)) + 1 + 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 + # parallel port. + events_ids = list() + for ch in include_names: + while not ch[0].isnumeric(): + ch = ch[1:] + events_ids.append(int(ch)) + else: + events_ids = np.arange(len(include_)) + 1 egi_info["new_trigger"] = _combine_triggers( egi_events[include_], remapping=events_ids ) diff --git a/mne/io/egi/tests/test_egi.py b/mne/io/egi/tests/test_egi.py index 7772d38f2d3..d57cb27359c 100644 --- a/mne/io/egi/tests/test_egi.py +++ b/mne/io/egi/tests/test_egi.py @@ -190,9 +190,9 @@ def test_io_egi_mff(): read_raw_egi(egi_mff_fname, include=["Foo"]) with pytest.raises(ValueError, match="Could not find event"): read_raw_egi(egi_mff_fname, exclude=["Bar"]) - for ii, k in enumerate(include, 1): - assert k in raw.event_id - assert raw.event_id[k] == ii + for ch in include: + assert ch in raw.event_id + assert raw.event_id[ch] == int(ch[-1]) def test_io_egi(): From 8c003e843717e316ba85008eff2298917489bbd1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 23 Oct 2023 21:34:20 +0000 Subject: [PATCH 016/405] [pre-commit.ci] pre-commit autoupdate (#12124) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5880c0d3b72..98d8db9db03 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,13 +1,13 @@ repos: - repo: https://github.com/psf/black - rev: 23.9.1 + rev: 23.10.1 hooks: - id: black args: [--quiet] # Ruff mne - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.0.292 + rev: v0.1.1 hooks: - id: ruff name: ruff mne @@ -15,7 +15,7 @@ repos: # Ruff tutorials and examples - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.0.292 + rev: v0.1.1 hooks: - id: ruff name: ruff tutorials and examples From 89b5a533867012a122fc43dfe57a18ad530c5906 Mon Sep 17 00:00:00 2001 From: Clemens Brunner Date: Tue, 24 Oct 2023 15:04:19 +0200 Subject: [PATCH 017/405] Fix array-like check (#12128) --- mne/utils/check.py | 3 +-- mne/utils/tests/test_check.py | 5 +++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/mne/utils/check.py b/mne/utils/check.py index a26495106e4..2faa364b779 100644 --- a/mne/utils/check.py +++ b/mne/utils/check.py @@ -8,7 +8,6 @@ import os import re from builtins import input # no-op here but facilitates testing -from collections.abc import Sequence from difflib import get_close_matches from importlib import import_module from importlib.metadata import version @@ -542,7 +541,7 @@ def __instancecheck__(cls, other): "path-like": path_like, "int-like": (int_like,), "callable": (_Callable(),), - "array-like": (Sequence, np.ndarray), + "array-like": (list, tuple, set, np.ndarray), } diff --git a/mne/utils/tests/test_check.py b/mne/utils/tests/test_check.py index 2856d9ea37b..2cda3188cd8 100644 --- a/mne/utils/tests/test_check.py +++ b/mne/utils/tests/test_check.py @@ -212,6 +212,11 @@ def test_validate_type(): _validate_type(1, "int-like") with pytest.raises(TypeError, match="int-like"): _validate_type(False, "int-like") + _validate_type([1, 2, 3], "array-like") + _validate_type((1, 2, 3), "array-like") + _validate_type({1, 2, 3}, "array-like") + with pytest.raises(TypeError, match="array-like"): + _validate_type("123", "array-like") # a string is not array-like def test_check_range(): From a320b631a7c5b06201505f7f86b1d163074d8420 Mon Sep 17 00:00:00 2001 From: Daniel McCloy Date: Tue, 24 Oct 2023 10:35:21 -0500 Subject: [PATCH 018/405] typo fix in maxwell filtering tutorial (#12129) --- tutorials/preprocessing/60_maxwell_filtering_sss.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tutorials/preprocessing/60_maxwell_filtering_sss.py b/tutorials/preprocessing/60_maxwell_filtering_sss.py index c1453528975..f07caa46257 100644 --- a/tutorials/preprocessing/60_maxwell_filtering_sss.py +++ b/tutorials/preprocessing/60_maxwell_filtering_sss.py @@ -326,7 +326,7 @@ # %% # Head position data can be computed using # :func:`mne.chpi.compute_chpi_locs` and :func:`mne.chpi.compute_head_pos`, -# or loaded with the:func:`mne.chpi.read_head_pos` function. The +# or loaded with the :func:`mne.chpi.read_head_pos` function. The # :ref:`example data ` doesn't include cHPI, so here we'll # load a :file:`.pos` file used for testing, just to demonstrate: From 4518a1a918051542ab7360ff63c529121a499417 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Tue, 24 Oct 2023 16:29:39 -0400 Subject: [PATCH 019/405] DOC: Document governance updates (#12133) --- doc/development/governance.rst | 2 +- doc/overview/people.rst | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/doc/development/governance.rst b/doc/development/governance.rst index f7d81ee8a85..f5b70e39485 100644 --- a/doc/development/governance.rst +++ b/doc/development/governance.rst @@ -69,7 +69,7 @@ BDFL ---- The Project will have a BDFL (Benevolent Dictator for Life), who is currently -Alexandre Gramfort. As Dictator, the BDFL has the authority to make all final +Daniel McCloy. As Dictator, the BDFL has the authority to make all final decisions for The Project. As Benevolent, the BDFL, in practice, chooses to defer that authority to the consensus of the community discussion channels and the Steering Council (see below). It is expected, and in the past has been the diff --git a/doc/overview/people.rst b/doc/overview/people.rst index 14c20724095..3647aae978a 100644 --- a/doc/overview/people.rst +++ b/doc/overview/people.rst @@ -22,8 +22,6 @@ Steering Council * `Daniel McCloy`_ * `Denis Engemann`_ * `Eric Larson`_ -* `Guillaume Favelier`_ -* `Luke Bloy`_ * `Mainak Jas`_ * `Marijn van Vliet`_ * `Mathieu Scheltienne`_ From b9983df4673dbcb36155509c3b3c4b4352798402 Mon Sep 17 00:00:00 2001 From: Dimitri Papadopoulos Orfanos <3234522+DimitriPapadopoulos@users.noreply.github.com> Date: Thu, 26 Oct 2023 15:34:00 +0200 Subject: [PATCH 020/405] DOC: Fix typo found by codespell (#12140) --- examples/preprocessing/eeg_bridging.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/preprocessing/eeg_bridging.py b/examples/preprocessing/eeg_bridging.py index 7eadb7239d2..6d2c1aec165 100644 --- a/examples/preprocessing/eeg_bridging.py +++ b/examples/preprocessing/eeg_bridging.py @@ -10,7 +10,7 @@ electrode connects with the gel conducting signal from another electrode "bridging" the two signals. This is undesirable because the signals from the two (or more) electrodes are not as independent as they would otherwise be; -they are very similar to each other introducting additional +they are very similar to each other introducing additional spatial smearing. An algorithm has been developed to detect electrode bridging :footcite:`TenkeKayser2001`, which has been implemented in EEGLAB :footcite:`DelormeMakeig2004`. Unfortunately, there is not a lot to be From c733e7b4d471eb72231b6492aa3031efc28d426d Mon Sep 17 00:00:00 2001 From: Dimitri Papadopoulos Orfanos <3234522+DimitriPapadopoulos@users.noreply.github.com> Date: Thu, 26 Oct 2023 15:34:26 +0200 Subject: [PATCH 021/405] Remove LGTM.com configuration file (#12139) --- .lgtm.yml | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 .lgtm.yml diff --git a/.lgtm.yml b/.lgtm.yml deleted file mode 100644 index 4a43aa25c57..00000000000 --- a/.lgtm.yml +++ /dev/null @@ -1,8 +0,0 @@ -extraction: - javascript: - index: - filters: - - exclude: "**/*.js" -queries: - - exclude: py/missing-equals - - exclude: py/import-and-import-from From 58e8c7e779e741fe4168b4b0dd723aba7552a665 Mon Sep 17 00:00:00 2001 From: Mathieu Scheltienne Date: Thu, 26 Oct 2023 16:30:34 +0200 Subject: [PATCH 022/405] Add mne-icalabel wildcard (#12143) --- mne/utils/config.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mne/utils/config.py b/mne/utils/config.py index aa4e0b9dd90..37aca22bd03 100644 --- a/mne/utils/config.py +++ b/mne/utils/config.py @@ -205,6 +205,7 @@ def set_memmap_min_size(memmap_min_size): "MNE_DATASETS_FNIRS", # mne-nirs "MNE_NIRS", # mne-nirs "MNE_KIT2FIFF", # mne-kit-gui + "MNE_ICALABEL", # mne-icalabel ) From debc275b795ea4cf4da72a74ee586c82b2e154fc Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Thu, 26 Oct 2023 15:18:04 -0400 Subject: [PATCH 023/405] DOC: Better documentation of realign_raw (#12135) --- mne/epochs.py | 3 ++- mne/io/bti/read.py | 12 +++++++++--- mne/io/ctf/res4.py | 17 ++++++++++++++++- mne/io/edf/edf.py | 6 +++--- mne/io/egi/egi.py | 6 +++--- mne/io/egi/egimff.py | 2 +- mne/io/nihon/nihon.py | 8 ++++---- mne/io/nsx/nsx.py | 2 +- mne/preprocessing/realign.py | 13 +++++++------ mne/report/tests/test_report.py | 2 ++ mne/surface.py | 2 +- tools/azure_dependencies.sh | 3 ++- tools/github_actions_dependencies.sh | 11 +++++++++-- 13 files changed, 60 insertions(+), 27 deletions(-) diff --git a/mne/epochs.py b/mne/epochs.py index 5af8f382c88..459a4ce3460 100644 --- a/mne/epochs.py +++ b/mne/epochs.py @@ -4062,12 +4062,13 @@ def _concatenate_epochs( event_id = deepcopy(out.event_id) selection = out.selection # offset is the last epoch + tmax + 10 second - shift = int((10 + tmax) * out.info["sfreq"]) + shift = np.int64((10 + tmax) * out.info["sfreq"]) # Allow reading empty epochs (ToDo: Maybe not anymore in the future) if out._allow_empty: events_offset = 0 else: events_offset = int(np.max(events[0][:, 0])) + shift + events_offset = np.int64(events_offset) events_overflow = False warned = False for ii, epochs in enumerate(epochs_list[1:], 1): diff --git a/mne/io/bti/read.py b/mne/io/bti/read.py index 4af53112ae8..d05e2d9d941 100644 --- a/mne/io/bti/read.py +++ b/mne/io/bti/read.py @@ -48,12 +48,12 @@ def read_int8(fid): def read_uint16(fid): """Read unsigned 16bit integer from bti file.""" - return _unpack_simple(fid, ">u2", np.uint16) + return _unpack_simple(fid, ">u2", np.uint32) def read_int16(fid): """Read 16bit integer from bti file.""" - return _unpack_simple(fid, ">i2", np.int16) + return _unpack_simple(fid, ">i2", np.int32) def read_uint32(fid): @@ -88,7 +88,13 @@ def read_double(fid): def read_int16_matrix(fid, rows, cols): """Read 16bit integer matrix from bti file.""" - return _unpack_matrix(fid, rows, cols, dtype=">i2", out_dtype=np.int16) + return _unpack_matrix( + fid, + rows, + cols, + dtype=">i2", + out_dtype=np.int32, + ) def read_float_matrix(fid, rows, cols): diff --git a/mne/io/ctf/res4.py b/mne/io/ctf/res4.py index b5c0f884c99..2ea2f619bcc 100644 --- a/mne/io/ctf/res4.py +++ b/mne/io/ctf/res4.py @@ -43,7 +43,7 @@ def _read_ustring(fid, n_bytes): def _read_int2(fid): """Read int from short.""" - return np.fromfile(fid, ">i2", 1)[0] + return _auto_cast(np.fromfile(fid, ">i2", 1)[0]) def _read_int(fid): @@ -208,6 +208,9 @@ def _read_res4(dsdir): coil["area"] *= 1e-4 # convert to dict chs = [dict(zip(chs.dtype.names, x)) for x in chs] + for ch in chs: + for key, val in ch.items(): + ch[key] = _auto_cast(val) res["chs"] = chs for k in range(res["nchan"]): res["chs"][k]["ch_name"] = res["ch_names"][k] @@ -216,3 +219,15 @@ def _read_res4(dsdir): _read_comp_coeff(fid, res) logger.info(" res4 data read.") return res + + +def _auto_cast(x): + # Upcast scalars + if isinstance(x, np.ScalarType): + if x.dtype.kind == "i": + if x.dtype != np.int64: + x = x.astype(np.int64) + elif x.dtype.kind == "f": + if x.dtype != np.float64: + x = x.astype(np.float64) + return x diff --git a/mne/io/edf/edf.py b/mne/io/edf/edf.py index 8831989860c..e507a651676 100644 --- a/mne/io/edf/edf.py +++ b/mne/io/edf/edf.py @@ -1106,7 +1106,7 @@ def _read_gdf_header(fname, exclude, include=None): "Header information is incorrect for record length. " "Default record length set to 1." ) - nchan = np.fromfile(fid, UINT32, 1)[0] + nchan = int(np.fromfile(fid, UINT32, 1)[0]) channels = list(range(nchan)) ch_names = [_edf_str(fid.read(16)).strip() for ch in channels] exclude = _find_exclude_idx(ch_names, exclude, include) @@ -1177,7 +1177,7 @@ def _read_gdf_header(fname, exclude, include=None): fid.seek(etp) etmode = np.fromfile(fid, UINT8, 1)[0] if etmode in (1, 3): - sr = np.fromfile(fid, UINT8, 3) + sr = np.fromfile(fid, UINT8, 3).astype(np.uint32) event_sr = sr[0] for i in range(1, len(sr)): event_sr = event_sr + sr[i] * 2 ** (i * 8) @@ -1297,7 +1297,7 @@ def _read_gdf_header(fname, exclude, include=None): "Default record length set to 1." ) - nchan = np.fromfile(fid, UINT16, 1)[0] + nchan = int(np.fromfile(fid, UINT16, 1)[0]) fid.seek(2, 1) # 2bytes reserved # Channels (variable header) diff --git a/mne/io/egi/egi.py b/mne/io/egi/egi.py index d6ba4b884f6..0bd669837a3 100644 --- a/mne/io/egi/egi.py +++ b/mne/io/egi/egi.py @@ -29,7 +29,7 @@ def _read_header(fid): ) def my_fread(*x, **y): - return np.fromfile(*x, **y)[0] + return int(np.fromfile(*x, **y)[0]) info = dict( version=version, @@ -57,8 +57,8 @@ def my_fread(*x, **y): dict( n_categories=0, n_segments=1, - n_samples=np.fromfile(fid, ">i4", 1)[0], - n_events=np.fromfile(fid, ">i2", 1)[0], + n_samples=int(np.fromfile(fid, ">i4", 1)[0]), + n_events=int(np.fromfile(fid, ">i2", 1)[0]), event_codes=[], category_names=[], category_lengths=[], diff --git a/mne/io/egi/egimff.py b/mne/io/egi/egimff.py index fee53f2e589..1120324c58a 100644 --- a/mne/io/egi/egimff.py +++ b/mne/io/egi/egimff.py @@ -79,7 +79,7 @@ def _read_mff_header(filepath): # by what we need to (e.g., a sample rate of 500 means we can multiply # by 1 and divide by 2 rather than multiplying by 500 and dividing by # 1000) - numerator = signal_blocks["sfreq"] + numerator = int(signal_blocks["sfreq"]) denominator = 1000 this_gcd = math.gcd(numerator, denominator) numerator = numerator // this_gcd diff --git a/mne/io/nihon/nihon.py b/mne/io/nihon/nihon.py index ab1e476fc5d..b6b7e3179ff 100644 --- a/mne/io/nihon/nihon.py +++ b/mne/io/nihon/nihon.py @@ -207,7 +207,7 @@ def _read_nihon_header(fname): t_datablock["address"] = t_data_address fid.seek(t_data_address + 0x26) - t_n_channels = np.fromfile(fid, np.uint8, 1)[0] + t_n_channels = np.fromfile(fid, np.uint8, 1)[0].astype(np.int64) t_datablock["n_channels"] = t_n_channels t_channels = [] @@ -219,14 +219,14 @@ def _read_nihon_header(fname): t_datablock["channels"] = t_channels fid.seek(t_data_address + 0x1C) - t_record_duration = np.fromfile(fid, np.uint32, 1)[0] + t_record_duration = np.fromfile(fid, np.uint32, 1)[0].astype(np.int64) t_datablock["duration"] = t_record_duration fid.seek(t_data_address + 0x1A) sfreq = np.fromfile(fid, np.uint16, 1)[0] & 0x3FFF - t_datablock["sfreq"] = sfreq + t_datablock["sfreq"] = sfreq.astype(np.int64) - t_datablock["n_samples"] = int(t_record_duration * sfreq / 10) + t_datablock["n_samples"] = np.int64(t_record_duration * sfreq // 10) t_controlblock["datablocks"].append(t_datablock) controlblocks.append(t_controlblock) header["controlblocks"] = controlblocks diff --git a/mne/io/nsx/nsx.py b/mne/io/nsx/nsx.py index 5d3b2e7a659..a74bcd05f30 100644 --- a/mne/io/nsx/nsx.py +++ b/mne/io/nsx/nsx.py @@ -365,7 +365,7 @@ def _get_hdr_info(fname, stim_channel=True, eog=None, misc=None): stim_channel_idxs, _ = _check_stim_channel(stim_channel, ch_names) - nchan = nsx_info["channel_count"] + nchan = int(nsx_info["channel_count"]) logger.info("Setting channel info structure...") chs = list() pick_mask = np.ones(len(ch_names)) diff --git a/mne/preprocessing/realign.py b/mne/preprocessing/realign.py index 396e4ba33e6..09442ca9b1c 100644 --- a/mne/preprocessing/realign.py +++ b/mne/preprocessing/realign.py @@ -28,8 +28,9 @@ def realign_raw(raw, other, t_raw, t_other, verbose=None): The second raw instance. It will be resampled to match ``raw``. t_raw : array-like, shape (n_events,) The times of shared events in ``raw`` relative to ``raw.times[0]`` (0). - Typically these could be events on some TTL channel like - ``find_events(raw)[:, 0] - raw.first_samp``. + Typically these could be events on some TTL channel such as:: + + find_events(raw)[:, 0] / raw.info["sfreq"] - raw.first_time t_other : array-like, shape (n_events,) The times of shared events in ``other`` relative to ``other.times[0]``. %(verbose)s @@ -92,11 +93,11 @@ def realign_raw(raw, other, t_raw, t_other, verbose=None): logger.info(f"Cropping {zero_ord:0.3f} s from the start of raw") raw.crop(zero_ord, None) t_raw -= zero_ord - else: # need to crop start of other to match raw - t_crop = zero_ord / first_ord + elif zero_ord < 0: # need to crop start of other to match raw + t_crop = -zero_ord / first_ord logger.info(f"Cropping {t_crop:0.3f} s from the start of other") - other.crop(-t_crop, None) - t_other += t_crop + other.crop(t_crop, None) + t_other -= t_crop # 3. Resample data using the first-order term nan_ch_names = [ diff --git a/mne/report/tests/test_report.py b/mne/report/tests/test_report.py index 7577774e313..dd7ebfc34c8 100644 --- a/mne/report/tests/test_report.py +++ b/mne/report/tests/test_report.py @@ -877,6 +877,8 @@ def test_survive_pickle(tmp_path): def test_manual_report_2d(tmp_path, invisible_fig): """Simulate user manually creating report by adding one file at a time.""" pytest.importorskip("sklearn") + pytest.importorskip("pandas") + from sklearn.exceptions import ConvergenceWarning r = Report(title="My Report") diff --git a/mne/surface.py b/mne/surface.py index b042361305a..d0aac3abe0d 100644 --- a/mne/surface.py +++ b/mne/surface.py @@ -772,7 +772,7 @@ def _call_old(self, rr, n_jobs): def _fread3(fobj): """Read 3 bytes and adjust.""" - b1, b2, b3 = np.fromfile(fobj, ">u1", 3) + b1, b2, b3 = np.fromfile(fobj, ">u1", 3).astype(np.int64) return (b1 << 16) + (b2 << 8) + b3 diff --git a/tools/azure_dependencies.sh b/tools/azure_dependencies.sh index 072665d9c3c..5cf455cf4f4 100755 --- a/tools/azure_dependencies.sh +++ b/tools/azure_dependencies.sh @@ -9,7 +9,8 @@ elif [ "${TEST_MODE}" == "pip-pre" ]; then python -m pip install $STD_ARGS pip setuptools wheel packaging setuptools_scm python -m pip install $STD_ARGS --only-binary ":all:" --extra-index-url "https://www.riverbankcomputing.com/pypi/simple" PyQt6 PyQt6-sip PyQt6-Qt6 echo "Numpy etc." - python -m pip install $STD_ARGS --only-binary ":all:" --extra-index-url "https://pypi.anaconda.org/scientific-python-nightly-wheels/simple" "numpy>=2.0.0.dev0" "scipy>=1.12.0.dev0" statsmodels pandas scikit-learn matplotlib + # As of 2023/10/25 no pandas (or statsmodels) because they pin to NumPy < 2 + python -m pip install $STD_ARGS --only-binary ":all:" --extra-index-url "https://pypi.anaconda.org/scientific-python-nightly-wheels/simple" "numpy>=2.0.0.dev0" "scipy>=1.12.0.dev0" scikit-learn matplotlib echo "dipy" python -m pip install $STD_ARGS --only-binary ":all:" --extra-index-url "https://pypi.anaconda.org/scipy-wheels-nightly/simple" dipy echo "h5py" diff --git a/tools/github_actions_dependencies.sh b/tools/github_actions_dependencies.sh index d08e5727e77..65a64e05ae5 100755 --- a/tools/github_actions_dependencies.sh +++ b/tools/github_actions_dependencies.sh @@ -1,5 +1,7 @@ #!/bin/bash -ef +set -o pipefail + STD_ARGS="--progress-bar off --upgrade" if [ ! -z "$CONDA_ENV" ]; then echo "Uninstalling MNE for CONDA_ENV=${CONDA_ENV}" @@ -18,7 +20,8 @@ else echo "PyQt6" pip install $STD_ARGS --only-binary ":all:" --default-timeout=60 --extra-index-url https://www.riverbankcomputing.com/pypi/simple PyQt6 echo "NumPy/SciPy/pandas etc." - pip install $STD_ARGS --only-binary ":all:" --default-timeout=60 --extra-index-url "https://pypi.anaconda.org/scientific-python-nightly-wheels/simple" "numpy>=2.0.0.dev0" scipy scikit-learn pandas matplotlib pillow statsmodels + # As of 2023/10/25 no pandas (or statsmodels, nilearn) because they pin to NumPy < 2 + pip install $STD_ARGS --only-binary ":all:" --default-timeout=60 --extra-index-url "https://pypi.anaconda.org/scientific-python-nightly-wheels/simple" "numpy>=2.0.0.dev0" scipy scikit-learn matplotlib pillow echo "dipy" pip install $STD_ARGS --only-binary ":all:" --default-timeout=60 --extra-index-url "https://pypi.anaconda.org/scipy-wheels-nightly/simple" dipy echo "H5py" @@ -27,7 +30,8 @@ else pip install $STD_ARGS --only-binary ":all:" --extra-index-url "https://test.pypi.org/simple" openmeeg # No Numba because it forces an old NumPy version echo "nilearn and openmeeg" - pip install $STD_ARGS git+https://github.com/nilearn/nilearn + # pip install $STD_ARGS git+https://github.com/nilearn/nilearn + pip install $STD_ARGS openmeeg echo "VTK" pip install $STD_ARGS --only-binary ":all:" --extra-index-url "https://wheels.vtk.org" vtk python -c "import vtk" @@ -45,9 +49,12 @@ else pip install $STD_ARGS git+https://github.com/joblib/joblib@master echo "EDFlib-Python" pip install $STD_ARGS git+https://gitlab.com/Teuniz/EDFlib-Python@master + # Until Pandas is fixed, make sure we didn't install it + ! python -c "import pandas" fi echo "" + # for compat_minimal and compat_old, we don't want to --upgrade if [ ! -z "$CONDA_DEPENDENCIES" ]; then echo "Installing dependencies for conda" From cdad29dac30041ee79d4e719cb53ad63ffdf21a9 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Thu, 26 Oct 2023 21:51:29 -0400 Subject: [PATCH 024/405] ENH: set color for bad channel with spatial_colors=True (#12142) --- doc/changes/devel.rst | 2 ++ mne/time_frequency/tests/test_spectrum.py | 14 +++++++++++--- mne/viz/evoked.py | 8 +++++--- mne/viz/tests/test_evoked.py | 7 +++++++ mne/viz/utils.py | 3 ++- tutorials/intro/70_report.py | 3 +-- 6 files changed, 28 insertions(+), 9 deletions(-) diff --git a/doc/changes/devel.rst b/doc/changes/devel.rst index 5457e844f6c..c52705a9a9d 100644 --- a/doc/changes/devel.rst +++ b/doc/changes/devel.rst @@ -38,6 +38,7 @@ Enhancements - Add support for writing forward solutions to HDF5 and convenience function :meth:`mne.Forward.save` (:gh:`12036` by `Eric Larson`_) - Refactored internals of :func:`mne.read_annotations` (:gh:`11964` by `Paul Roujansky`_) - Add support for drawing MEG sensors in :ref:`mne coreg` (:gh:`12098` by `Eric Larson`_) +- Bad channels are now colored gray in addition to being dashed when spatial colors are used in :func:`mne.viz.plot_evoked` and related functions (:gh:`12142` by `Eric Larson`_) - By default MNE-Python creates matplotlib figures with ``layout='constrained'`` rather than the default ``layout='tight'`` (:gh:`12050`, :gh:`12103` by `Mathieu Scheltienne`_ and `Eric Larson`_) - Enhance :func:`~mne.viz.plot_evoked_field` with a GUI that has controls for time, colormap, and contour lines (:gh:`11942` by `Marijn van Vliet`_) - Add :class:`mne.viz.ui_events.UIEvent` linking for interactive colorbars, allowing users to link figures and change the colormap and limits interactively. This supports :func:`~mne.viz.plot_evoked_topomap`, :func:`~mne.viz.plot_ica_components`, :func:`~mne.viz.plot_tfr_topomap`, :func:`~mne.viz.plot_projs_topomap`, :meth:`~mne.Evoked.plot_image`, and :meth:`~mne.Epochs.plot_image` (:gh:`12057` by `Santeri Ruuskanen`_) @@ -71,6 +72,7 @@ Bugs - Fix :func:`~mne.viz.plot_volume_source_estimates` with :class:`~mne.VolSourceEstimate` which include a list of vertices (:gh:`12025` by `Mathieu Scheltienne`_) - Add support for non-ASCII characters in Annotations, Evoked comments, etc when saving to FIFF format (:gh:`12080` by `Daniel McCloy`_) - Correctly handle passing ``"eyegaze"`` or ``"pupil"`` to :meth:`mne.io.Raw.pick` (:gh:`12019` by `Scott Huberty`_) +- Fix bug with :func:`mne.time_frequency.Spectrum.plot` and related functions where bad channels were not marked (:gh:`12142` by `Eric Larson`_) - Fix bug with :func:`~mne.viz.plot_raw` where changing ``MNE_BROWSER_BACKEND`` via :func:`~mne.set_config` would have no effect within a Python session (:gh:`12078` by `Santeri Ruuskanen`_) - Improve handling of ``method`` argument in the channel interpolation function to support :class:`str` and raise helpful error messages (:gh:`12113` by `Mathieu Scheltienne`_) - Fix combination of ``DIN`` event channels into a single synthetic trigger channel ``STI 014`` by the MFF reader of :func:`mne.io.read_raw_egi` (:gh:`12122` by `Mathieu Scheltienne`_) diff --git a/mne/time_frequency/tests/test_spectrum.py b/mne/time_frequency/tests/test_spectrum.py index 7aaa5b40ea6..96fe89a2e6d 100644 --- a/mne/time_frequency/tests/test_spectrum.py +++ b/mne/time_frequency/tests/test_spectrum.py @@ -1,9 +1,9 @@ from contextlib import nullcontext from functools import partial -import matplotlib.pyplot as plt import numpy as np import pytest +from matplotlib.colors import same_color from numpy.testing import assert_allclose, assert_array_equal from mne import Annotations, create_info, make_fixed_length_epochs @@ -449,8 +449,16 @@ def test_plot_spectrum(kind, array, request): data, freqs = spectrum.get_data(return_freqs=True) Klass = SpectrumArray if kind == "raw" else EpochsSpectrumArray spectrum = Klass(data=data, info=spectrum.info, freqs=freqs) + spectrum.info["bads"] = spectrum.ch_names[:1] # one grad channel spectrum.plot(average=True, amplitude=True, spatial_colors=True) - spectrum.plot(average=False, amplitude=False, spatial_colors=False) + spectrum.plot(average=True, amplitude=False, spatial_colors=False) + n_grad = sum(ch_type == "grad" for ch_type in spectrum.get_channel_types()) + for amp, sc in ((True, True), (False, False)): + fig = spectrum.plot(average=False, amplitude=amp, spatial_colors=sc, exclude=()) + lines = fig.axes[0].lines[2:] # grads, ignore two vlines + assert len(lines) == n_grad + bad_color = "0.5" if sc else "r" + n_bad = sum(same_color(line.get_color(), bad_color) for line in lines) + assert n_bad == 1 spectrum.plot_topo() spectrum.plot_topomap() - plt.close("all") diff --git a/mne/viz/evoked.py b/mne/viz/evoked.py index 6abcbcc0d1d..1c6712a6bec 100644 --- a/mne/viz/evoked.py +++ b/mne/viz/evoked.py @@ -679,15 +679,17 @@ def _plot_lines( _handle_spatial_colors( colors, info, idx, this_type, psd, ax, sphere ) + bad_color = (0.5, 0.5, 0.5) else: if isinstance(_spat_col, (tuple, str)): col = [_spat_col] else: col = ["k"] + bad_color = "r" colors = col * len(idx) - for i in bad_ch_idx: - if i in idx: - colors[idx.index(i)] = "r" + for i in bad_ch_idx: + if i in idx: + colors[idx.index(i)] = bad_color if zorder == "std": # find the channels with the least activity diff --git a/mne/viz/tests/test_evoked.py b/mne/viz/tests/test_evoked.py index c089b064d4a..51b83f222fa 100644 --- a/mne/viz/tests/test_evoked.py +++ b/mne/viz/tests/test_evoked.py @@ -16,6 +16,7 @@ import pytest from matplotlib import gridspec from matplotlib.collections import PolyCollection +from matplotlib.colors import same_color from mpl_toolkits.axes_grid1.parasite_axes import HostAxes # spatial_colors from numpy.testing import assert_allclose @@ -134,6 +135,12 @@ def test_plot_evoked(): amplitudes = _get_amplitudes(fig) assert len(amplitudes) == len(default_picks) assert evoked.proj is False + assert evoked.info["bads"] == ["MEG 2641", "EEG 004"] + eeg_lines = fig.axes[2].lines + n_eeg = sum(ch_type == "eeg" for ch_type in evoked.get_channel_types()) + assert len(eeg_lines) == n_eeg == 4 + n_bad = sum(same_color(line.get_color(), "0.5") for line in eeg_lines) + assert n_bad == 1 # Test a click ax = fig.get_axes()[0] line = ax.lines[0] diff --git a/mne/viz/utils.py b/mne/viz/utils.py index c81bdf354c2..e9c36281bae 100644 --- a/mne/viz/utils.py +++ b/mne/viz/utils.py @@ -2506,6 +2506,7 @@ def _plot_psd( if not average: picks = np.concatenate(picks_list) info = pick_info(inst.info, sel=picks, copy=True) + bad_ch_idx = [info["ch_names"].index(ch) for ch in info["bads"]] types = np.array(info.get_channel_types()) ch_types_used = list() for this_type in _VALID_CHANNEL_TYPES: @@ -2538,7 +2539,7 @@ def _plot_psd( xlim=(freqs[0], freqs[-1]), ylim=None, times=freqs, - bad_ch_idx=[], + bad_ch_idx=bad_ch_idx, titles=titles, ch_types_used=ch_types_used, selectable=True, diff --git a/tutorials/intro/70_report.py b/tutorials/intro/70_report.py index a7d3b02b2b3..951c82a5e6a 100644 --- a/tutorials/intro/70_report.py +++ b/tutorials/intro/70_report.py @@ -17,8 +17,7 @@ HTML pages it generates are self-contained and do not require a running Python environment. However, it is less flexible as you can't change code and re-run something directly within the browser. This tutorial covers the basics of -building a report. As usual, we will start by importing the modules and data we -need: +building a report. As usual, we will start by importing the modules and data we need: """ # %% From 3add8f87fd729af9d740ebe5ea3e6f46e95bb782 Mon Sep 17 00:00:00 2001 From: Mathieu Scheltienne Date: Mon, 30 Oct 2023 13:44:40 +0100 Subject: [PATCH 025/405] Fix in conftest.py (#12150) --- mne/conftest.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/mne/conftest.py b/mne/conftest.py index 1357a6a7c4a..33396621890 100644 --- a/mne/conftest.py +++ b/mne/conftest.py @@ -92,8 +92,6 @@ def pytest_configure(config): # Fixtures for fixture in ( "matplotlib_config", - "close_all", - "check_verbose", "qt_config", "protect_config", ): @@ -268,10 +266,7 @@ def matplotlib_config(): # functionality) plt.ioff() plt.rcParams["figure.dpi"] = 100 - try: - plt.rcParams["figure.raise_window"] = False - except KeyError: # MPL < 3.3 - pass + plt.rcParams["figure.raise_window"] = False # Make sure that we always reraise exceptions in handlers orig = cbook.CallbackRegistry From 72225b57014ea05cd26327e57c6852b969cbc570 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Mon, 30 Oct 2023 13:17:33 -0400 Subject: [PATCH 026/405] ENH: Warn about versions in sys_info (#12146) Co-authored-by: Daniel McCloy --- .pre-commit-config.yaml | 1 + doc/changes/devel.rst | 1 + mne/commands/mne_sys_info.py | 8 +++ mne/commands/tests/test_commands.py | 2 +- mne/utils/config.py | 77 +++++++++++++++++++++++++++-- mne/utils/tests/test_config.py | 74 ++++++++++++++++++++++++++- 6 files changed, 157 insertions(+), 6 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 98d8db9db03..3ae6169dc76 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,6 +11,7 @@ repos: hooks: - id: ruff name: ruff mne + args: ["--fix"] files: ^mne/ # Ruff tutorials and examples diff --git a/doc/changes/devel.rst b/doc/changes/devel.rst index c52705a9a9d..3778dd6f6d6 100644 --- a/doc/changes/devel.rst +++ b/doc/changes/devel.rst @@ -38,6 +38,7 @@ Enhancements - Add support for writing forward solutions to HDF5 and convenience function :meth:`mne.Forward.save` (:gh:`12036` by `Eric Larson`_) - Refactored internals of :func:`mne.read_annotations` (:gh:`11964` by `Paul Roujansky`_) - Add support for drawing MEG sensors in :ref:`mne coreg` (:gh:`12098` by `Eric Larson`_) +- Add ``check_version=True`` to :ref:`mne sys_info` to check for a new release on GitHub (:gh:`12146` by `Eric Larson`_) - Bad channels are now colored gray in addition to being dashed when spatial colors are used in :func:`mne.viz.plot_evoked` and related functions (:gh:`12142` by `Eric Larson`_) - By default MNE-Python creates matplotlib figures with ``layout='constrained'`` rather than the default ``layout='tight'`` (:gh:`12050`, :gh:`12103` by `Mathieu Scheltienne`_ and `Eric Larson`_) - Enhance :func:`~mne.viz.plot_evoked_field` with a GUI that has controls for time, colormap, and contour lines (:gh:`11942` by `Marijn van Vliet`_) diff --git a/mne/commands/mne_sys_info.py b/mne/commands/mne_sys_info.py index 62bbf9afbfe..ae355c2cef5 100644 --- a/mne/commands/mne_sys_info.py +++ b/mne/commands/mne_sys_info.py @@ -41,6 +41,13 @@ def run(): action="store_false", default=True, ) + parser.add_option( + "--no-check-version", + dest="check_version", + help="Disable MNE-Python remote version checking.", + action="store_false", + default=True, + ) options, args = parser.parse_args() dependencies = "developer" if options.developer else "user" if len(args) != 0: @@ -51,6 +58,7 @@ def run(): show_paths=options.show_paths, dependencies=dependencies, unicode=options.unicode, + check_version=options.check_version, ) diff --git a/mne/commands/tests/test_commands.py b/mne/commands/tests/test_commands.py index fecd0235a87..ba7693237b9 100644 --- a/mne/commands/tests/test_commands.py +++ b/mne/commands/tests/test_commands.py @@ -538,7 +538,7 @@ def test_sys_info(): with ArgvSetter((raw_fname,)): with pytest.raises(SystemExit, match="1"): mne_sys_info.run() - with ArgvSetter() as out: + with ArgvSetter(("--no-check-version",)) as out: mne_sys_info.run() assert "numpy" in out.stdout.getvalue() diff --git a/mne/utils/config.py b/mne/utils/config.py index 37aca22bd03..7f6267a4e19 100644 --- a/mne/utils/config.py +++ b/mne/utils/config.py @@ -14,9 +14,13 @@ import subprocess import sys import tempfile -from functools import partial +from functools import lru_cache, partial from importlib import import_module from pathlib import Path +from urllib.error import URLError +from urllib.request import urlopen + +from packaging.version import parse from ._logging import logger, warn from .check import _check_fname, _check_option, _check_qt_version, _validate_type @@ -569,6 +573,7 @@ def _get_numpy_libs(): print(gi.renderer)""" +@lru_cache(maxsize=1) def _get_gpu_info(): # Once https://github.com/pyvista/pyvista/pull/2250 is merged and PyVista # does a release, we can triage based on version > 0.33.2 @@ -581,7 +586,14 @@ def _get_gpu_info(): return out -def sys_info(fid=None, show_paths=False, *, dependencies="user", unicode=True): +def sys_info( + fid=None, + show_paths=False, + *, + dependencies="user", + unicode=True, + check_version=True, +): """Print system information. This function prints system information useful when triaging bugs. @@ -600,9 +612,16 @@ def sys_info(fid=None, show_paths=False, *, dependencies="user", unicode=True): Include Unicode symbols in output. .. versionadded:: 0.24 + check_version : bool | float + If True (default), attempt to check that the version of MNE-Python is up to date + with the latest release on GitHub. Can be a float to give a different timeout + (in sec) from the default (2 sec). + + .. versionadded:: 1.6 """ _validate_type(dependencies, str) _check_option("dependencies", dependencies, ("user", "developer")) + _validate_type(check_version, (bool, "numeric"), "check_version") ljust = 24 if dependencies == "developer" else 21 platform_str = platform.platform() if platform.system() == "Darwin" and sys.version_info[:2] < (3, 8): @@ -695,6 +714,7 @@ def sys_info(fid=None, show_paths=False, *, dependencies="user", unicode=True): unicode = unicode and (sys.stdout.encoding.lower().startswith("utf")) except Exception: # in case someone overrides sys.stdout in an unsafe way unicode = False + mne_version_good = True for mi, mod_name in enumerate(use_mod_names): # upcoming break if mod_name == "": # break @@ -719,7 +739,16 @@ def sys_info(fid=None, show_paths=False, *, dependencies="user", unicode=True): except Exception: unavailable.append(mod_name) else: - out(f"{pre}☑ " if unicode else " + ") + mark = "☑" if unicode else "+" + mne_extra = "" + if mod_name == "mne" and check_version: + timeout = 2.0 if check_version is True else float(check_version) + mne_version_good, mne_extra = _check_mne_version(timeout) + if mne_version_good is None: + mne_version_good = True + elif not mne_version_good: + mark = "☒" if unicode else "X" + out(f"{pre}{mark} " if unicode else f" {mark} ") out(f"{mod_name}".ljust(ljust)) if mod_name == "vtk": vtk_version = mod.vtkVersion() @@ -747,6 +776,9 @@ def sys_info(fid=None, show_paths=False, *, dependencies="user", unicode=True): out(" (OpenGL unavailable)") else: out(f" (OpenGL {version} via {renderer})") + elif mod_name == "mne": + out(f" ({mne_extra})") + # Now comes stuff after the version if show_paths: if last: pre = " " @@ -756,3 +788,42 @@ def sys_info(fid=None, show_paths=False, *, dependencies="user", unicode=True): pre = " | " out(f'\n{pre}{" " * ljust}{op.dirname(mod.__file__)}') out("\n") + + if not mne_version_good: + out( + "\nTo update to the latest supported release version to get bugfixes and " + "improvements, visit " + "https://mne.tools/stable/install/updating.html\n" + ) + + +def _get_latest_version(timeout): + # Bandit complains about urlopen, but we know the URL here + url = "https://api.github.com/repos/mne-tools/mne-python/releases/latest" + try: + with urlopen(url, timeout=timeout) as f: # nosec + response = json.load(f) + except URLError as err: + # Triage error type + if "SSL" in str(err): + return "SSL error" + elif "timed out" in str(err): + return f"timeout after {timeout} sec" + else: + return f"unknown error: {str(err)}" + else: + return response["tag_name"].lstrip("v") or "version unknown" + + +def _check_mne_version(timeout): + rel_ver = _get_latest_version(timeout) + if not rel_ver[0].isnumeric(): + return None, (f"unable to check for latest version on GitHub, {rel_ver}") + rel_ver = parse(rel_ver) + this_ver = parse(import_module("mne").__version__) + if this_ver > rel_ver: + return True, f"devel, latest release is {rel_ver}" + if this_ver == rel_ver: + return True, "latest release" + else: + return False, f"outdated, release {rel_ver} is available!" diff --git a/mne/utils/tests/test_config.py b/mne/utils/tests/test_config.py index d29aa43feda..fe802734f67 100644 --- a/mne/utils/tests/test_config.py +++ b/mne/utils/tests/test_config.py @@ -1,15 +1,21 @@ import os import platform +import re +from functools import partial from pathlib import Path +from urllib.error import URLError import pytest +import mne +import mne.utils.config from mne.utils import ( ClosingStringIO, _get_stim_channel, get_config, get_config_path, get_subjects_dir, + requires_good_network, set_config, set_memmap_min_size, sys_info, @@ -95,7 +101,7 @@ def test_config(tmp_path): def test_sys_info(): """Test info-showing utility.""" out = ClosingStringIO() - sys_info(fid=out) + sys_info(fid=out, check_version=False) out = out.getvalue() assert "numpy" in out @@ -109,7 +115,7 @@ def test_sys_info_qt_browser(): """Test if mne_qt_browser is correctly detected.""" pytest.importorskip("mne_qt_browser") out = ClosingStringIO() - sys_info(fid=out) + sys_info(fid=out, check_version=False) out = out.getvalue() assert "mne-qt-browser" in out @@ -134,3 +140,67 @@ def test_get_subjects_dir(tmp_path, monkeypatch): monkeypatch.setenv("HOME", str(tmp_path)) monkeypatch.setenv("USERPROFILE", str(tmp_path)) # Windows assert str(get_subjects_dir("~/foo")) == str(subjects_dir) + + +@pytest.mark.slowtest +@requires_good_network +def test_sys_info_check_outdated(monkeypatch): + """Test sys info checking.""" + # Old (actually ping GitHub) + monkeypatch.setattr(mne, "__version__", "0.1") + out = ClosingStringIO() + sys_info(fid=out, check_version=10) + out = out.getvalue() + assert "(outdated, release " in out + assert "updating.html" in out + + # Timeout (will call urllib.open) + out = ClosingStringIO() + sys_info(fid=out, check_version=1e-12) + out = out.getvalue() + assert re.match(".*unable to check.*timeout.*", out, re.DOTALL) is not None + assert "updating.html" not in out + + +def test_sys_info_check_other(monkeypatch): + """Test other failure modes of the sys info check.""" + + def bad_open(url, timeout, msg): + raise URLError(msg) + + # SSL error + out = ClosingStringIO() + with monkeypatch.context() as m: + m.setattr(mne.utils.config, "urlopen", partial(bad_open, msg="SSL: CERT")) + sys_info(fid=out) + out = out.getvalue() + assert re.match(".*unable to check.*SSL.*", out, re.DOTALL) is not None + + # Other error + out = ClosingStringIO() + with monkeypatch.context() as m: + m.setattr(mne.utils.config, "urlopen", partial(bad_open, msg="foo bar")) + sys_info(fid=out) + out = out.getvalue() + match = re.match(".*unable to .*unknown error: .*foo bar.*", out, re.DOTALL) + assert match is not None + + # Match + monkeypatch.setattr( + mne.utils.config, + "_get_latest_version", + lambda timeout: "1.5.1", + ) + monkeypatch.setattr(mne, "__version__", "1.5.1") + out = ClosingStringIO() + sys_info(fid=out) + out = out.getvalue() + assert " 1.5.1 (latest release)" in out + + # Devel + monkeypatch.setattr(mne, "__version__", "1.6.dev0") + out = ClosingStringIO() + sys_info(fid=out) + out = out.getvalue() + assert "devel, " in out + assert "updating.html" not in out From 7ff8c586e4521d0d45833d715fd3055056aec505 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Mon, 30 Oct 2023 13:25:43 -0400 Subject: [PATCH 027/405] BUG: Fix bug with interior points not showing (#12148) --- doc/changes/devel.rst | 2 ++ mne/commands/mne_coreg.py | 7 +++--- mne/gui/_gui.py | 53 ++++++++++++++++++--------------------- mne/viz/_3d.py | 25 +++++------------- 4 files changed, 36 insertions(+), 51 deletions(-) diff --git a/doc/changes/devel.rst b/doc/changes/devel.rst index 3778dd6f6d6..ff59286dee2 100644 --- a/doc/changes/devel.rst +++ b/doc/changes/devel.rst @@ -65,6 +65,7 @@ Bugs - Fix bug with ``subject_info`` when loading data from and exporting to EDF file (:gh:`11952` by `Paul Roujansky`_) - Fix rendering glitches when plotting Neuromag/TRIUX sensors in :func:`mne.viz.plot_alignment` and related functions (:gh:`12098` by `Eric Larson`_) - Fix bug with delayed checking of :class:`info["bads"] ` (:gh:`12038` by `Eric Larson`_) +- Fix bug with :ref:`mne coreg` where points inside the head surface were not shown (:gh:`12147` by `Eric Larson`_) - Fix bug with :func:`mne.viz.plot_alignment` where ``sensor_colors`` were not handled properly on a per-channel-type basis (:gh:`12067` by `Eric Larson`_) - Fix handling of channel information in annotations when loading data from and exporting to EDF file (:gh:`11960` :gh:`12017` :gh:`12044` by `Paul Roujansky`_) - Add missing ``overwrite`` and ``verbose`` parameters to :meth:`Transform.save() ` (:gh:`12004` by `Marijn van Vliet`_) @@ -84,3 +85,4 @@ API changes - :func:`mne.io.kit.read_mrk` reading pickled files is deprecated using something like ``np.savetxt(fid, pts, delimiter="\t", newline="\n")`` to save your points instead (:gh:`11937` by `Eric Larson`_) - Replace legacy ``inst.pick_channels`` and ``inst.pick_types`` with ``inst.pick`` (where ``inst`` is an instance of :class:`~mne.io.Raw`, :class:`~mne.Epochs`, or :class:`~mne.Evoked`) wherever possible (:gh:`11907` by `Clemens Brunner`_) - The ``reset_camera`` parameter has been removed in favor of ``distance="auto"`` in :func:`mne.viz.set_3d_view`, :meth:`mne.viz.Brain.show_view`, and related functions (:gh:`12000` by `Eric Larson`_) +- Several unused parameters from :func:`mne.gui.coregistration` are now deprecated: tabbed, split, scrollable, head_inside, guess_mri_subject, scale, and ``advanced_rendering``. All arguments are also now keyword-only. (:gh:`12147` by `Eric Larson`_) diff --git a/mne/commands/mne_coreg.py b/mne/commands/mne_coreg.py index add4a65845b..ed440e987e3 100644 --- a/mne/commands/mne_coreg.py +++ b/mne/commands/mne_coreg.py @@ -44,7 +44,7 @@ def run(): "--tabbed", dest="tabbed", action="store_true", - default=False, + default=None, help="Option for small screens: Combine " "the data source panel and the coregistration panel " "into a single panel with tabs.", @@ -103,6 +103,7 @@ def run(): "--simple-rendering", action="store_false", dest="advanced_rendering", + default=None, help="Use simplified OpenGL rendering", ) _add_verbose_flag(parser) @@ -131,7 +132,7 @@ def run(): faulthandler.enable() mne.gui.coregistration( - options.tabbed, + tabbed=options.tabbed, inst=options.inst, subject=options.subject, subjects_dir=subjects_dir, @@ -139,7 +140,7 @@ def run(): head_opacity=options.head_opacity, head_high_res=head_high_res, trans=trans, - scrollable=True, + scrollable=None, interaction=options.interaction, scale=options.scale, advanced_rendering=options.advanced_rendering, diff --git a/mne/gui/_gui.py b/mne/gui/_gui.py index de6e35482b8..82d7591651d 100644 --- a/mne/gui/_gui.py +++ b/mne/gui/_gui.py @@ -7,8 +7,9 @@ @verbose def coregistration( - tabbed=False, - split=True, + *, + tabbed=None, + split=None, width=None, inst=None, subject=None, @@ -18,15 +19,14 @@ def coregistration( head_opacity=None, head_high_res=None, trans=None, - scrollable=True, - *, - orient_to_surface=True, - scale_by_distance=True, - mark_inside=True, + scrollable=None, + orient_to_surface=None, + scale_by_distance=None, + mark_inside=None, interaction=None, scale=None, advanced_rendering=None, - head_inside=True, + head_inside=None, fullscreen=None, show=True, block=False, @@ -143,10 +143,10 @@ def coregistration( .. youtube:: ALV5qqMHLlQ """ unsupported_params = { - "tabbed": (tabbed, False), - "split": (split, True), - "scrollable": (scrollable, True), - "head_inside": (head_inside, True), + "tabbed": tabbed, + "split": split, + "scrollable": scrollable, + "head_inside": head_inside, "guess_mri_subject": guess_mri_subject, "scale": scale, "advanced_rendering": advanced_rendering, @@ -158,22 +158,17 @@ def coregistration( to_raise = val is not None if to_raise: warn( - f"The parameter {key} is not supported with" - " the pyvistaqt 3d backend. It will be ignored." + f"The parameter {key} is deprecated and will be removed in 1.7, do " + "not pass a value for it", + FutureWarning, ) + del tabbed, split, scrollable, head_inside, guess_mri_subject, scale + del advanced_rendering config = get_config() - if guess_mri_subject is None: - guess_mri_subject = config.get("MNE_COREG_GUESS_MRI_SUBJECT", "true") == "true" if head_high_res is None: head_high_res = config.get("MNE_COREG_HEAD_HIGH_RES", "true") == "true" - if advanced_rendering is None: - advanced_rendering = ( - config.get("MNE_COREG_ADVANCED_RENDERING", "true") == "true" - ) if head_opacity is None: head_opacity = config.get("MNE_COREG_HEAD_OPACITY", 0.8) - if head_inside is None: - head_inside = config.get("MNE_COREG_HEAD_INSIDE", "true").lower() == "true" if width is None: width = config.get("MNE_COREG_WINDOW_WIDTH", 800) if height is None: @@ -183,23 +178,23 @@ def coregistration( subjects_dir = config["SUBJECTS_DIR"] elif "MNE_COREG_SUBJECTS_DIR" in config: subjects_dir = config["MNE_COREG_SUBJECTS_DIR"] + false_like = ("false", "0") if orient_to_surface is None: - orient_to_surface = config.get("MNE_COREG_ORIENT_TO_SURFACE", "") == "true" + orient_to_surface = config.get("MNE_COREG_ORIENT_TO_SURFACE", "true").lower() + orient_to_surface = orient_to_surface not in false_like if scale_by_distance is None: - scale_by_distance = config.get("MNE_COREG_SCALE_BY_DISTANCE", "") == "true" + scale_by_distance = config.get("MNE_COREG_SCALE_BY_DISTANCE", "true").lower() + scale_by_distance = scale_by_distance not in false_like if interaction is None: interaction = config.get("MNE_COREG_INTERACTION", "terrain") if mark_inside is None: - mark_inside = config.get("MNE_COREG_MARK_INSIDE", "") == "true" - if scale is None: - scale = config.get("MNE_COREG_SCENE_SCALE", 0.16) + mark_inside = config.get("MNE_COREG_MARK_INSIDE", "true").lower() + mark_inside = mark_inside not in false_like if fullscreen is None: fullscreen = config.get("MNE_COREG_FULLSCREEN", "") == "true" head_opacity = float(head_opacity) - head_inside = bool(head_inside) width = int(width) height = int(height) - scale = float(scale) from ..viz.backends.renderer import MNE_3D_BACKEND_TESTING from ._coreg import CoregistrationUI diff --git a/mne/viz/_3d.py b/mne/viz/_3d.py index 14edb396edf..ece5798b582 100644 --- a/mne/viz/_3d.py +++ b/mne/viz/_3d.py @@ -1252,10 +1252,9 @@ def _orient_glyphs( proj_pts, proj_nn = _get_nearest(nearest, check_inside, project_to_trans, proj_rr) vec = pts - proj_pts # point to the surface nn = proj_nn + scalars = np.ones(len(pts)) if mark_inside and not project_to_surface: - scalars = (~check_inside(proj_rr)).astype(int) - else: - scalars = np.ones(len(pts)) + scalars[:] = ~check_inside(proj_rr) dist = np.linalg.norm(vec, axis=-1, keepdims=True) vectors = (250 * dist + 1) * nn return scalars, vectors, proj_pts @@ -1277,28 +1276,16 @@ def _plot_glyphs( check_inside=None, nearest=None, ): + from matplotlib.colors import ListedColormap, to_rgba + + _validate_type(mark_inside, bool, "mark_inside") if surf is not None and len(loc) > 0: defaults = DEFAULTS["coreg"] scalars, vectors, proj_pts = _orient_glyphs( loc, surf, project_points, mark_inside, check_inside, nearest ) if mark_inside: - from matplotlib.colors import ListedColormap - - color = np.append(color, 1) - colormap = ListedColormap( - np.array( - [ - ( - 0, - 0, - 0, - 1, - ), - color, - ] - ) - ) + colormap = ListedColormap([to_rgba("darkslategray"), to_rgba(color)]) color = None clim = [0, 1] else: From 3e0e543089b4ad3a4b6b064f45749c86ebbaa081 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 30 Oct 2023 16:36:57 -0400 Subject: [PATCH 028/405] [pre-commit.ci] pre-commit autoupdate (#12155) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3ae6169dc76..c4ec4cd6c62 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -7,7 +7,7 @@ repos: # Ruff mne - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.1 + rev: v0.1.3 hooks: - id: ruff name: ruff mne @@ -16,7 +16,7 @@ repos: # Ruff tutorials and examples - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.1 + rev: v0.1.3 hooks: - id: ruff name: ruff tutorials and examples From b9cab3ceb4e68c979e8496dd9799ab242b951bdc Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Tue, 31 Oct 2023 11:45:10 -0400 Subject: [PATCH 029/405] ENH: Collapse only in doc gen (#12145) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Richard Höchenberger Co-authored-by: Daniel McCloy --- doc/changes/devel.rst | 1 + doc/conf.py | 2 + mne/_fiff/meas_info.py | 45 ++-- mne/_fiff/tests/test_meas_info.py | 6 +- mne/html_templates/_templates.py | 18 +- mne/html_templates/repr/evoked.html.jinja | 1 + mne/html_templates/repr/info.html.jinja | 239 +++++++++------------- mne/io/tests/test_raw.py | 2 +- mne/utils/tests/test_misc.py | 4 +- tutorials/intro/30_info.py | 4 +- 10 files changed, 142 insertions(+), 180 deletions(-) diff --git a/doc/changes/devel.rst b/doc/changes/devel.rst index ff59286dee2..20b0951bd40 100644 --- a/doc/changes/devel.rst +++ b/doc/changes/devel.rst @@ -63,6 +63,7 @@ Bugs - Fix bug with :meth:`~mne.viz.Brain.add_annotation` when reading an annotation from a file with both hemispheres shown (:gh:`11946` by `Marijn van Vliet`_) - Fix bug with axis clip box boundaries in :func:`mne.viz.plot_evoked_topo` and related functions (:gh:`11999` by `Eric Larson`_) - Fix bug with ``subject_info`` when loading data from and exporting to EDF file (:gh:`11952` by `Paul Roujansky`_) +- Fix bug where :class:`mne.Info` HTML representations listed all channel counts instead of good channel counts under the heading "Good channels" (:gh:`12145` by `Eric Larson`_) - Fix rendering glitches when plotting Neuromag/TRIUX sensors in :func:`mne.viz.plot_alignment` and related functions (:gh:`12098` by `Eric Larson`_) - Fix bug with delayed checking of :class:`info["bads"] ` (:gh:`12038` by `Eric Larson`_) - Fix bug with :ref:`mne coreg` where points inside the head surface were not shown (:gh:`12147` by `Eric Larson`_) diff --git a/doc/conf.py b/doc/conf.py index f9128227cd6..b07f3c50495 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -23,6 +23,7 @@ from numpydoc import docscrape import mne +import mne.html_templates._templates from mne.tests.test_docstring_parameters import error_ignores from mne.utils import ( linkcode_resolve, # noqa, analysis:ignore @@ -41,6 +42,7 @@ # https://numba.readthedocs.io/en/latest/reference/deprecation.html#deprecation-of-old-style-numba-captured-errors # noqa: E501 os.environ["NUMBA_CAPTURED_ERRORS"] = "new_style" sphinx_logger = sphinx.util.logging.getLogger("mne") +mne.html_templates._templates._COLLAPSED = True # collapse info _repr_html_ # -- Path setup -------------------------------------------------------------- diff --git a/mne/_fiff/meas_info.py b/mne/_fiff/meas_info.py index 7b74e8c0ead..d7f94461a4d 100644 --- a/mne/_fiff/meas_info.py +++ b/mne/_fiff/meas_info.py @@ -9,8 +9,7 @@ import datetime import operator import string -import uuid -from collections import Counter, OrderedDict +from collections import Counter, OrderedDict, defaultdict from collections.abc import Mapping from copy import deepcopy from io import BytesIO @@ -1852,37 +1851,25 @@ def _get_chs_for_repr(self): titles = _handle_default("titles") # good channels - channels = {} - ch_types = [channel_type(self, idx) for idx in range(len(self["chs"]))] - ch_counts = Counter(ch_types) - for ch_type, count in ch_counts.items(): - if ch_type == "meg": - channels["mag"] = len(pick_types(self, meg="mag")) - channels["grad"] = len(pick_types(self, meg="grad")) - elif ch_type == "eog": - pick_eog = pick_types(self, eog=True) - eog = ", ".join(np.array(self["ch_names"])[pick_eog]) - elif ch_type == "ecg": - pick_ecg = pick_types(self, ecg=True) - ecg = ", ".join(np.array(self["ch_names"])[pick_ecg]) - channels[ch_type] = count - + 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"{v} {titles.get(k, k.upper())}" for k, v in channels.items()] + [f"{len(v)} {titles.get(k, k.upper())}" for k, v in good_names.items()] ) - - if "ecg" not in channels.keys(): - ecg = "Not available" - if "eog" not in channels.keys(): - eog = "Not available" + 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 - if len(self["bads"]) > 0: - bad_channels = ", ".join(self["bads"]) - else: - bad_channels = "None" + bad_channels = ", ".join(self["bads"]) or "None" - return good_channels, bad_channels, ecg, eog + return good_channels, bad_channels, good_names["ecg"], good_names["eog"] @repr_html def _repr_html_(self, caption=None, duration=None, filenames=None): @@ -1918,10 +1905,8 @@ def _repr_html_(self, caption=None, duration=None, filenames=None): info_template = _get_html_template("repr", "info.html.jinja") sections = ("General", "Channels", "Data") - section_ids = [f"section_{str(uuid.uuid4())}" for _ in sections] return html + info_template.render( sections=sections, - section_ids=section_ids, caption=caption, meas_date=meas_date, projs=projs, diff --git a/mne/_fiff/tests/test_meas_info.py b/mne/_fiff/tests/test_meas_info.py index 5ecb9e62775..bcb8b96f8c8 100644 --- a/mne/_fiff/tests/test_meas_info.py +++ b/mne/_fiff/tests/test_meas_info.py @@ -899,11 +899,11 @@ def test_repr_html(): assert "EEG 053" in info._repr_html_() html = info._repr_html_() - for ch in [ - "204 Gradiometers", + for ch in [ # good channel counts + "203 Gradiometers", "102 Magnetometers", "9 Stimulus", - "60 EEG", + "59 EEG", "1 EOG", ]: assert ch in html diff --git a/mne/html_templates/_templates.py b/mne/html_templates/_templates.py index dff9c6b6c18..2ece5fea66f 100644 --- a/mne/html_templates/_templates.py +++ b/mne/html_templates/_templates.py @@ -1,5 +1,7 @@ import functools +_COLLAPSED = False # will override in doc build + @functools.lru_cache(maxsize=2) def _get_html_templates_env(kind): @@ -19,4 +21,18 @@ def _get_html_templates_env(kind): def _get_html_template(kind, name): - return _get_html_templates_env(kind).get_template(name) + return _RenderWrap( + _get_html_templates_env(kind).get_template(name), + collapsed=_COLLAPSED, + ) + + +class _RenderWrap: + """Class that allows functools.partial-like wrapping of jinja2 Template.render().""" + + def __init__(self, template, **kwargs): + self._template = template + self._kwargs = kwargs + + def render(self, *args, **kwargs): + return self._template.render(*args, **kwargs, **self._kwargs) diff --git a/mne/html_templates/repr/evoked.html.jinja b/mne/html_templates/repr/evoked.html.jinja index cd3c471b3d0..bb9ef0e5f97 100644 --- a/mne/html_templates/repr/evoked.html.jinja +++ b/mne/html_templates/repr/evoked.html.jinja @@ -11,6 +11,7 @@ Timepoints {{ evoked.data.shape[1] }} samples + Channels {{ evoked.data.shape[0] }} channels diff --git a/mne/html_templates/repr/info.html.jinja b/mne/html_templates/repr/info.html.jinja index f6d46c49f34..5b787cbfe31 100644 --- a/mne/html_templates/repr/info.html.jinja +++ b/mne/html_templates/repr/info.html.jinja @@ -1,144 +1,101 @@ - - - - - - - - - - - - - {% 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 %} + + {{sections[0]}} +
- -
Measurement date{{ meas_date }}Unknown
Experimenter{{ experimenter }}Unknown
Participant
+ + + {% 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 %} - - - - - - - {% if dig is not none %} - - {% else %} - - {% endif %} - - - - - - - - - - - - - - - - - - - - - {% 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 %} -
Measurement date{{ meas_date }}Unknown
Experimenter{{ experimenter }}Unknown
Participant{{ subject_info['his_id'] }}Unknown
- -
Digitized points{{ dig|length }} pointsNot available
Good channels{{ good_channels }}
Bad channels{{ bad_channels }}
EOG channels{{ eog }}
ECG channels{{ ecg }}
- -
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)
+ {% else %} + Unknown + {% endif %} + + + + + {{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)
+ diff --git a/mne/io/tests/test_raw.py b/mne/io/tests/test_raw.py index 362fb293fdf..5cc017588e3 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.info._repr_html_() + assert "Good channels" in raw._repr_html_() # test resetting raw if test_kwargs: diff --git a/mne/utils/tests/test_misc.py b/mne/utils/tests/test_misc.py index aca5efa5fbc..6892d561777 100644 --- a/mne/utils/tests/test_misc.py +++ b/mne/utils/tests/test_misc.py @@ -24,8 +24,8 @@ def test_html_repr(): os.environ[key] = "True" # HTML repr on info = mne.create_info(10, 256) r = info._repr_html_() - assert r.startswith("") + assert r.startswith("
") os.environ[key] = "False" # HTML repr off r = info._repr_html_() assert r.startswith("
")
diff --git a/tutorials/intro/30_info.py b/tutorials/intro/30_info.py
index 2de72747528..2df1c17e87b 100644
--- a/tutorials/intro/30_info.py
+++ b/tutorials/intro/30_info.py
@@ -5,8 +5,8 @@
 The Info data structure
 =======================
 
-This tutorial describes the :class:`mne.Info` data structure, which keeps track
-of various recording details, and is attached to :class:`~mne.io.Raw`,
+This tutorial describes the :class:`mne.Info` data structure, which keeps track of
+various recording details, and is attached to :class:`~mne.io.Raw`,
 :class:`~mne.Epochs`, and :class:`~mne.Evoked` objects.
 
 We will begin by loading the Python modules we need, and loading the same

From 60db738c20fbfc84cb5468252742610522974aac Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Richard=20H=C3=B6chenberger?=
 
Date: Tue, 31 Oct 2023 19:08:40 +0100
Subject: [PATCH 030/405] Allow automated metadata generation to be bounded by
 "row events" instead of explicit time windows (#12118)

---
 doc/changes/devel.rst    |  1 +
 mne/epochs.py            | 92 +++++++++++++++++++++++++++++++---------
 mne/tests/test_epochs.py | 91 ++++++++++++++++++++++++++++++++++++---
 3 files changed, 159 insertions(+), 25 deletions(-)

diff --git a/doc/changes/devel.rst b/doc/changes/devel.rst
index 20b0951bd40..524e2d252ca 100644
--- a/doc/changes/devel.rst
+++ b/doc/changes/devel.rst
@@ -43,6 +43,7 @@ Enhancements
 - By default MNE-Python creates matplotlib figures with ``layout='constrained'`` rather than the default ``layout='tight'`` (:gh:`12050`, :gh:`12103` by `Mathieu Scheltienne`_ and `Eric Larson`_)
 - Enhance :func:`~mne.viz.plot_evoked_field` with a GUI that has controls for time, colormap, and contour lines (:gh:`11942` by `Marijn van Vliet`_)
 - Add :class:`mne.viz.ui_events.UIEvent` linking for interactive colorbars, allowing users to link figures and change the colormap and limits interactively. This supports :func:`~mne.viz.plot_evoked_topomap`, :func:`~mne.viz.plot_ica_components`, :func:`~mne.viz.plot_tfr_topomap`, :func:`~mne.viz.plot_projs_topomap`, :meth:`~mne.Evoked.plot_image`, and :meth:`~mne.Epochs.plot_image` (:gh:`12057` by `Santeri Ruuskanen`_)
+- :func:`~mne.epochs.make_metadata` now accepts ``tmin=None`` and ``tmax=None``, which will bound the time window used for metadata generation by event names (instead of a fixed time). That way, you can now for example generate metadata spanning from one cue or fixation cross to the next, even if trial durations vary throughout the recording (:gh:`12118` by `Richard Höchenberger`_)
 
 Bugs
 ~~~~
diff --git a/mne/epochs.py b/mne/epochs.py
index 459a4ce3460..510161f99bc 100644
--- a/mne/epochs.py
+++ b/mne/epochs.py
@@ -2664,17 +2664,18 @@ def make_metadata(
     keep_first=None,
     keep_last=None,
 ):
-    """Generate metadata from events for use with `mne.Epochs`.
+    """Automatically generate metadata for use with `mne.Epochs` from events.
 
     This function mimics the epoching process (it constructs time windows
     around time-locked "events of interest") and collates information about
     any other events that occurred within those time windows. The information
-    is returned as a :class:`pandas.DataFrame` suitable for use as
+    is returned as a :class:`pandas.DataFrame`, suitable for use as
     `~mne.Epochs` metadata: one row per time-locked event, and columns
-    indicating presence/absence and latency of each ancillary event type.
+    indicating presence or absence and latency of each ancillary event type.
 
     The function will also return a new ``events`` array and ``event_id``
-    dictionary that correspond to the generated metadata.
+    dictionary that correspond to the generated metadata, which together can then be
+    readily fed into `~mne.Epochs`.
 
     Parameters
     ----------
@@ -2687,9 +2688,9 @@ def make_metadata(
         A mapping from event names (keys) to event IDs (values). The event
         names will be incorporated as columns of the returned metadata
         :class:`~pandas.DataFrame`.
-    tmin, tmax : float
-        Start and end of the time interval for metadata generation in seconds,
-        relative to the time-locked event of the respective time window.
+    tmin, tmax : float | None
+        Start and end of the time interval for metadata generation in seconds, relative
+        to the time-locked event of the respective time window (the "row events").
 
         .. note::
            If you are planning to attach the generated metadata to
@@ -2697,15 +2698,27 @@ def make_metadata(
            your epochs time interval, pass the same ``tmin`` and ``tmax``
            values here as you use for your epochs.
 
+        If ``None``, the time window used for metadata generation is bounded by the
+        ``row_events``. This is can be particularly practical if trial duration varies
+        greatly, but each trial starts with a known event (e.g., a visual cue or
+        fixation).
+
+        .. note::
+           If ``tmin=None``, the first time window for metadata generation starts with
+           the first row event. If ``tmax=None``, the last time window for metadata
+           generation ends with the last event in ``events``.
+
+        .. versionchanged:: 1.6.0
+           Added support for ``None``.
     sfreq : float
         The sampling frequency of the data from which the events array was
         extracted.
     row_events : list of str | str | None
-        Event types around which to create the time windows / for which to
-        create **rows** in the returned metadata :class:`pandas.DataFrame`. If
-        provided, the string(s) must be keys of ``event_id``. If ``None``
-        (default), rows are created for **all** event types present in
-        ``event_id``.
+        Event types around which to create the time windows. For each of these
+        time-locked events, we will create a **row** in the returned metadata
+        :class:`pandas.DataFrame`. If provided, the string(s) must be keys of
+        ``event_id``. If ``None`` (default), rows are created for **all** event types
+        present in ``event_id``.
     keep_first : str | list of str | None
         Specify subsets of :term:`hierarchical event descriptors` (HEDs,
         inspired by :footcite:`BigdelyShamloEtAl2013`) matching events of which
@@ -2780,8 +2793,10 @@ def make_metadata(
     The time window used for metadata generation need not correspond to the
     time window used to create the `~mne.Epochs`, to which the metadata will
     be attached; it may well be much shorter or longer, or not overlap at all,
-    if desired. The can be useful, for example, to include events that occurred
-    before or after an epoch, e.g. during the inter-trial interval.
+    if desired. This can be useful, for example, to include events that
+    occurred before or after an epoch, e.g. during the inter-trial interval.
+    If either ``tmin``, ``tmax``, or both are ``None``, the time window will
+    typically vary, too.
 
     .. versionadded:: 0.23
 
@@ -2791,7 +2806,11 @@ def make_metadata(
     """
     pd = _check_pandas_installed()
 
+    _validate_type(events, types=("array-like",), item_name="events")
     _validate_type(event_id, types=(dict,), item_name="event_id")
+    _validate_type(sfreq, types=("numeric",), item_name="sfreq")
+    _validate_type(tmin, types=("numeric", None), item_name="tmin")
+    _validate_type(tmax, types=("numeric", None), item_name="tmax")
     _validate_type(row_events, types=(None, str, list, tuple), item_name="row_events")
     _validate_type(keep_first, types=(None, str, list, tuple), item_name="keep_first")
     _validate_type(keep_last, types=(None, str, list, tuple), item_name="keep_last")
@@ -2840,8 +2859,8 @@ def _ensure_list(x):
 
     # First and last sample of each epoch, relative to the time-locked event
     # This follows the approach taken in mne.Epochs
-    start_sample = int(round(tmin * sfreq))
-    stop_sample = int(round(tmax * sfreq)) + 1
+    start_sample = None if tmin is None else int(round(tmin * sfreq))
+    stop_sample = None if tmax is None else int(round(tmax * sfreq)) + 1
 
     # Make indexing easier
     # We create the DataFrame before subsetting the events so we end up with
@@ -2887,16 +2906,49 @@ def _ensure_list(x):
     start_idx = stop_idx
     metadata.iloc[:, start_idx:] = None
 
-    # We're all set, let's iterate over all eventns and fill in in the
+    # We're all set, let's iterate over all events and fill in in the
     # respective cells in the metadata. We will subset this to include only
     # `row_events` later
     for row_event in events_df.itertuples(name="RowEvent"):
         row_idx = row_event.Index
         metadata.loc[row_idx, "event_name"] = id_to_name_map[row_event.id]
 
-        # Determine which events fall into the current epoch
-        window_start_sample = row_event.sample + start_sample
-        window_stop_sample = row_event.sample + stop_sample
+        # Determine which events fall into the current time window
+        if start_sample is None:
+            # Lower bound is the current event.
+            window_start_sample = row_event.sample
+        else:
+            # Lower bound is determined by tmin.
+            window_start_sample = row_event.sample + start_sample
+
+        if stop_sample is None:
+            # Upper bound: next event of the same type, or the last event (of
+            # any type) if no later event of the same type can be found.
+            next_events = events_df.loc[
+                (events_df["sample"] > row_event.sample),
+                :,
+            ]
+            if next_events.size == 0:
+                # We've reached the last event in the recording.
+                window_stop_sample = row_event.sample
+            elif next_events.loc[next_events["id"] == row_event.id, :].size > 0:
+                # There's still an event of the same type appearing after the
+                # current event. Stop one sample short, we don't want to include that
+                # last event here, but in the next iteration.
+                window_stop_sample = (
+                    next_events.loc[next_events["id"] == row_event.id, :].iloc[0][
+                        "sample"
+                    ]
+                    - 1
+                )
+            else:
+                # There are still events after the current one, but not of the
+                # same type.
+                window_stop_sample = next_events.iloc[-1]["sample"]
+        else:
+            # Upper bound is determined by tmax.
+            window_stop_sample = row_event.sample + stop_sample
+
         events_in_window = events_df.loc[
             (events_df["sample"] >= window_start_sample)
             & (events_df["sample"] <= window_stop_sample),
diff --git a/mne/tests/test_epochs.py b/mne/tests/test_epochs.py
index 9f72be1803a..423fe556365 100644
--- a/mne/tests/test_epochs.py
+++ b/mne/tests/test_epochs.py
@@ -3914,29 +3914,36 @@ def assert_metadata_equal(got, exp):
 
 
 @pytest.mark.parametrize(
-    ("all_event_id", "row_events", "keep_first", "keep_last"),
+    ("all_event_id", "row_events", "tmin", "tmax", "keep_first", "keep_last"),
     [
         (
             {"a/1": 1, "a/2": 2, "b/1": 3, "b/2": 4, "c": 32},  # all events
             None,
+            -0.5,
+            1.5,
             None,
             None,
         ),
-        ({"a/1": 1, "a/2": 2}, None, None, None),  # subset of events
-        (dict(), None, None, None),  # empty set of events
+        ({"a/1": 1, "a/2": 2}, None, -0.5, 1.5, None, None),  # subset of events
+        (dict(), None, -0.5, 1.5, None, None),  # empty set of events
         (
             {"a/1": 1, "a/2": 2, "b/1": 3, "b/2": 4, "c": 32},
             ("a/1", "a/2", "b/1", "b/2"),
+            -0.5,
+            1.5,
             ("a", "b"),
             "c",
         ),
+        # Test when tmin, tmax are None
+        ({"a/1": 1, "a/2": 2}, None, None, 1.5, None, None),  # tmin is None
+        ({"a/1": 1, "a/2": 2}, None, -0.5, None, None, None),  # tmax is None
+        ({"a/1": 1, "a/2": 2}, None, None, None, None, None),  # tmin and tmax are None
     ],
 )
-def test_make_metadata(all_event_id, row_events, keep_first, keep_last):
+def test_make_metadata(all_event_id, row_events, tmin, tmax, keep_first, keep_last):
     """Test that make_metadata works."""
     pytest.importorskip("pandas")
     raw, all_events, _ = _get_data()
-    tmin, tmax = -0.5, 1.5
     sfreq = raw.info["sfreq"]
     kwargs = dict(
         events=all_events,
@@ -4005,6 +4012,80 @@ def test_make_metadata(all_event_id, row_events, keep_first, keep_last):
     Epochs(raw, events=events, event_id=event_id, metadata=metadata, verbose="warning")
 
 
+def test_make_metadata_bounded_by_row_events():
+    """Test make_metadata() with tmin, tmax set to None."""
+    pytest.importorskip("pandas")
+
+    sfreq = 100
+    duration = 15
+    n_chs = 10
+
+    # Define events and generate annotations
+    experimental_events = [
+        # Beginning of recording until response (1st trial)
+        {"onset": 0.0, "description": "rec_start", "duration": 1 / sfreq},
+        {"onset": 1.0, "description": "cue", "duration": 1 / sfreq},
+        {"onset": 2.0, "description": "stim", "duration": 1 / sfreq},
+        {"onset": 2.5, "description": "resp", "duration": 1 / sfreq},
+        # 2nd trial
+        {"onset": 4.0, "description": "cue", "duration": 1 / sfreq},
+        {"onset": 4.3, "description": "stim", "duration": 1 / sfreq},
+        {"onset": 8.0, "description": "resp", "duration": 1 / sfreq},
+        # 3rd trial until end of the recording
+        {"onset": 10.0, "description": "cue", "duration": 1 / sfreq},
+        {"onset": 12.0, "description": "stim", "duration": 1 / sfreq},
+        {"onset": 13.0, "description": "resp", "duration": 1 / sfreq},
+        {"onset": 14.9, "description": "rec_end", "duration": 1 / sfreq},
+    ]
+
+    annots = mne.Annotations(
+        onset=[e["onset"] for e in experimental_events],
+        description=[e["description"] for e in experimental_events],
+        duration=[e["duration"] for e in experimental_events],
+    )
+
+    # Generate raw data, attach the annotations, and convert to events
+    rng = np.random.default_rng()
+    data = 1e-5 * rng.standard_normal((n_chs, sfreq * duration))
+    info = mne.create_info(
+        ch_names=[f"EEG {i}" for i in range(n_chs)], sfreq=sfreq, ch_types="eeg"
+    )
+
+    raw = mne.io.RawArray(data=data, info=info)
+    raw.set_annotations(annots)
+    events, event_id = mne.events_from_annotations(raw=raw)
+
+    metadata, events_new, event_id_new = mne.epochs.make_metadata(
+        events=events,
+        event_id=event_id,
+        tmin=None,
+        tmax=None,
+        sfreq=raw.info["sfreq"],
+        row_events="cue",
+    )
+
+    # We should have 3 rows in the metadata table in total.
+    # rec_start occurred before the first row_event, so should not be included
+    # rec_end occurred after the last row_event and should be included
+
+    assert len(metadata) == 3
+    assert (metadata["event_name"] == "cue").all()
+    assert (metadata["cue"] == 0.0).all()
+
+    for row in metadata.itertuples():
+        assert row.cue < row.stim < row.resp
+        assert np.isnan(row.rec_start)
+
+    # Beginning of recording until end of 1st trial
+    assert np.isnan(metadata.iloc[0]["rec_end"])
+
+    # 2nd trial
+    assert np.isnan(metadata.iloc[1]["rec_end"])
+
+    # 3rd trial until end of the recording
+    assert metadata.iloc[2]["resp"] < metadata.iloc[2]["rec_end"]
+
+
 def test_events_list():
     """Test that events can be a list."""
     events = [[100, 0, 1], [200, 0, 1], [300, 0, 1]]

From e4a6eba5d0a4c5f92b904207c33252f1979043db Mon Sep 17 00:00:00 2001
From: nordme <38704848+nordme@users.noreply.github.com>
Date: Wed, 1 Nov 2023 11:52:05 -0700
Subject: [PATCH 031/405] ENH: Add multiple label support to
 source_band_induced_power, source_induced_power (#12026)

Co-authored-by: Eric Larson 
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Daniel McCloy 
---
 azure-pipelines.yml                           |   2 +-
 doc/changes/devel.rst                         |   2 +
 .../source_label_time_frequency.py            |  48 +++-
 mne/commands/tests/test_commands.py           |   1 +
 mne/minimum_norm/tests/test_time_frequency.py | 141 ++++++++++-
 mne/minimum_norm/time_frequency.py            | 221 ++++++++++++++++--
 mne/source_space/tests/test_source_space.py   |   2 +
 mne/tests/test_dipole.py                      |   2 +
 mne/tests/test_surface.py                     |   1 +
 9 files changed, 397 insertions(+), 23 deletions(-)

diff --git a/azure-pipelines.yml b/azure-pipelines.yml
index 396cfe956b2..1b8ddc505a4 100644
--- a/azure-pipelines.yml
+++ b/azure-pipelines.yml
@@ -108,7 +108,7 @@ stages:
           - bash: |
               set -e
               python -m pip install --progress-bar off --upgrade pip setuptools wheel
-              python -m pip install --progress-bar off "mne-qt-browser[opengl] @ git+https://github.com/mne-tools/mne-qt-browser.git@main" pyvista scikit-learn pytest-error-for-skips python-picard "PyQt6!=6.5.1" qtpy
+              python -m pip install --progress-bar off "mne-qt-browser[opengl] @ git+https://github.com/mne-tools/mne-qt-browser.git@main" pyvista scikit-learn pytest-error-for-skips python-picard "PyQt6!=6.5.1" qtpy nibabel
               python -m pip uninstall -yq mne
               python -m pip install --progress-bar off --upgrade -e .[test]
             displayName: 'Install dependencies with pip'
diff --git a/doc/changes/devel.rst b/doc/changes/devel.rst
index 524e2d252ca..c800e6c538c 100644
--- a/doc/changes/devel.rst
+++ b/doc/changes/devel.rst
@@ -44,6 +44,8 @@ Enhancements
 - Enhance :func:`~mne.viz.plot_evoked_field` with a GUI that has controls for time, colormap, and contour lines (:gh:`11942` by `Marijn van Vliet`_)
 - Add :class:`mne.viz.ui_events.UIEvent` linking for interactive colorbars, allowing users to link figures and change the colormap and limits interactively. This supports :func:`~mne.viz.plot_evoked_topomap`, :func:`~mne.viz.plot_ica_components`, :func:`~mne.viz.plot_tfr_topomap`, :func:`~mne.viz.plot_projs_topomap`, :meth:`~mne.Evoked.plot_image`, and :meth:`~mne.Epochs.plot_image` (:gh:`12057` by `Santeri Ruuskanen`_)
 - :func:`~mne.epochs.make_metadata` now accepts ``tmin=None`` and ``tmax=None``, which will bound the time window used for metadata generation by event names (instead of a fixed time). That way, you can now for example generate metadata spanning from one cue or fixation cross to the next, even if trial durations vary throughout the recording (:gh:`12118` by `Richard Höchenberger`_)
+- Add support for passing multiple labels to :func:`mne.minimum_norm.source_induced_power` (:gh:`12026` by `Erica Peterson`_, `Eric Larson`_, and `Daniel McCloy`_ )
+
 
 Bugs
 ~~~~
diff --git a/examples/time_frequency/source_label_time_frequency.py b/examples/time_frequency/source_label_time_frequency.py
index f88d1ce2c50..a9c32934e38 100644
--- a/examples/time_frequency/source_label_time_frequency.py
+++ b/examples/time_frequency/source_label_time_frequency.py
@@ -35,8 +35,8 @@
 meg_path = data_path / "MEG" / "sample"
 raw_fname = meg_path / "sample_audvis_raw.fif"
 fname_inv = meg_path / "sample_audvis-meg-oct-6-meg-inv.fif"
-label_name = "Aud-rh"
-fname_label = meg_path / "labels" / f"{label_name}.label"
+label_names = ["Aud-lh", "Aud-rh"]
+fname_labels = [meg_path / "labels" / f"{ln}.label" for ln in label_names]
 
 tmin, tmax, event_id = -0.2, 0.5, 2
 
@@ -70,7 +70,8 @@
 # Compute a source estimate per frequency band including and excluding the
 # evoked response
 freqs = np.arange(7, 30, 2)  # define frequencies of interest
-label = mne.read_label(fname_label)
+labels = [mne.read_label(fl) for fl in fname_labels]
+label = labels[0]
 n_cycles = freqs / 3.0  # different number of cycle per frequency
 
 # subtract the evoked response in order to exclude evoked activity
@@ -122,3 +123,44 @@
     )
     ax.set(xlabel="Time (s)", ylabel="Frequency (Hz)", title=f"ITC ({title})")
     fig.colorbar(ax.images[0], ax=axes[ii])
+
+# %%
+
+##############################################################################
+# In the example above, we averaged power across vertices after calculating
+# power because we provided a single label for power calculation and therefore
+# power of all sources within the single label were returned separately. When
+# we provide a list of labels, power is averaged across sources within each
+# label automatically. With a list of labels, averaging is performed before
+# rescaling, so choose a baseline method appropriately.
+
+
+# Get power from multiple labels
+multi_label_power = source_induced_power(
+    epochs,
+    inverse_operator,
+    freqs,
+    labels,
+    baseline=(-0.1, 0),
+    baseline_mode="mean",
+    n_cycles=n_cycles,
+    n_jobs=None,
+    return_plv=False,
+)
+
+# visually compare evoked power in left and right auditory regions
+fig, axes = plt.subplots(ncols=2, layout="constrained")
+for l_idx, l_power in enumerate(multi_label_power):
+    ax = axes[l_idx]
+    ax.imshow(
+        l_power,
+        extent=[epochs.times[0], epochs.times[-1], freqs[0], freqs[-1]],
+        aspect="auto",
+        origin="lower",
+        vmin=multi_label_power.min(),
+        vmax=multi_label_power.max(),
+        cmap="RdBu_r",
+    )
+    title = f"{labels[l_idx].hemi.upper()} Evoked Power"
+    ax.set(xlabel="Time (s)", ylabel="Frequency (Hz)", title=title)
+    fig.colorbar(ax.images[0], ax=ax)
diff --git a/mne/commands/tests/test_commands.py b/mne/commands/tests/test_commands.py
index ba7693237b9..b2a93abfa96 100644
--- a/mne/commands/tests/test_commands.py
+++ b/mne/commands/tests/test_commands.py
@@ -319,6 +319,7 @@ def test_watershed_bem(tmp_path):
 @testing.requires_testing_data
 def test_flash_bem(tmp_path):
     """Test mne flash_bem."""
+    pytest.importorskip("nibabel")
     check_usage(mne_flash_bem, force_help=True)
     # Copy necessary files to tempdir
     tempdir = Path(str(tmp_path))
diff --git a/mne/minimum_norm/tests/test_time_frequency.py b/mne/minimum_norm/tests/test_time_frequency.py
index e581b4ae694..7072faeda9d 100644
--- a/mne/minimum_norm/tests/test_time_frequency.py
+++ b/mne/minimum_norm/tests/test_time_frequency.py
@@ -6,7 +6,7 @@
 from mne._fiff.constants import FIFF
 from mne.datasets import testing
 from mne.io import read_raw_fif
-from mne.label import read_label
+from mne.label import BiHemiLabel, read_label
 from mne.minimum_norm import (
     INVERSE_METHODS,
     apply_inverse_epochs,
@@ -27,6 +27,7 @@
 )
 fname_data = data_path / "MEG" / "sample" / "sample_audvis_trunc_raw.fif"
 fname_label = data_path / "MEG" / "sample" / "labels" / "Aud-lh.label"
+fname_label2 = data_path / "MEG" / "sample" / "labels" / "Aud-rh.label"
 
 
 @testing.requires_testing_data
@@ -129,9 +130,141 @@ def test_tfr_with_inverse_operator(method):
         method=method,
         prepared=True,
     )
+    assert power.shape == phase_lock.shape
     assert np.all(phase_lock > 0)
     assert np.all(phase_lock <= 1)
     assert 5 < np.max(power) < 7
+    # fairly precise spot check that our values match what we had on 2023/09/28
+    if method != "eLORETA":
+        # check phase-lock using arbitrary index value since pl max is 1
+        assert_allclose(phase_lock[1, 0, 0], 0.576, rtol=1e-3)
+        # check power
+        max_inds = np.unravel_index(np.argmax(power), power.shape)
+        assert_allclose(max_inds, [0, 11, 135])
+        assert_allclose(power[max_inds], 6.05, rtol=1e-3)
+
+
+@testing.requires_testing_data
+def test_tfr_multi_label():
+    """Test multi-label functionality."""
+    tmin, tmax, event_id = -0.2, 0.5, 1
+
+    # Setup for reading the raw data
+    raw = read_raw_fif(fname_data)
+    events = find_events(raw, stim_channel="STI 014")
+    inv = read_inverse_operator(fname_inv)
+    inv = prepare_inverse_operator(inv, nave=1, lambda2=1.0 / 9.0, method="dSPM")
+
+    raw.info["bads"] += ["MEG 2443", "EEG 053"]  # bads + 2 more
+
+    # picks MEG gradiometers
+    picks = pick_types(
+        raw.info, meg=True, eeg=False, eog=True, stim=False, exclude="bads"
+    )
+
+    # Load condition 1
+    event_id = 1
+    epochs = Epochs(
+        raw,
+        events[:3],  # take 3 events to keep the computation time low
+        event_id,
+        tmin,
+        tmax,
+        picks=picks,
+        baseline=(None, 0),
+        reject=dict(grad=4000e-13, eog=150e-6),
+        preload=True,
+    )
+
+    freqs = np.arange(7, 30, 2)
+
+    n_times = len(epochs.times)
+    n_freqs = len(freqs)
+
+    # prepare labels
+    label = read_label(fname_label)  # lh Aud
+    label2 = read_label(fname_label2)  # rh Aud
+    labels = [label, label2]
+    bad_lab = label.copy()
+    bad_lab.vertices = np.hstack((label.vertices, [2121]))  # add 1 unique vert
+    bad_lbls = [label, bad_lab]
+    nverts_lh = len(np.intersect1d(inv["src"][0]["vertno"], label.vertices))
+    nverts_rh = len(np.intersect1d(inv["src"][1]["vertno"], label2.vertices))
+    assert nverts_lh + 1 == nverts_rh == 3
+
+    # prepare instances of BiHemiLabel
+    fname_lvis = data_path / "MEG" / "sample" / "labels" / "Vis-lh.label"
+    fname_rvis = data_path / "MEG" / "sample" / "labels" / "Vis-rh.label"
+    lvis = read_label(fname_lvis)
+    rvis = read_label(fname_rvis)
+    bihl = BiHemiLabel(lh=label, rh=label2)  # auditory labels
+    bihl.name = "Aud"
+    bihl2 = BiHemiLabel(lh=lvis, rh=rvis)  # visual labels
+    bihl2.name = "Vis"
+    bihls = [bihl, bihl2]
+    bad_bihl = BiHemiLabel(lh=bad_lab, rh=rvis)  # 1 unique vert on lh, rh ok
+    bad_bihls = [bihl, bad_bihl]
+    print("BiHemi label verts:", bihl.lh.vertices.shape, bihl.rh.vertices.shape)
+
+    # check error handling
+    sip_kwargs = dict(
+        baseline=(-0.1, 0),
+        baseline_mode="mean",
+        n_cycles=2,
+        n_jobs=None,
+        return_plv=False,
+        method="dSPM",
+        prepared=True,
+    )
+    # label input errors
+    with pytest.raises(TypeError, match="Label or BiHemi"):
+        source_induced_power(epochs, inv, freqs, label="bad_input", **sip_kwargs)
+    with pytest.raises(TypeError, match="Label or BiHemi"):
+        source_induced_power(
+            epochs, inv, freqs, label=[label, "bad_input"], **sip_kwargs
+        )
+
+    # error handling for multi-label and plv
+    sip_kwargs_bad = sip_kwargs.copy()
+    sip_kwargs_bad["return_plv"] = True
+    with pytest.raises(RuntimeError, match="value cannot be calculated"):
+        source_induced_power(epochs, inv, freqs, labels, **sip_kwargs_bad)
+
+    # check multi-label handling
+    label_sets = dict(Label=(labels, bad_lbls), BiHemi=(bihls, bad_bihls))
+    for ltype, lab_set in label_sets.items():
+        n_verts = nverts_lh if ltype == "Label" else nverts_lh + nverts_rh
+        # check overlapping verts error handling
+        with pytest.raises(RuntimeError, match="overlapping vertices"):
+            source_induced_power(epochs, inv, freqs, lab_set[1], **sip_kwargs)
+
+        # TODO someday, eliminate both levels of this nested for-loop and use
+        # pytest.mark.parametrize, but not unless/until the data IO and the loading /
+        # preparing of the inverse operator have been made into fixtures (the overhead
+        # of those operations makes it a bad idea to parametrize now)
+        for ori in (None, "normal"):  # check loose and normal orientations
+            sip_kwargs.update(pick_ori=ori)
+            lbl = lab_set[0][0]
+
+            # check label=Label vs label=[Label]
+            no_list_pow = source_induced_power(
+                epochs, inv, freqs, label=lbl, **sip_kwargs
+            )
+            assert no_list_pow.shape == (n_verts, n_freqs, n_times)
+
+            list_pow = source_induced_power(
+                epochs, inv, freqs, label=[lbl], **sip_kwargs
+            )
+            assert list_pow.shape == (1, n_freqs, n_times)
+
+            nlp_ave = np.mean(no_list_pow, axis=0)
+            assert_allclose(nlp_ave, list_pow[0], rtol=1e-3)
+
+            # check label=[Label1, Label2]
+            multi_lab_pow = source_induced_power(
+                epochs, inv, freqs, label=lab_set[0], **sip_kwargs
+            )
+            assert multi_lab_pow.shape == (2, n_freqs, n_times)
 
 
 @testing.requires_testing_data
@@ -205,6 +338,7 @@ def test_source_psd_epochs(method):
     raw = read_raw_fif(fname_data)
     inverse_operator = read_inverse_operator(fname_inv)
     label = read_label(fname_label)
+    label2 = read_label(fname_label2)
 
     event_id, tmin, tmax = 1, -0.2, 0.5
     lambda2 = 1.0 / 9.0
@@ -242,6 +376,7 @@ def test_source_psd_epochs(method):
     inv = prepare_inverse_operator(
         inverse_operator, nave=1, lambda2=1.0 / 9.0, method="dSPM"
     )
+
     # return list
     stc_psd = compute_source_psd_epochs(
         one_epochs,
@@ -311,3 +446,7 @@ def test_source_psd_epochs(method):
             return_generator=False,
             prepared=True,
         )
+
+    # check error handling for label
+    with pytest.raises(TypeError, match="Label or BiHemi"):
+        compute_source_psd_epochs(one_epochs, inv, label=[label, label2])
diff --git a/mne/minimum_norm/time_frequency.py b/mne/minimum_norm/time_frequency.py
index 3c037c67085..f9f5571ae9b 100644
--- a/mne/minimum_norm/time_frequency.py
+++ b/mne/minimum_norm/time_frequency.py
@@ -12,6 +12,7 @@
 from ..event import make_fixed_length_events
 from ..evoked import EvokedArray
 from ..fixes import _safe_svd
+from ..label import BiHemiLabel, Label
 from ..parallel import parallel_func
 from ..source_estimate import _make_stc
 from ..time_frequency.multitaper import (
@@ -21,7 +22,7 @@
     _psd_from_mt_adaptive,
 )
 from ..time_frequency.tfr import cwt, morlet
-from ..utils import ProgressBar, _check_option, logger, verbose
+from ..utils import ProgressBar, _check_option, _pl, _validate_type, logger, verbose
 from .inverse import (
     INVERSE_METHODS,
     _assemble_kernel,
@@ -33,6 +34,72 @@
 )
 
 
+def _restrict_K_to_lbls(labels, K, noise_norm, vertno, pick_ori):
+    """Use labels to choose desired sources in the kernel."""
+    verts_to_use = [[], []]
+    # create mask for K by compiling original vertices from vertno in labels
+    for ii in range(len(labels)):
+        lab = labels[ii]
+        # handle BiHemi labels; ok so long as no overlap w/ single hemi labels
+        if lab.hemi == "both":
+            l_verts = np.intersect1d(vertno[0], lab.lh.vertices)
+            r_verts = np.intersect1d(vertno[1], lab.rh.vertices)  # output sorted
+            verts_to_use[0] += list(l_verts)
+            verts_to_use[1] += list(r_verts)
+        else:
+            hidx = 0 if lab.hemi == "lh" else 1
+            verts = np.intersect1d(vertno[hidx], lab.vertices)
+            verts_to_use[hidx] += list(verts)
+
+    # check that we don't have overlapping vertices in our labels
+    for ii in range(2):
+        if len(np.unique(verts_to_use[ii])) != len(verts_to_use[ii]):
+            raise RuntimeError(
+                "Labels cannot have overlapping vertices. "
+                "Please select labels with unique vertices "
+                "and try again."
+            )
+
+    # turn original vertex numbers from vertno into indices for K
+    K_mask = np.searchsorted(vertno[0], verts_to_use[0])
+    r_kmask = np.searchsorted(vertno[1], verts_to_use[1]) + len(vertno[0])
+    K_mask = np.hstack((K_mask, r_kmask))
+
+    # record which original vertices are at each index in out_K
+    hemis = ("lh", "rh")
+    ki_keys = [
+        (hemis[hi], verts_to_use[hi][ii])
+        for hi in range(2)
+        for ii in range(len(verts_to_use[hi]))
+    ]
+    ki_vals = list(range(len(K_mask)))
+    k_idxs = dict(zip(ki_keys, ki_vals))
+
+    # mask K, handling the orientation issue
+    len_allverts = len(vertno[0]) + len(vertno[1])
+    if len(K) == len_allverts:
+        assert pick_ori == "normal"
+        out_K = K[K_mask]
+    else:
+        # here, K = [x0, y0, z0, x1, y1, z1 ...]
+        # we need to drop x, y and z of unused vertices
+        assert not pick_ori == "normal", pick_ori
+        assert len(K) == 3 * len_allverts, (len(K), len_allverts)
+        out_len = len(K_mask) * 3
+        out_K = K[0:out_len]  # get the correct-shaped array
+        for di in range(3):
+            K_pick = K[di::3]
+            out_K[di::3] = K_pick[K_mask]  # set correct values for out
+
+    out_vertno = verts_to_use
+    if noise_norm is not None:
+        out_nn = noise_norm[K_mask]
+    else:
+        out_nn = None
+
+    return out_K, out_nn, out_vertno, k_idxs
+
+
 def _prepare_source_params(
     inst,
     inverse_operator,
@@ -64,9 +131,26 @@ def _prepare_source_params(
     #   This does all the data transformations to compute the weights for the
     #   eigenleads
     #
-    K, noise_norm, vertno, _ = _assemble_kernel(
-        inv, label, method, pick_ori, use_cps=use_cps
-    )
+    # K shape: (3 x n_sources, n_channels) or (n_sources, n_channels)
+    # noise_norm shape: (n_sources, 1)
+    # vertno: [lh_verts, rh_verts]
+
+    k_idxs = None
+    if not isinstance(label, (Label, BiHemiLabel)):
+        whole_K, whole_noise_norm, whole_vertno, _ = _assemble_kernel(
+            inv, None, method, pick_ori, use_cps=use_cps
+        )
+        if isinstance(label, list):
+            K, noise_norm, vertno, k_idxs = _restrict_K_to_lbls(
+                label, whole_K, whole_noise_norm, whole_vertno, pick_ori
+            )
+        else:
+            assert not label
+            K, noise_norm, vertno = whole_K, whole_noise_norm, whole_vertno
+    elif isinstance(label, (Label, BiHemiLabel)):
+        K, noise_norm, vertno, _ = _assemble_kernel(
+            inv, label, method, pick_ori, use_cps=use_cps
+        )
 
     if pca:
         U, s, Vh = _safe_svd(K, full_matrices=False)
@@ -78,7 +162,7 @@ def _prepare_source_params(
         Vh = None
     is_free_ori = inverse_operator["source_ori"] == FIFF.FIFFV_MNE_FREE_ORI
 
-    return K, sel, Vh, vertno, is_free_ori, noise_norm
+    return K, sel, Vh, vertno, is_free_ori, noise_norm, k_idxs
 
 
 @verbose
@@ -114,8 +198,9 @@ def source_band_induced_power(
         The inverse operator.
     bands : dict
         Example : bands = dict(alpha=[8, 9]).
-    label : Label
-        Restricts the source estimates to a given label.
+    label : Label | list of Label
+        Restricts the source estimates to a given label or list of labels. If
+        labels are provided in a list, power will be averaged over vertices.
     lambda2 : float
         The regularization parameter of the minimum norm.
     method : "MNE" | "dSPM" | "sLORETA" | "eLORETA"
@@ -170,7 +255,10 @@ def source_band_induced_power(
     Returns
     -------
     stcs : dict of SourceEstimate (or VolSourceEstimate)
-        The estimated source space induced power estimates.
+        The estimated source space induced power estimates in shape
+        (n_vertices, n_frequencies, n_samples) if label=None or label=label.
+        For lists of one or more labels, the induced power estimate has shape
+        (n_labels, n_frequencies, n_samples).
     """  # noqa: E501
     _check_option("method", method, INVERSE_METHODS)
 
@@ -262,6 +350,7 @@ def _compute_pow_plv(
     with_plv,
     pick_ori,
     decim,
+    noise_norm=None,
     verbose=None,
 ):
     """Aux function for induced power and PLV."""
@@ -292,6 +381,9 @@ def _compute_pow_plv(
         if with_plv:
             plv += plv_e
 
+    if noise_norm is not None:
+        power *= noise_norm[:, :, np.newaxis] ** 2
+
     return power, plv
 
 
@@ -345,6 +437,41 @@ def _single_epoch_tfr(
     return tfr_e, plv_e
 
 
+def _get_label_power(power, labels, vertno, k_idxs):
+    """Average power across vertices in labels."""
+    (_, ps1, ps2) = power.shape
+    # construct out array with correct shape
+    out_power = np.zeros(shape=(len(labels), ps1, ps2))
+
+    # for each label, compile list of vertices we want
+    for li in np.arange(len(labels)):
+        lab = labels[li]
+        hemis = ("lh", "rh")
+        all_vnums = [[], []]
+        if lab.hemi == "both":
+            all_vnums[0] = np.intersect1d(lab.lh.vertices, vertno[0])
+            all_vnums[1] = np.intersect1d(lab.rh.vertices, vertno[1])
+        else:
+            assert lab.hemi == "lh" or lab.hemi == "rh"
+            h_id = 0 if lab.hemi == "lh" else 1
+            all_vnums[h_id] = np.intersect1d(vertno[h_id], lab.vertices)
+
+        verts = [(hemis[hi], vn) for hi in range(2) for vn in all_vnums[hi]]
+
+        # restrict power to relevant vertices in label
+        lab_mask = np.array([False] * len(power))
+        for vert in verts:
+            lab_mask[k_idxs[vert]] = True  # k_idxs[vert] gives power row index
+        lab_power = power[lab_mask]  # only pass through rows we want
+        assert lab_power.shape == (len(verts), ps1, ps2)
+
+        # set correct out values for label
+        out_power[li, :, :] = np.mean(lab_power, axis=0)
+
+    assert out_power.shape == (len(labels), ps1, ps2)
+    return out_power
+
+
 @verbose
 def _source_induced_power(
     epochs,
@@ -368,8 +495,29 @@ def _source_induced_power(
     verbose=None,
 ):
     """Aux function for source induced power."""
+    if label:
+        _validate_type(
+            label,
+            types=(Label, BiHemiLabel, list, tuple, None),
+            type_name=("Label or BiHemiLabel", "list of labels", "None"),
+        )
+        if isinstance(label, (list, tuple)):
+            for item in label:
+                _validate_type(
+                    item,
+                    types=(Label, BiHemiLabel),
+                    type_name=("Label or BiHemiLabel"),
+                )
+            if len(label) > 1 and with_plv:
+                raise RuntimeError(
+                    "Phase-locking value cannot be calculated "
+                    "when averaging induced power within "
+                    "labels. Please set `with_plv` to False, pass a "
+                    "single `label=label`, or set `label=None`."
+                )
+
     epochs_data = epochs.get_data()
-    K, sel, Vh, vertno, is_free_ori, noise_norm = _prepare_source_params(
+    K, sel, Vh, vertno, is_free_ori, noise_norm, k_id = _prepare_source_params(
         inst=epochs,
         inverse_operator=inverse_operator,
         label=label,
@@ -406,12 +554,26 @@ def _source_induced_power(
             with_power=True,
             pick_ori=pick_ori,
             decim=decim,
+            noise_norm=noise_norm,
         )
         for data in np.array_split(epochs_data, n_jobs)
     )
-    power = sum(o[0] for o in out)
+    power = sum(o[0] for o in out)  # power shape: (n_verts, n_freqs, n_samps)
     power /= len(epochs_data)  # average power over epochs
 
+    if isinstance(label, (Label, BiHemiLabel)):
+        logger.info(
+            f"Outputting power for {len(power)} vertices in label {label.name}."
+        )
+    elif isinstance(label, list):
+        power = _get_label_power(power, label, vertno, k_id)
+        logger.info(
+            "Averaging induced power across vertices within labels "
+            f"for {len(label)} label{_pl(label)}."
+        )
+    else:
+        assert not label
+
     if with_plv:
         plv = sum(o[1] for o in out)
         plv = np.abs(plv)
@@ -419,9 +581,6 @@ def _source_induced_power(
     else:
         plv = None
 
-    if noise_norm is not None:
-        power *= noise_norm[:, :, np.newaxis] ** 2
-
     return power, plv, vertno
 
 
@@ -442,6 +601,8 @@ def source_induced_power(
     baseline_mode="logratio",
     pca=True,
     n_jobs=None,
+    *,
+    return_plv=True,
     zero_mean=False,
     prepared=False,
     method_params=None,
@@ -460,8 +621,10 @@ def source_induced_power(
         The inverse operator.
     freqs : array
         Array of frequencies of interest.
-    label : Label
-        Restricts the source estimates to a given label.
+    label : Label | list of Label
+        Restricts the source estimates to a given label or list of labels. If
+        labels are provided in a list, power will be averaged over vertices within each
+        label.
     lambda2 : float
         The regularization parameter of the minimum norm.
     method : "MNE" | "dSPM" | "sLORETA" | "eLORETA"
@@ -506,6 +669,10 @@ def source_induced_power(
         the time-frequency transforms. It reduces the computation times
         e.g. with a dataset that was maxfiltered (true dim is 64).
     %(n_jobs)s
+    return_plv : bool
+        If True, return the phase-locking value array. Else, only return power.
+
+        .. versionadded:: 1.6
     zero_mean : bool
         Make sure the wavelets are zero mean.
     prepared : bool
@@ -520,7 +687,12 @@ def source_induced_power(
     Returns
     -------
     power : array
-        The induced power.
+        The induced power array with shape (n_sources, n_freqs, n_samples) if
+        label=None or label=label. For lists of one or more labels, the induced
+        power estimate has shape (n_labels, n_frequencies, n_samples).
+    plv : array
+        The phase-locking value array with shape (n_sources, n_freqs,
+        n_samples). Only returned if ``return_plv=True``.
     """  # noqa: E501
     _check_option("method", method, INVERSE_METHODS)
     _check_ori(pick_ori, inverse_operator["source_ori"], inverse_operator["src"])
@@ -539,6 +711,7 @@ def source_induced_power(
         pick_ori=pick_ori,
         pca=pca,
         n_jobs=n_jobs,
+        with_plv=return_plv,
         method_params=method_params,
         zero_mean=zero_mean,
         prepared=prepared,
@@ -547,7 +720,9 @@ def source_induced_power(
 
     # Run baseline correction
     power = rescale(power, epochs.times[::decim], baseline, baseline_mode, copy=False)
-    return power, plv
+
+    outs = (power, plv) if return_plv else power
+    return outs
 
 
 @verbose
@@ -761,7 +936,17 @@ def _compute_source_psd_epochs(
     """Generate compute_source_psd_epochs."""
     logger.info("Considering frequencies %g ... %g Hz" % (fmin, fmax))
 
-    K, sel, Vh, vertno, is_free_ori, noise_norm = _prepare_source_params(
+    if label:
+        # TODO: add multi-label support
+        # since `_prepare_source_params` can handle a list of labels now,
+        # multi-label support should be within reach for psd calc as well
+        _validate_type(
+            label,
+            types=(Label, BiHemiLabel, None),
+            type_name=("Label or BiHemiLabel", "None"),
+        )
+
+    K, sel, Vh, vertno, is_free_ori, noise_norm, _ = _prepare_source_params(
         inst=epochs,
         inverse_operator=inverse_operator,
         label=label,
diff --git a/mne/source_space/tests/test_source_space.py b/mne/source_space/tests/test_source_space.py
index a0fe8dde4a1..afccc567074 100644
--- a/mne/source_space/tests/test_source_space.py
+++ b/mne/source_space/tests/test_source_space.py
@@ -386,6 +386,7 @@ def test_other_volume_source_spaces(tmp_path):
     """Test setting up other volume source spaces."""
     # these are split off because they require the MNE tools, and
     # Travis doesn't seem to like them
+    pytest.importorskip("nibabel")
 
     # let's try the spherical one (no bem or surf supplied)
     temp_name = tmp_path / "temp-src.fif"
@@ -562,6 +563,7 @@ def test_setup_source_space(tmp_path):
 @pytest.mark.parametrize("spacing", [2, 7])
 def test_setup_source_space_spacing(tmp_path, spacing, monkeypatch):
     """Test setting up surface source spaces using a given spacing."""
+    pytest.importorskip("nibabel")
     copytree(subjects_dir / "sample", tmp_path / "sample")
     args = [] if spacing == 7 else ["--spacing", str(spacing)]
     monkeypatch.setenv("SUBJECTS_DIR", str(tmp_path))
diff --git a/mne/tests/test_dipole.py b/mne/tests/test_dipole.py
index 26cde7a3ea4..b26cfec936f 100644
--- a/mne/tests/test_dipole.py
+++ b/mne/tests/test_dipole.py
@@ -101,6 +101,7 @@ def test_io_dipoles(tmp_path):
 @testing.requires_testing_data
 def test_dipole_fitting_ctf():
     """Test dipole fitting with CTF data."""
+    pytest.importorskip("nibabel")
     raw_ctf = read_raw_ctf(fname_ctf).set_eeg_reference(projection=True)
     events = make_fixed_length_events(raw_ctf, 1)
     evoked = Epochs(raw_ctf, events, 1, 0, 0, baseline=None).average()
@@ -125,6 +126,7 @@ def test_dipole_fitting_ctf():
 @requires_mne
 def test_dipole_fitting(tmp_path):
     """Test dipole fitting."""
+    pytest.importorskip("nibabel")
     amp = 100e-9
     rng = np.random.RandomState(0)
     fname_dtemp = tmp_path / "test.dip"
diff --git a/mne/tests/test_surface.py b/mne/tests/test_surface.py
index 60b9fed5a17..3513a32bb32 100644
--- a/mne/tests/test_surface.py
+++ b/mne/tests/test_surface.py
@@ -196,6 +196,7 @@ def test_decimate_surface_vtk(n_tri):
 @requires_freesurfer("mris_sphere")
 def test_decimate_surface_sphere():
     """Test sphere mode of decimation."""
+    pytest.importorskip("nibabel")
     rr, tris = _tessellate_sphere(3)
     assert len(rr) == 66
     assert len(tris) == 128

From a4d4c10c6546bd53fa01f7b81ca87c6d2c0f155a Mon Sep 17 00:00:00 2001
From: Eric Larson 
Date: Wed, 1 Nov 2023 16:38:07 -0400
Subject: [PATCH 032/405] MAINT: Fix linkcheck (#12162)

---
 doc/changes/names.inc            | 2 +-
 doc/conf.py                      | 6 ++++++
 doc/development/contributing.rst | 2 --
 doc/help/faq.rst                 | 1 +
 mne/cov.py                       | 5 ++---
 5 files changed, 10 insertions(+), 6 deletions(-)

diff --git a/doc/changes/names.inc b/doc/changes/names.inc
index 722f0c2dc0d..82910422954 100644
--- a/doc/changes/names.inc
+++ b/doc/changes/names.inc
@@ -184,7 +184,7 @@
 
 .. _George O'Neill: https://georgeoneill.github.io
 
-.. _Gonzalo Reina: https://greina.me/
+.. _Gonzalo Reina: https://github.com/Gon-reina
 
 .. _Guillaume Dumas: https://mila.quebec/en/person/guillaume-dumas
 
diff --git a/doc/conf.py b/doc/conf.py
index b07f3c50495..df835e4a088 100644
--- a/doc/conf.py
+++ b/doc/conf.py
@@ -722,11 +722,17 @@ def append_attr_meth_examples(app, what, name, obj, options, lines):
     "https://doi.org/10.1088/",  # www.tandfonline.com
     "https://doi.org/10.3109/",  # www.tandfonline.com
     "https://www.researchgate.net/profile/",
+    "https://www.intel.com/content/www/us/en/developer/tools/oneapi/onemkl.html",
+    "https://scholar.google.com/scholar?cites=12188330066413208874&as_ylo=2014",
+    "https://scholar.google.com/scholar?cites=1521584321377182930&as_ylo=2013",
+    # 500 server error
+    "https://openwetware.org/wiki/Beauchamp:FreeSurfer",
     # 503 Server error
     "https://hal.archives-ouvertes.fr/hal-01848442",
     # Read timed out
     "http://www.cs.ucl.ac.uk/staff/d.barber/brml",
     "https://www.cea.fr",
+    "http://www.humanconnectome.org/data",
     # Max retries exceeded
     "https://doi.org/10.7488/ds/1556",
     "https://datashare.is.ed.ac.uk/handle/10283",
diff --git a/doc/development/contributing.rst b/doc/development/contributing.rst
index d741c540479..f5194cb688d 100644
--- a/doc/development/contributing.rst
+++ b/doc/development/contributing.rst
@@ -215,8 +215,6 @@ Once you have git installed and configured, and before creating your local copy
 of the codebase, go to the `MNE-Python GitHub`_ page and create a `fork`_ into
 your GitHub user account.
 
-.. image:: https://docs.github.com/assets/cb-28613/images/help/repository/fork_button.png
-
 This will create a copy of the MNE-Python codebase inside your GitHub user
 account (this is called "your fork"). Changes you make to MNE-Python will
 eventually get "pushed" to your fork, and will be incorporated into the
diff --git a/doc/help/faq.rst b/doc/help/faq.rst
index ea280290c14..14d85f4e038 100644
--- a/doc/help/faq.rst
+++ b/doc/help/faq.rst
@@ -275,6 +275,7 @@ magnitude.
 Forward and Inverse Solution
 ============================
 
+.. _faq_how_should_i_regularize:
 
 How should I regularize the covariance matrix?
 ----------------------------------------------
diff --git a/mne/cov.py b/mne/cov.py
index db4b2126a3d..376cd6a8a59 100644
--- a/mne/cov.py
+++ b/mne/cov.py
@@ -1895,9 +1895,8 @@ def regularize(
     .. note:: This function is kept for reasons of backward-compatibility.
               Please consider explicitly using the ``method`` parameter in
               :func:`mne.compute_covariance` to directly combine estimation
-              with regularization in a data-driven fashion. See the `faq
-              `_
-              for more information.
+              with regularization in a data-driven fashion. See the
+              :ref:`FAQ ` for more information.
 
     Parameters
     ----------

From fbc666d98f76d02a1917317c3733b1fe94ea95dd Mon Sep 17 00:00:00 2001
From: Stefan Appelhoff 
Date: Thu, 2 Nov 2023 16:07:07 +0100
Subject: [PATCH 033/405] DOC: fix sphinx style typos (#12161)

---
 doc/changes/v0.17.rst | 2 +-
 doc/changes/v0.18.rst | 4 ++--
 doc/changes/v1.0.rst  | 2 +-
 doc/changes/v1.1.rst  | 2 +-
 doc/changes/v1.4.rst  | 2 +-
 doc/changes/v1.5.rst  | 2 +-
 6 files changed, 7 insertions(+), 7 deletions(-)

diff --git a/doc/changes/v0.17.rst b/doc/changes/v0.17.rst
index 82db1975a9f..40896b6f383 100644
--- a/doc/changes/v0.17.rst
+++ b/doc/changes/v0.17.rst
@@ -201,7 +201,7 @@ Bug
 
 - Fix processing of data with bad segments and acquisition skips with new ``skip_by_annotation`` parameter in :func:`mne.preprocessing.maxwell_filter` by `Eric Larson`_
 
-- Fix symlinking to use relative paths in ``mne flash_bem` and ``mne watershed_bem`` by `Eric Larson`_
+- Fix symlinking to use relative paths in ``mne flash_bem`` and ``mne watershed_bem`` by `Eric Larson`_
 
 - Fix error in mne coreg when saving with scaled MRI if fiducials haven't been saved by `Ezequiel Mikulan`_
 
diff --git a/doc/changes/v0.18.rst b/doc/changes/v0.18.rst
index e525e8849a9..4e73e42239b 100644
--- a/doc/changes/v0.18.rst
+++ b/doc/changes/v0.18.rst
@@ -8,7 +8,7 @@ Changelog
 
 - Add ``event_id='auto'`` in :func:`mne.events_from_annotations` to accommodate Brainvision markers by `Jona Sassenhagen`_, `Joan Massich`_ and `Eric Larson`_
 
-- Add example on how to simulate raw data using subject anatomy, by `Ivana Kojcic`_,`Eric Larson`_,`Samuel Deslauriers-Gauthier`_ and`Kostiantyn Maksymenko`_
+- Add example on how to simulate raw data using subject anatomy, by `Ivana Kojcic`_, `Eric Larson`_, `Samuel Deslauriers-Gauthier`_ and `Kostiantyn Maksymenko`_
 
 - :func:`mne.beamformer.apply_lcmv_cov` returns static source power after supplying a data covariance matrix to the beamformer filter by `Britta Westner`_ and `Marijn van Vliet`_
 
@@ -159,7 +159,7 @@ Bug
 
 - Fix 32bits annotations in :func:`mne.io.read_raw_cnt` by `Joan Massich`_
 
-- Fix :func:`mne.events_from_annotations` to ignore ``'BAD_'` and ``'EDGE_'`` annotations by default using a new default ``regexp`` by `Eric Larson`_
+- Fix :func:`mne.events_from_annotations` to ignore ``'BAD_'`` and ``'EDGE_'`` annotations by default using a new default ``regexp`` by `Eric Larson`_
 
 - Fix bug in ``mne.preprocessing.mark_flat`` where ``raw.first_samp`` was not taken into account by `kalenkovich`_
 
diff --git a/doc/changes/v1.0.rst b/doc/changes/v1.0.rst
index 0c053e134a5..dd5e7b501ed 100644
--- a/doc/changes/v1.0.rst
+++ b/doc/changes/v1.0.rst
@@ -132,7 +132,7 @@ Bugs
 
 - Fix use of arguments in :func:`numpy.loadtxt` (:gh:`10189` by :newcontrib:`Federico Zamberlan`)
 
-- Fix documentation of options in :func:`mne.stc_near_sensors` (:gh:`` by :newcontrib:`Nikolai Chapochnikov`)
+- Fix documentation of options in :func:`mne.stc_near_sensors` (:gh:`10007` by :newcontrib:`Nikolai Chapochnikov`)
 
 - :func:`mne.time_frequency.tfr_array_multitaper` now returns results per taper when ``output='complex'`` (:gh:`10281` by `Mikołaj Magnuski`_)
 
diff --git a/doc/changes/v1.1.rst b/doc/changes/v1.1.rst
index 50ebc8111e8..de0f597c0ee 100644
--- a/doc/changes/v1.1.rst
+++ b/doc/changes/v1.1.rst
@@ -131,7 +131,7 @@ Bugs
 
 - Fix bug in :func:`mne.io.read_raw_ctf` where invalid measurement dates were not handled properly (:gh:`10957` by `Jean-Remi King`_ and `Eric Larson`_)
 
-- Rendering issues with recent MESA releases can be avoided by setting the new environment variable``MNE_3D_OPTION_MULTI_SAMPLES=1`` or using :func:`mne.viz.set_3d_options` (:gh:`10513` by `Eric Larson`_)
+- Rendering issues with recent MESA releases can be avoided by setting the new environment variable ``MNE_3D_OPTION_MULTI_SAMPLES=1`` or using :func:`mne.viz.set_3d_options` (:gh:`10513` by `Eric Larson`_)
 
 - Fix behavior for the ``pyvista`` 3D renderer's ``quiver3D`` function so that default arguments plot a glyph in ``arrow`` mode (:gh:`10493` by `Alex Rockhill`_)
 
diff --git a/doc/changes/v1.4.rst b/doc/changes/v1.4.rst
index 735a7b6af18..2fa9ec2a0d1 100644
--- a/doc/changes/v1.4.rst
+++ b/doc/changes/v1.4.rst
@@ -8,7 +8,7 @@ Enhancements
 - Add functionality for reading CNT spans/annotations marked bad to :func:`mne.io.read_raw_cnt` (:gh:`11631` by :newcontrib:`Jacob Woessner`)
 - Add ``:unit:`` Sphinx directive to enable use of uniform non-breaking spaces throughout the documentation (:gh:`11469` by :newcontrib:`Sawradip Saha`)
 - Adjusted the algorithm used in :class:`mne.decoding.SSD` to support non-full rank data (:gh:`11458` by :newcontrib:`Thomas Binns`)
-- Changed suggested type for ``ch_groups``` in `mne.viz.plot_sensors` from array to list of list(s) (arrays are still supported). (:gh:`11465` by `Hyonyoung Shin`_)
+- Changed suggested type for ``ch_groups`` in `mne.viz.plot_sensors` from array to list of list(s) (arrays are still supported). (:gh:`11465` by `Hyonyoung Shin`_)
 - Add support for UCL/FIL OPM data using :func:`mne.io.read_raw_fil` (:gh:`11366` by :newcontrib:`George O'Neill` and `Robert Seymour`_)
 - Add harmonic field correction (HFC) for OPM sensors in :func:`mne.preprocessing.compute_proj_hfc` (:gh:`11536` by :newcontrib:`George O'Neill` and `Eric Larson`_)
 - Forward argument ``axes`` from `mne.viz.plot_sensors` to `mne.channels.DigMontage.plot` (:gh:`11470` by :newcontrib:`Jan Ebert` and `Mathieu Scheltienne`_)
diff --git a/doc/changes/v1.5.rst b/doc/changes/v1.5.rst
index a272e6c6fdc..c607aefe26d 100644
--- a/doc/changes/v1.5.rst
+++ b/doc/changes/v1.5.rst
@@ -76,7 +76,7 @@ Bugs
 API changes
 ~~~~~~~~~~~
 - The ``baseline`` argument can now be array-like (e.g. ``list``, ``tuple``, ``np.ndarray``, ...) instead of only a ``tuple`` (:gh:`11713` by `Clemens Brunner`_)
-- The ``events`` and ``event_id`` parameters of `:meth:`Epochs.plot() ` now accept boolean values; see docstring for details (:gh:`11445` by `Daniel McCloy`_ and `Clemens Brunner`_)
+- The ``events`` and ``event_id`` parameters of :meth:`Epochs.plot() ` now accept boolean values; see docstring for details (:gh:`11445` by `Daniel McCloy`_ and `Clemens Brunner`_)
 - Deprecated ``gap_description`` keyword argument of :func:`mne.io.read_raw_eyelink`, which will be removed in mne version 1.6, in favor of using :meth:`mne.Annotations.rename` (:gh:`11719` by `Scott Huberty`_)
 
 Authors

From 89ec1d156595ca8289b3fc81b9a0ef1119d3655b Mon Sep 17 00:00:00 2001
From: Eric Larson 
Date: Thu, 2 Nov 2023 15:26:18 -0400
Subject: [PATCH 034/405] MAINT: Add rstcheck to CIs and pre-commit (#12163)

---
 .pre-commit-config.yaml                 | 10 ++++++++++
 doc/_includes/dig_formats.rst           |  1 +
 doc/development/contributing.rst        |  2 ++
 doc/documentation/design_philosophy.rst |  2 ++
 doc/documentation/index.rst             |  2 ++
 doc/install/mne_tools_suite.rst         |  2 ++
 mne/decoding/tests/test_search_light.py |  6 +++++-
 pyproject.toml                          | 14 ++++++++++++++
 tools/azure_dependencies.sh             |  2 +-
 tools/github_actions_dependencies.sh    |  2 +-
 10 files changed, 40 insertions(+), 3 deletions(-)

diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index c4ec4cd6c62..6ff357ca832 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -41,3 +41,13 @@ repos:
     hooks:
       - id: yamllint
         args: [--strict, -c, .yamllint.yml]
+
+  # rstcheck
+  - repo: https://github.com/rstcheck/rstcheck.git
+    rev: v6.2.0
+    hooks:
+      - id: rstcheck
+        files: ^doc/.*\.(rst|inc)$
+        # https://github.com/rstcheck/rstcheck/issues/199
+        # https://github.com/rstcheck/rstcheck/issues/200
+        exclude: ^doc/(help/faq|install/manual_install|install/mne_c|install/advanced|install/updating|_includes/channel_interpolation|_includes/inverse|_includes/ssp)\.rst$
diff --git a/doc/_includes/dig_formats.rst b/doc/_includes/dig_formats.rst
index c2d3fde4c27..5928b081aea 100644
--- a/doc/_includes/dig_formats.rst
+++ b/doc/_includes/dig_formats.rst
@@ -1,4 +1,5 @@
 :orphan:
+
 .. _dig-formats:
 
 Supported formats for digitized 3D locations
diff --git a/doc/development/contributing.rst b/doc/development/contributing.rst
index f5194cb688d..2dbf90d306b 100644
--- a/doc/development/contributing.rst
+++ b/doc/development/contributing.rst
@@ -1111,3 +1111,5 @@ it can serve as a useful example of what to expect from the PR review process.
 
 .. _optipng: http://optipng.sourceforge.net/
 .. _optipng for Windows: http://prdownloads.sourceforge.net/optipng/optipng-0.7.7-win32.zip?download
+
+.. include:: ../links.inc
diff --git a/doc/documentation/design_philosophy.rst b/doc/documentation/design_philosophy.rst
index af43f630aa1..5bdec09b4fa 100644
--- a/doc/documentation/design_philosophy.rst
+++ b/doc/documentation/design_philosophy.rst
@@ -94,3 +94,5 @@ of data.
 .. LINKS
 
 .. _`method chaining`: https://en.wikipedia.org/wiki/Method_chaining
+
+.. include:: ../links.inc
diff --git a/doc/documentation/index.rst b/doc/documentation/index.rst
index 6830edff012..764fcd08188 100644
--- a/doc/documentation/index.rst
+++ b/doc/documentation/index.rst
@@ -59,3 +59,5 @@ Documentation for the related C and MATLAB tools are available here:
    cookbook
    cite
    cited
+
+.. include:: ../links.inc
diff --git a/doc/install/mne_tools_suite.rst b/doc/install/mne_tools_suite.rst
index 579e3c77c08..03b65671826 100644
--- a/doc/install/mne_tools_suite.rst
+++ b/doc/install/mne_tools_suite.rst
@@ -112,3 +112,5 @@ Help with installation is available through the `MNE Forum`_. See the
 .. _invertmeeg: https://github.com/LukeTheHecker/invert
 .. _MNE-ARI: https://github.com/john-veillette/mne_ari
 .. _niseq: https://github.com/john-veillette/niseq
+
+.. include:: ../links.inc
diff --git a/mne/decoding/tests/test_search_light.py b/mne/decoding/tests/test_search_light.py
index 00b4f98f997..3d5009763eb 100644
--- a/mne/decoding/tests/test_search_light.py
+++ b/mne/decoding/tests/test_search_light.py
@@ -2,6 +2,7 @@
 #
 # License: BSD-3-Clause
 
+import platform
 from inspect import signature
 
 import numpy as np
@@ -10,7 +11,7 @@
 
 from mne.decoding.search_light import GeneralizingEstimator, SlidingEstimator
 from mne.decoding.transformer import Vectorizer
-from mne.utils import _record_warnings, use_log_level
+from mne.utils import _record_warnings, check_version, use_log_level
 
 pytest.importorskip("sklearn")
 
@@ -29,6 +30,9 @@ def make_data():
 
 def test_search_light():
     """Test SlidingEstimator."""
+    # https://github.com/scikit-learn/scikit-learn/issues/27711
+    if platform.system() == "Windows" and check_version("numpy", "2.0.0.dev0"):
+        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.pipeline import make_pipeline
diff --git a/pyproject.toml b/pyproject.toml
index 0dc29069335..f28204f5bca 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -140,3 +140,17 @@ exclude = "(dist/)|(build/)|(.*\\.ipynb)"
 
 [tool.bandit.assert_used]
 skips = ["*/test_*.py"] # assert statements are good practice with pytest
+
+[tool.rstcheck]
+report_level = "WARNING"
+ignore_roles = [
+    "func", "class", "term", "ref", "doc", "gh", "file", "samp", "meth", "mod", "kbd",
+    "newcontrib", "footcite", "footcite:t", "eq", "py:mod", "attr", "py:class", "exc",
+]
+ignore_directives = [
+    "rst-class", "tab-set", "grid", "toctree", "footbibliography", "autosummary",
+    "currentmodule", "automodule", "cssclass", "tabularcolumns", "minigallery",
+    "autoclass", "highlight", "dropdown", "graphviz", "glossary", "autofunction",
+    "bibliography",
+]
+ignore_messages = "^.*(Unknown target name|Undefined substitution referenced)[^`]*$"
diff --git a/tools/azure_dependencies.sh b/tools/azure_dependencies.sh
index 5cf455cf4f4..d3ce1a98119 100755
--- a/tools/azure_dependencies.sh
+++ b/tools/azure_dependencies.sh
@@ -25,7 +25,7 @@ elif [ "${TEST_MODE}" == "pip-pre" ]; then
 	echo "misc"
 	python -m pip install $STD_ARGS imageio-ffmpeg xlrd mffpy python-picard pillow
 	echo "nibabel with workaround"
-	python -m pip install --progress-bar off git+https://github.com/mscheltienne/nibabel.git@np.sctypes
+	python -m pip install --progress-bar off git+https://github.com/nipy/nibabel.git
 	echo "joblib"
 	python -m pip install --progress-bar off git+https://github.com/joblib/joblib@master
 	echo "EDFlib-Python"
diff --git a/tools/github_actions_dependencies.sh b/tools/github_actions_dependencies.sh
index 65a64e05ae5..c5f4dd0ea7e 100755
--- a/tools/github_actions_dependencies.sh
+++ b/tools/github_actions_dependencies.sh
@@ -44,7 +44,7 @@ else
 	echo "mne-qt-browser"
 	pip install $STD_ARGS git+https://github.com/mne-tools/mne-qt-browser
 	echo "nibabel with workaround"
-	pip install $STD_ARGS git+https://github.com/mscheltienne/nibabel.git@np.sctypes
+	pip install $STD_ARGS git+https://github.com/nipy/nibabel.git
 	echo "joblib"
 	pip install $STD_ARGS git+https://github.com/joblib/joblib@master
 	echo "EDFlib-Python"

From 70a915b08a8a7b1a02fcbfde3e5d4eee4c057717 Mon Sep 17 00:00:00 2001
From: Eric Larson 
Date: Thu, 2 Nov 2023 16:54:50 -0400
Subject: [PATCH 035/405] BUG: Fix bug with Report.add_ica component number
 (#12156)

---
 doc/changes/devel.rst           |  1 +
 mne/report/report.py            | 19 ++-----------------
 mne/report/tests/test_report.py |  7 ++++---
 tutorials/intro/70_report.py    |  2 +-
 4 files changed, 8 insertions(+), 21 deletions(-)

diff --git a/doc/changes/devel.rst b/doc/changes/devel.rst
index c800e6c538c..57a9b1fcb41 100644
--- a/doc/changes/devel.rst
+++ b/doc/changes/devel.rst
@@ -64,6 +64,7 @@ Bugs
 - Fix bug with :meth:`mne.viz.Brain.get_view` where calling :meth:`~mne.viz.Brain.show_view` with returned parameters would change the view (:gh:`12000` by `Eric Larson`_)
 - Fix bug with :meth:`mne.viz.Brain.show_view` where ``distance=None`` would change the view distance (:gh:`12000` by `Eric Larson`_)
 - Fix bug with :meth:`~mne.viz.Brain.add_annotation` when reading an annotation from a file with both hemispheres shown (:gh:`11946` by `Marijn van Vliet`_)
+- Fix bug with reported component number and errant reporting of PCA explained variance as ICA explained variance in :meth:`mne.Report.add_ica` (:gh:`12155` by `Eric Larson`_)
 - Fix bug with axis clip box boundaries in :func:`mne.viz.plot_evoked_topo` and related functions (:gh:`11999` by `Eric Larson`_)
 - Fix bug with ``subject_info`` when loading data from and exporting to EDF file (:gh:`11952` by `Paul Roujansky`_)
 - Fix bug where :class:`mne.Info` HTML representations listed all channel counts instead of good channel counts under the heading "Good channels" (:gh:`12145` by `Eric Larson`_) 
diff --git a/mne/report/report.py b/mne/report/report.py
index d1138e1e610..a99414697d9 100644
--- a/mne/report/report.py
+++ b/mne/report/report.py
@@ -1669,24 +1669,9 @@ def _add_ica_properties(
         figs = _plot_ica_properties_as_arrays(
             ica=ica, inst=inst, picks=picks, n_jobs=n_jobs
         )
-        rel_explained_var = (
-            ica.pca_explained_variance_ / ica.pca_explained_variance_.sum()
-        )
-        cum_explained_var = np.cumsum(rel_explained_var)
         captions = []
-        for idx, rel_var, cum_var in zip(
-            range(len(figs)),
-            rel_explained_var[: len(figs)],
-            cum_explained_var[: len(figs)],
-        ):
-            caption = (
-                f"ICA component {idx}. " f"Variance explained: {round(100 * rel_var)}%"
-            )
-            if idx == 0:
-                caption += "."
-            else:
-                caption += f" ({round(100 * cum_var)}% cumulative)."
-
+        for idx in range(len(figs)):
+            caption = f"ICA component {picks[idx]}."
             captions.append(caption)
 
         title = "ICA component properties"
diff --git a/mne/report/tests/test_report.py b/mne/report/tests/test_report.py
index dd7ebfc34c8..9f8760dfbb6 100644
--- a/mne/report/tests/test_report.py
+++ b/mne/report/tests/test_report.py
@@ -913,10 +913,10 @@ def test_manual_report_2d(tmp_path, invisible_fig):
     evoked = evokeds[0].pick("eeg")
 
     with pytest.warns(ConvergenceWarning, match="did not converge"):
-        ica = ICA(n_components=2, max_iter=1, random_state=42).fit(
+        ica = ICA(n_components=3, max_iter=1, random_state=42).fit(
             inst=raw.copy().crop(tmax=1)
         )
-    ica_ecg_scores = ica_eog_scores = np.array([3, 0])
+    ica_ecg_scores = ica_eog_scores = np.array([3, 0, 0])
     ica_ecg_evoked = ica_eog_evoked = epochs_without_metadata.average()
 
     r.add_raw(raw=raw, title="my raw data", tags=("raw",), psd=True, projs=False)
@@ -969,12 +969,13 @@ def test_manual_report_2d(tmp_path, invisible_fig):
         ica=ica,
         title="my ica with raw inst",
         inst=raw.copy().load_data(),
-        picks=[0],
+        picks=[2],
         ecg_evoked=ica_ecg_evoked,
         eog_evoked=ica_eog_evoked,
         ecg_scores=ica_ecg_scores,
         eog_scores=ica_eog_scores,
     )
+    assert "ICA component 2" in r._content[-1].html
     epochs_baseline = epochs_without_metadata.copy().load_data()
     epochs_baseline.apply_baseline((None, 0))
     r.add_ica(
diff --git a/tutorials/intro/70_report.py b/tutorials/intro/70_report.py
index 951c82a5e6a..bbbb4ab2abf 100644
--- a/tutorials/intro/70_report.py
+++ b/tutorials/intro/70_report.py
@@ -266,7 +266,7 @@
 report.add_ica(
     ica=ica,
     title="ICA cleaning",
-    picks=[0, 1],  # only plot the first two components
+    picks=ica.exclude,  # plot the excluded EOG components
     inst=raw,
     eog_evoked=eog_epochs.average(),
     eog_scores=eog_scores,

From 87d6be9047bbd5b234e22eafd79ab7695814fb41 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Richard=20H=C3=B6chenberger?=
 
Date: Fri, 3 Nov 2023 14:05:04 +0100
Subject: [PATCH 036/405] Try to fix ICA Report (#12167)

---
 doc/changes/devel.rst           | 2 +-
 mne/report/report.py            | 8 +++++---
 mne/report/tests/test_report.py | 1 +
 3 files changed, 7 insertions(+), 4 deletions(-)

diff --git a/doc/changes/devel.rst b/doc/changes/devel.rst
index 57a9b1fcb41..fce4656104a 100644
--- a/doc/changes/devel.rst
+++ b/doc/changes/devel.rst
@@ -64,7 +64,7 @@ Bugs
 - Fix bug with :meth:`mne.viz.Brain.get_view` where calling :meth:`~mne.viz.Brain.show_view` with returned parameters would change the view (:gh:`12000` by `Eric Larson`_)
 - Fix bug with :meth:`mne.viz.Brain.show_view` where ``distance=None`` would change the view distance (:gh:`12000` by `Eric Larson`_)
 - Fix bug with :meth:`~mne.viz.Brain.add_annotation` when reading an annotation from a file with both hemispheres shown (:gh:`11946` by `Marijn van Vliet`_)
-- Fix bug with reported component number and errant reporting of PCA explained variance as ICA explained variance in :meth:`mne.Report.add_ica` (:gh:`12155` by `Eric Larson`_)
+- Fix bug with reported component number and errant reporting of PCA explained variance as ICA explained variance in :meth:`mne.Report.add_ica` (:gh:`12155`, :gh:`12167` by `Eric Larson`_ and `Richard Höchenberger`_)
 - Fix bug with axis clip box boundaries in :func:`mne.viz.plot_evoked_topo` and related functions (:gh:`11999` by `Eric Larson`_)
 - Fix bug with ``subject_info`` when loading data from and exporting to EDF file (:gh:`11952` by `Paul Roujansky`_)
 - Fix bug where :class:`mne.Info` HTML representations listed all channel counts instead of good channel counts under the heading "Good channels" (:gh:`12145` by `Eric Larson`_) 
diff --git a/mne/report/report.py b/mne/report/report.py
index a99414697d9..18ca4be3768 100644
--- a/mne/report/report.py
+++ b/mne/report/report.py
@@ -587,9 +587,6 @@ def _plot_ica_properties_as_arrays(*, ica, inst, picks, n_jobs):
     """
     import matplotlib.pyplot as plt
 
-    if picks is None:
-        picks = list(range(ica.n_components_))
-
     def _plot_one_ica_property(*, ica, inst, pick):
         figs = ica.plot_properties(inst=inst, picks=pick, show=False)
         assert len(figs) == 1
@@ -1666,9 +1663,14 @@ def _add_ica_properties(
             )
             return
 
+        if picks is None:
+            picks = list(range(ica.n_components_))
+
         figs = _plot_ica_properties_as_arrays(
             ica=ica, inst=inst, picks=picks, n_jobs=n_jobs
         )
+        assert len(figs) == len(picks)
+
         captions = []
         for idx in range(len(figs)):
             caption = f"ICA component {picks[idx]}."
diff --git a/mne/report/tests/test_report.py b/mne/report/tests/test_report.py
index 9f8760dfbb6..67b065ad4fe 100644
--- a/mne/report/tests/test_report.py
+++ b/mne/report/tests/test_report.py
@@ -984,6 +984,7 @@ def test_manual_report_2d(tmp_path, invisible_fig):
         inst=epochs_baseline,
         picks=[0],
     )
+    r.add_ica(ica=ica, title="my ica with picks=None", inst=epochs_baseline, picks=None)
     r.add_covariance(cov=cov, info=raw_fname, title="my cov")
     r.add_forward(
         forward=fwd_fname,

From 3bc18ffec21c2d0293d6af6452948d6f56f73b5d Mon Sep 17 00:00:00 2001
From: Clemens Brunner 
Date: Fri, 3 Nov 2023 15:47:45 +0100
Subject: [PATCH 037/405] Fix inferring fiducials from EEGLAB (#12165)

---
 doc/changes/devel.rst   |  1 +
 mne/io/eeglab/eeglab.py | 26 ++++++++++++++++----------
 2 files changed, 17 insertions(+), 10 deletions(-)

diff --git a/doc/changes/devel.rst b/doc/changes/devel.rst
index fce4656104a..fcb51b8b7c3 100644
--- a/doc/changes/devel.rst
+++ b/doc/changes/devel.rst
@@ -83,6 +83,7 @@ Bugs
 - Fix bug with :func:`~mne.viz.plot_raw` where changing ``MNE_BROWSER_BACKEND`` via :func:`~mne.set_config` would have no effect within a Python session (:gh:`12078` by `Santeri Ruuskanen`_)
 - Improve handling of ``method`` argument in the channel interpolation function to support :class:`str` and raise helpful error messages (:gh:`12113` by `Mathieu Scheltienne`_)
 - Fix combination of ``DIN`` event channels into a single synthetic trigger channel ``STI 014`` by the MFF reader of :func:`mne.io.read_raw_egi` (:gh:`12122` by `Mathieu Scheltienne`_)
+- Fix bug with :func:`mne.io.read_raw_eeglab` and :func:`mne.read_epochs_eeglab` where automatic fiducial detection would fail for certain files (:gh:`12165` by `Clemens Brunner`_) 
 
 API changes
 ~~~~~~~~~~~
diff --git a/mne/io/eeglab/eeglab.py b/mne/io/eeglab/eeglab.py
index 9e08807db49..6cf92bfd7bf 100644
--- a/mne/io/eeglab/eeglab.py
+++ b/mne/io/eeglab/eeglab.py
@@ -165,17 +165,23 @@ def _get_montage_information(eeg, get_pos, *, montage_units):
         )
 
     lpa, rpa, nasion = None, None, None
-    if hasattr(eeg, "chaninfo") and len(eeg.chaninfo.get("nodatchans", [])):
-        for item in list(zip(*eeg.chaninfo["nodatchans"].values())):
-            d = dict(zip(eeg.chaninfo["nodatchans"].keys(), item))
-            if d.get("type", None) != "FID":
+    if hasattr(eeg, "chaninfo") and isinstance(eeg.chaninfo["nodatchans"], dict):
+        nodatchans = eeg.chaninfo["nodatchans"]
+        types = nodatchans.get("type", [])
+        descriptions = nodatchans.get("description", [])
+        xs = nodatchans.get("X", [])
+        ys = nodatchans.get("Y", [])
+        zs = nodatchans.get("Z", [])
+
+        for type_, description, x, y, z in zip(types, descriptions, xs, ys, zs):
+            if type_ != "FID":
                 continue
-            elif d.get("description", None) == "Nasion":
-                nasion = np.array([d["X"], d["Y"], d["Z"]])
-            elif d.get("description", None) == "Right periauricular point":
-                rpa = np.array([d["X"], d["Y"], d["Z"]])
-            elif d.get("description", None) == "Left periauricular point":
-                lpa = np.array([d["X"], d["Y"], d["Z"]])
+            if description == "Nasion":
+                nasion = np.array([x, y, z])
+            elif description == "Right periauricular point":
+                rpa = np.array([x, y, z])
+            elif description == "Left periauricular point":
+                lpa = np.array([x, y, z])
 
     # Always check this even if it's not used
     _check_option("montage_units", montage_units, ("m", "dm", "cm", "mm", "auto"))

From fcb59266b7616dd99908c6dac0e2295722b0c567 Mon Sep 17 00:00:00 2001
From: Jacob Woessner 
Date: Fri, 3 Nov 2023 10:00:00 -0500
Subject: [PATCH 038/405] [DOC] Add documentation for setting montage order
 (#12160)

---
 doc/changes/devel.rst  |  2 +-
 mne/_fiff/meas_info.py |  6 ++++++
 mne/_fiff/reference.py | 12 +++++++++++-
 3 files changed, 18 insertions(+), 2 deletions(-)

diff --git a/doc/changes/devel.rst b/doc/changes/devel.rst
index fcb51b8b7c3..84d24f284df 100644
--- a/doc/changes/devel.rst
+++ b/doc/changes/devel.rst
@@ -45,7 +45,7 @@ Enhancements
 - Add :class:`mne.viz.ui_events.UIEvent` linking for interactive colorbars, allowing users to link figures and change the colormap and limits interactively. This supports :func:`~mne.viz.plot_evoked_topomap`, :func:`~mne.viz.plot_ica_components`, :func:`~mne.viz.plot_tfr_topomap`, :func:`~mne.viz.plot_projs_topomap`, :meth:`~mne.Evoked.plot_image`, and :meth:`~mne.Epochs.plot_image` (:gh:`12057` by `Santeri Ruuskanen`_)
 - :func:`~mne.epochs.make_metadata` now accepts ``tmin=None`` and ``tmax=None``, which will bound the time window used for metadata generation by event names (instead of a fixed time). That way, you can now for example generate metadata spanning from one cue or fixation cross to the next, even if trial durations vary throughout the recording (:gh:`12118` by `Richard Höchenberger`_)
 - Add support for passing multiple labels to :func:`mne.minimum_norm.source_induced_power` (:gh:`12026` by `Erica Peterson`_, `Eric Larson`_, and `Daniel McCloy`_ )
-
+- Added documentation to :meth:`mne.io.Raw.set_montage` and :func:`mne.add_reference_channels` to specify that montages should be set after adding reference channels (:gh:`12160` by `Jacob Woessner`_)
 
 Bugs
 ~~~~
diff --git a/mne/_fiff/meas_info.py b/mne/_fiff/meas_info.py
index d7f94461a4d..5d67b77470c 100644
--- a/mne/_fiff/meas_info.py
+++ b/mne/_fiff/meas_info.py
@@ -410,6 +410,12 @@ def set_montage(
             a montage. Other channel types (e.g., MEG channels) should have
             their positions defined properly using their data reading
             functions.
+        .. warning::
+            Applying a montage will only set locations of channels that exist
+            at the time it is applied. This means when
+            :ref:`re-referencing `
+            make sure to apply the montage only after calling
+            :func:`mne.add_reference_channels`
         """
         # How to set up a montage to old named fif file (walk through example)
         # https://gist.github.com/massich/f6a9f4799f1fbeb8f5e8f8bc7b07d3df
diff --git a/mne/_fiff/reference.py b/mne/_fiff/reference.py
index 01d6e6e1230..0062bc4f40f 100644
--- a/mne/_fiff/reference.py
+++ b/mne/_fiff/reference.py
@@ -175,6 +175,14 @@ def add_reference_channels(inst, ref_channels, copy=True):
     -------
     inst : instance of Raw | Epochs | Evoked
         Data with added EEG reference channels.
+
+    Notes
+    -----
+    .. warning::
+        When :ref:`re-referencing `,
+        make sure to apply the montage using :meth:`mne.io.Raw.set_montage`
+        only after calling this function. Applying a montage will only set
+        locations of channels that exist at the time it is applied.
     """
     from ..epochs import BaseEpochs
     from ..evoked import Evoked
@@ -239,7 +247,9 @@ def add_reference_channels(inst, ref_channels, copy=True):
         ref_dig_array = np.full(12, np.nan)
         logger.info(
             "Location for this channel is unknown; consider calling "
-            "set_montage() again if needed."
+            "set_montage() after adding new reference channels if needed. "
+            "Applying a montage will only set locations of channels that "
+            "exist at the time it is applied."
         )
 
     for ch in ref_channels:

From f02e5576df7cfcee3fa435cafe66602e65ff76ec Mon Sep 17 00:00:00 2001
From: Rasmus Aagaard 
Date: Fri, 3 Nov 2023 19:24:24 +0100
Subject: [PATCH 039/405] Changed casting rule in np.clip to allow reading of
 raw GDF files (#12168)

Co-authored-by: roraa 
---
 doc/changes/devel.rst | 1 +
 doc/changes/names.inc | 2 ++
 mne/io/edf/edf.py     | 4 ++--
 3 files changed, 5 insertions(+), 2 deletions(-)

diff --git a/doc/changes/devel.rst b/doc/changes/devel.rst
index 84d24f284df..306f0689f4e 100644
--- a/doc/changes/devel.rst
+++ b/doc/changes/devel.rst
@@ -49,6 +49,7 @@ Enhancements
 
 Bugs
 ~~~~
+- Fix bug where :func:`mne.io.read_raw_gdf` would fail due to improper usage of ``np.clip`` (:gh:`12168` by :newcontrib:`Rasmus Aagaard`) 
 - Fix bugs with :func:`mne.preprocessing.realign_raw` where the start of ``other`` was incorrectly cropped; and onsets and durations in ``other.annotations`` were left unsynced with the resampled data (:gh:`11950` by :newcontrib:`Qian Chu`)
 - Fix bug where ``encoding`` argument was ignored when reading annotations from an EDF file (:gh:`11958` by :newcontrib:`Andrew Gilbert`)
 - Mark tests ``test_adjacency_matches_ft`` and ``test_fetch_uncompressed_file`` as network tests (:gh:`12041` by :newcontrib:`Maksym Balatsko`)
diff --git a/doc/changes/names.inc b/doc/changes/names.inc
index 82910422954..925e38bfd22 100644
--- a/doc/changes/names.inc
+++ b/doc/changes/names.inc
@@ -442,6 +442,8 @@
 
 .. _ramonapariciog: https://github.com/ramonapariciog
 
+.. _Rasmus Aagaard: https://github.com/rasgaard
+
 .. _Rasmus Zetter: https://people.aalto.fi/rasmus.zetter
 
 .. _Reza Nasri: https://github.com/rznas
diff --git a/mne/io/edf/edf.py b/mne/io/edf/edf.py
index e507a651676..d27aabae8a5 100644
--- a/mne/io/edf/edf.py
+++ b/mne/io/edf/edf.py
@@ -1443,7 +1443,7 @@ def _read_gdf_header(fname, exclude, include=None):
                 else:
                     chn = np.zeros(n_events, dtype=np.uint32)
                     dur = np.ones(n_events, dtype=np.uint32)
-                np.clip(dur, 1, np.inf, out=dur)
+                np.maximum(dur, 1, out=dur)
                 events = [n_events, pos, typ, chn, dur]
                 edf_info["event_sfreq"] = event_sr
 
@@ -1878,7 +1878,7 @@ def read_raw_gdf(
     input_fname = os.path.abspath(input_fname)
     ext = os.path.splitext(input_fname)[1][1:].lower()
     if ext != "gdf":
-        raise NotImplementedError(f"Only BDF files are supported, got {ext}.")
+        raise NotImplementedError(f"Only GDF files are supported, got {ext}.")
     return RawGDF(
         input_fname=input_fname,
         eog=eog,

From ec87fd8976a82ef7541c3114cd7955083eb278d6 Mon Sep 17 00:00:00 2001
From: Eric Larson 
Date: Fri, 3 Nov 2023 16:17:23 -0400
Subject: [PATCH 040/405] FIX: Fix bug with coreg scalars (#12164)

---
 doc/changes/devel.rst        |  2 +-
 mne/viz/backends/_pyvista.py | 15 ++++++++++++---
 2 files changed, 13 insertions(+), 4 deletions(-)

diff --git a/doc/changes/devel.rst b/doc/changes/devel.rst
index 306f0689f4e..8d02c056445 100644
--- a/doc/changes/devel.rst
+++ b/doc/changes/devel.rst
@@ -71,7 +71,7 @@ Bugs
 - Fix bug where :class:`mne.Info` HTML representations listed all channel counts instead of good channel counts under the heading "Good channels" (:gh:`12145` by `Eric Larson`_) 
 - Fix rendering glitches when plotting Neuromag/TRIUX sensors in :func:`mne.viz.plot_alignment` and related functions (:gh:`12098` by `Eric Larson`_)
 - Fix bug with delayed checking of :class:`info["bads"] ` (:gh:`12038` by `Eric Larson`_)
-- Fix bug with :ref:`mne coreg` where points inside the head surface were not shown (:gh:`12147` by `Eric Larson`_)
+- Fix bug with :ref:`mne coreg` where points inside the head surface were not shown (:gh:`12147`, :gh:`12164` by `Eric Larson`_)
 - Fix bug with :func:`mne.viz.plot_alignment` where ``sensor_colors`` were not handled properly on a per-channel-type basis (:gh:`12067` by `Eric Larson`_)
 - Fix handling of channel information in annotations when loading data from and exporting to EDF file (:gh:`11960` :gh:`12017` :gh:`12044` by `Paul Roujansky`_)
 - Add missing ``overwrite`` and ``verbose`` parameters to :meth:`Transform.save() ` (:gh:`12004` by `Marijn van Vliet`_)
diff --git a/mne/viz/backends/_pyvista.py b/mne/viz/backends/_pyvista.py
index b09314462dc..5cb89179ef5 100644
--- a/mne/viz/backends/_pyvista.py
+++ b/mne/viz/backends/_pyvista.py
@@ -655,6 +655,9 @@ def quiver3d(
         clim=None,
     ):
         _check_option("mode", mode, ALLOWED_QUIVER_MODES)
+        _validate_type(scale_mode, str, "scale_mode")
+        scale_map = dict(none=False, scalar="scalars", vector="vec")
+        _check_option("scale_mode", scale_mode, list(scale_map))
         with warnings.catch_warnings():
             warnings.filterwarnings("ignore", category=FutureWarning)
             factor = scale
@@ -667,7 +670,10 @@ def quiver3d(
             grid = UnstructuredGrid(*args)
             if scalars is None:
                 scalars = np.ones((n_points,))
-            grid.point_data["scalars"] = np.array(scalars)
+                mesh_scalars = None
+            else:
+                mesh_scalars = "scalars"
+            grid.point_data["scalars"] = np.array(scalars, float)
             grid.point_data["vec"] = vectors
             if mode == "2darrow":
                 return _arrow_glyph(grid, factor), grid
@@ -715,14 +721,17 @@ def quiver3d(
                 glyph.Update()
                 geom = glyph.GetOutput()
                 mesh = grid.glyph(
-                    orient="vec", scale=scale_mode == "vector", factor=factor, geom=geom
+                    orient="vec",
+                    scale=scale_map[scale_mode],
+                    factor=factor,
+                    geom=geom,
                 )
             actor = _add_mesh(
                 self.plotter,
                 mesh=mesh,
                 color=color,
                 opacity=opacity,
-                scalars=None,
+                scalars=mesh_scalars,
                 colormap=colormap,
                 show_scalar_bar=False,
                 backface_culling=backface_culling,

From b988e67d9916ed651c381b867e8bf10c3a470905 Mon Sep 17 00:00:00 2001
From: Scott Huberty <52462026+scott-huberty@users.noreply.github.com>
Date: Mon, 6 Nov 2023 05:25:13 -0500
Subject: [PATCH 041/405] FIX: skip empty lines in read_raw_eyelink (#12172)

---
 mne/io/eyelink/_utils.py | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/mne/io/eyelink/_utils.py b/mne/io/eyelink/_utils.py
index 65641e9cc43..3cbd06c52a6 100644
--- a/mne/io/eyelink/_utils.py
+++ b/mne/io/eyelink/_utils.py
@@ -122,6 +122,8 @@ def _parse_recording_blocks(fname):
                 is_recording_block = True
             if is_recording_block:
                 tokens = line.split()
+                if not tokens:
+                    continue  # skip empty lines
                 if tokens[0][0].isnumeric():  # Samples
                     data_dict["sample_lines"].append(tokens)
                 elif tokens[0] in data_dict["event_lines"].keys():

From 58afb0fb73e55296f21f6c68a643f2ea98f77483 Mon Sep 17 00:00:00 2001
From: Alexandre Gramfort 
Date: Mon, 6 Nov 2023 15:22:29 +0100
Subject: [PATCH 042/405] fix docstring in 60_sleep.py (#12171)

---
 tutorials/clinical/60_sleep.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/tutorials/clinical/60_sleep.py b/tutorials/clinical/60_sleep.py
index a878b7747e7..eefeda02070 100644
--- a/tutorials/clinical/60_sleep.py
+++ b/tutorials/clinical/60_sleep.py
@@ -249,7 +249,7 @@ def eeg_power_band(epochs):
 
     Returns
     -------
-    X : numpy array of shape [n_samples, 5]
+    X : numpy array of shape [n_samples, 5 * n_channels]
         Transformed data.
     """
     # specific frequency bands

From 55b0184b94ec1c3d6b2559eeed383e760220f516 Mon Sep 17 00:00:00 2001
From: Daniel McCloy 
Date: Mon, 6 Nov 2023 09:49:23 -0600
Subject: [PATCH 043/405] OpenSSF (#12175)

---
 tools/dev/Makefile                                | 15 +++++++++++++--
 ...ed-bug-reports.jq => unacknowledged-issues.jq} |  4 ++--
 2 files changed, 15 insertions(+), 4 deletions(-)
 rename tools/dev/{unacknowledged-bug-reports.jq => unacknowledged-issues.jq} (91%)

diff --git a/tools/dev/Makefile b/tools/dev/Makefile
index 2e9f9ad14f3..e2f61735a7e 100644
--- a/tools/dev/Makefile
+++ b/tools/dev/Makefile
@@ -3,8 +3,13 @@
 dev-activity: check_steering_committee.py
 	python check_steering_committee.py
 
-unacknowledged-bugs: unacknowledged-bug-reports.jq bug-reports-12-to-2-months-old.json
-	@jq -f unacknowledged-bug-reports.jq bug-reports-12-to-2-months-old.json
+unacknowledged-bug-reports: unacknowledged-issues.jq bug-reports-12-to-2-months-old.json
+	@echo "MUST acknowledge bug reports (OpenSSF criterion: 50%)"
+	@jq -f unacknowledged-issues.jq bug-reports-12-to-2-months-old.json
+
+unacknowledged-feature-requests: unacknowledged-issues.jq feature-requests-12-to-2-months-old.json
+	@echo "SHOULD acknowledge feature requests (OpenSSF criterion: 50%)"
+	@jq -f unacknowledged-issues.jq feature-requests-12-to-2-months-old.json
 
 bug-reports-12-to-2-months-old.json:
 	@echo "Querying GitHub REST API..."
@@ -12,5 +17,11 @@ bug-reports-12-to-2-months-old.json:
 	-l bug \
 	--json state,number,title,createdAt,comments > bug-reports-12-to-2-months-old.json
 
+feature-requests-12-to-2-months-old.json:
+	@echo "Querying GitHub REST API..."
+	@gh issue list \
+	-l enh \
+	--json state,number,title,createdAt,comments > feature-requests-12-to-2-months-old.json
+
 clean:
 	@rm bug-reports-12-to-2-months-old.json || true
diff --git a/tools/dev/unacknowledged-bug-reports.jq b/tools/dev/unacknowledged-issues.jq
similarity index 91%
rename from tools/dev/unacknowledged-bug-reports.jq
rename to tools/dev/unacknowledged-issues.jq
index 431d92be144..b91f5fc2e78 100644
--- a/tools/dev/unacknowledged-bug-reports.jq
+++ b/tools/dev/unacknowledged-issues.jq
@@ -1,4 +1,4 @@
-# Processor for `gh issue list` output that displays unacknowledged bug reports
+# Processor for `gh issue list` output that displays unacknowledged issues
 # that are 2-12 months old. The date range is specific to OpenSSF best practices.
 
 # `now` is in seconds since the unix epoch
@@ -28,5 +28,5 @@ map(
     "range": make_pretty_date_range,
     "has_dev_comments": map(select(.devComments > 0)) | length,
     "no_dev_comments": map(select(.devComments == 0) and .state == "OPEN") | length,
-    "unaddressed_bug_reports": map(select(.devComments == 0) | make_issue_url),
+    "unaddressed": map(select(.devComments == 0) | make_issue_url),
 }

From 8ea98e2dd542735e2ae950e5daf2bdcc363f90bd Mon Sep 17 00:00:00 2001
From: Eric Larson 
Date: Mon, 6 Nov 2023 11:30:47 -0500
Subject: [PATCH 044/405] MAINT: Add branch coverage (#12174)

---
 pyproject.toml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/pyproject.toml b/pyproject.toml
index f28204f5bca..a57b18928d4 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -128,7 +128,7 @@ ignore-decorators = [
 
 [tool.pytest.ini_options]
 addopts = """--durations=20 --doctest-modules -ra --cov-report= --tb=short \
-    --doctest-ignore-import-errors --junit-xml=junit-results.xml \
+    --cov-branch --doctest-ignore-import-errors --junit-xml=junit-results.xml \
     --ignore=doc --ignore=logo --ignore=examples --ignore=tutorials \
     --ignore=mne/gui/_*.py --ignore=mne/icons --ignore=tools \
     --ignore=mne/report/js_and_css \

From 8af12aae0ec3f1e73a19ce9be5739922712be299 Mon Sep 17 00:00:00 2001
From: "pre-commit-ci[bot]"
 <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Date: Mon, 6 Nov 2023 14:51:50 -0500
Subject: [PATCH 045/405] [pre-commit.ci] pre-commit autoupdate (#12177)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
---
 .pre-commit-config.yaml | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 6ff357ca832..c41418ed4b4 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -7,7 +7,7 @@ repos:
 
   # Ruff mne
   - repo: https://github.com/astral-sh/ruff-pre-commit
-    rev: v0.1.3
+    rev: v0.1.4
     hooks:
       - id: ruff
         name: ruff mne
@@ -16,7 +16,7 @@ repos:
 
   # Ruff tutorials and examples
   - repo: https://github.com/astral-sh/ruff-pre-commit
-    rev: v0.1.3
+    rev: v0.1.4
     hooks:
       - id: ruff
         name: ruff tutorials and examples

From 7b6b79583b5e738de08828670222e690c65085f1 Mon Sep 17 00:00:00 2001
From: Clemens Brunner 
Date: Tue, 7 Nov 2023 15:26:11 +0100
Subject: [PATCH 046/405] Tweak README.rst (#12166)

---
 README.rst | 196 +++++++++++++++++++++++++++++------------------------
 1 file changed, 108 insertions(+), 88 deletions(-)

diff --git a/README.rst b/README.rst
index a3d35deb76a..fdc620acd9f 100644
--- a/README.rst
+++ b/README.rst
@@ -1,87 +1,67 @@
 .. -*- mode: rst -*-
 
-|PyPI|_ |conda-forge|_ |Zenodo|_ |Discourse|_ |Codecov|_ |Bandit|_ |OpenSSF|_
-
 |MNE|_
 
-.. |PyPI| image:: https://img.shields.io/pypi/dm/mne.svg?label=PyPI
-.. _PyPI: https://pypi.org/project/mne/
-
-.. |conda-forge| image:: https://img.shields.io/conda/dn/conda-forge/mne.svg?label=Conda
-.. _conda-forge: https://anaconda.org/conda-forge/mne
-
-.. |Zenodo| image:: https://zenodo.org/badge/DOI/10.5281/zenodo.592483.svg
-.. _Zenodo: https://doi.org/10.5281/zenodo.592483
-
-.. |Discourse| image:: https://img.shields.io/discourse/status?label=Community&server=https%3A%2F%2Fmne.discourse.group%2F
-.. _Discourse: https://mne.discourse.group/
-
-.. |Codecov| image:: https://img.shields.io/codecov/c/github/mne-tools/mne-python?label=Coverage
-.. _Codecov: https://codecov.io/gh/mne-tools/mne-python
-
-.. |Bandit| image:: https://img.shields.io/badge/security-bandit-yellow.svg
-.. _Bandit: https://github.com/PyCQA/bandit
-
-.. |OpenSSF| image:: https://www.bestpractices.dev/projects/7783/badge
-.. _OpenSSF: https://www.bestpractices.dev/projects/7783
-
-.. |MNE| image:: https://mne.tools/stable/_static/mne_logo.svg
-.. _MNE: https://mne.tools/dev/
-
-
 MNE-Python
 ==========
 
-`MNE-Python software`_ is an open-source Python package for exploring,
+MNE-Python is an open-source Python package for exploring,
 visualizing, and analyzing human neurophysiological data such as MEG, EEG, sEEG,
 ECoG, and more. It includes modules for data input/output, preprocessing,
 visualization, source estimation, time-frequency analysis, connectivity analysis,
-machine learning, and statistics.
+machine learning, statistics, and more.
 
 
 Documentation
 ^^^^^^^^^^^^^
 
-`MNE documentation`_ for MNE-Python is available online.
+`Documentation`_ for MNE-Python encompasses installation instructions, tutorials,
+and examples for a wide variety of topics, contributing guidelines, and an API
+reference.
 
 
 Forum
 ^^^^^^
 
-Our user forum is https://mne.discourse.group and is the best place to ask
-questions about MNE-Python usage or about the contribution process. It also
-includes job opportunities and other announcements.
+The `user forum`_ is the best place to ask questions about MNE-Python usage or
+the contribution process. The forum also features job opportunities and other
+announcements.
+
+If you find a bug or have an idea for a new feature that should be added to
+MNE-Python, please use the
+`issue tracker `__ of
+our GitHub repository.
 
 
-Installing MNE-Python
-^^^^^^^^^^^^^^^^^^^^^
+Installation
+^^^^^^^^^^^^
 
-To install the latest stable version of MNE-Python, you can use pip_ in a terminal:
+To install the latest stable version of MNE-Python with minimal dependencies
+only, use pip_ in a terminal:
 
 .. code-block:: console
 
     $ pip install --upgrade mne
 
-- MNE-Python 0.17 was the last release to support Python 2.7
-- MNE-Python 0.18 requires Python 3.5 or higher
-- MNE-Python 0.21 requires Python 3.6 or higher
-- MNE-Python 0.24 requires Python 3.7 or higher
-- MNE-Python 1.4 requires Python 3.8 or higher
+The current MNE-Python release requires Python 3.8 or higher. MNE-Python 0.17
+was the last release to support Python 2.7.
 
-For more complete instructions and more advanced installation methods (e.g. for
-the latest development version), see the `installation guide`_.
+For more complete instructions, including our standalone installers and more
+advanced installation methods, please refer to the `installation guide`_.
 
 
-Get the latest code
-^^^^^^^^^^^^^^^^^^^
+Get the development version
+^^^^^^^^^^^^^^^^^^^^^^^^^^^
 
-To install the latest version of the code using pip_ open a terminal and type:
+To install the latest development version of MNE-Python using pip_, open a
+terminal and type:
 
 .. code-block:: console
 
     $ pip install --upgrade git+https://github.com/mne-tools/mne-python@main
 
-To get the latest code using `git `__, open a terminal and type:
+To clone the repository with `git `__, open a terminal
+and type:
 
 .. code-block:: console
 
@@ -93,55 +73,68 @@ Dependencies
 
 The minimum required dependencies to run MNE-Python are:
 
-- Python >= 3.8
-- NumPy >= 1.21.2
-- SciPy >= 1.7.1
-- Matplotlib >= 3.5.0
-- pooch >= 1.5
-- tqdm
-- Jinja2
-- decorator
-- lazy_loader
+- `Python `__ ≥ 3.8
+- `NumPy `__ ≥ 1.21.2
+- `SciPy `__ ≥ 1.7.1
+- `Matplotlib `__ ≥ 3.5.0
+- `Pooch `__ ≥ 1.5
+- `tqdm `__
+- `Jinja2 `__
+- `decorator `__
+- `lazy_loader `__
 
 For full functionality, some functions require:
 
-- Scikit-learn >= 1.0
-- joblib >= 0.15 (for parallelization control)
-- mne-qt-browser >= 0.1 (for fast raw data visualization)
-- Qt5 >= 5.12 via one of the following bindings (for fast raw data visualization and interactive 3D visualization):
-
-  - PyQt6 >= 6.0
-  - PySide6 >= 6.0
-  - PyQt5 >= 5.12
-  - PySide2 >= 5.12
+- `scikit-learn `__ ≥ 1.0
+- `Joblib `__ ≥ 0.15 (for parallelization)
+- `mne-qt-browser `__ ≥ 0.1 (for fast raw data visualization)
+- `Qt `__ ≥ 5.12 via one of the following bindings (for fast raw data visualization and interactive 3D visualization):
+
+  - `PyQt6 `__ ≥ 6.0
+  - `PySide6 `__ ≥ 6.0
+  - `PyQt5 `__ ≥ 5.12
+  - `PySide2 `__ ≥ 5.12
+
+- `Numba `__ ≥ 0.54.0
+- `NiBabel `__ ≥ 3.2.1
+- `OpenMEEG `__ ≥ 2.5.6
+- `pandas `__ ≥ 1.3.2
+- `Picard `__ ≥ 0.3
+- `CuPy `__ ≥ 9.0.0 (for NVIDIA CUDA acceleration)
+- `DIPY `__ ≥ 1.4.0
+- `imageio `__ ≥ 2.8.0
+- `PyVista `__ ≥ 0.32 (for 3D visualization)
+- `PyVistaQt `__ ≥ 0.4 (for 3D visualization)
+- `mffpy `__ ≥ 0.5.7
+- `h5py `__
+- `h5io `__
+- `pymatreader `__
+
+
+Contributing
+^^^^^^^^^^^^
 
-- Numba >= 0.54.0
-- NiBabel >= 3.2.1
-- OpenMEEG >= 2.5.6
-- Pandas >= 1.3.2
-- Picard >= 0.3
-- CuPy >= 9.0.0 (for NVIDIA CUDA acceleration)
-- DIPY >= 1.4.0
-- Imageio >= 2.8.0
-- PyVista >= 0.32 (for 3D visualization)
-- pyvistaqt >= 0.4 (for 3D visualization)
-- mffpy >= 0.5.7
-- h5py
-- h5io
-- pymatreader
+Please see the `contributing guidelines `__ on our documentation website.
 
-Contributing to MNE-Python
-^^^^^^^^^^^^^^^^^^^^^^^^^^
 
-Please see the documentation on the MNE-Python homepage:
+About
+^^^^^
 
-https://mne.tools/dev/install/contributing.html
++---------+------------+----------------+
+| CI      | |Codecov|_ | |Bandit|_      |
++---------+------------+----------------+
+| Package | |PyPI|_    | |conda-forge|_ |
++---------+------------+----------------+
+| Docs    | |Docs|_    | |Discourse|_   |
++---------+------------+----------------+
+| Meta    | |Zenodo|_  | |OpenSSF|_     |
++---------+------------+----------------+
 
 
-Licensing
-^^^^^^^^^
+License
+^^^^^^^
 
-MNE-Python is **BSD-licenced** (BSD-3-Clause):
+MNE-Python is **BSD-licensed** (BSD-3-Clause):
 
     This software is OSI Certified Open Source Software.
     OSI Certified is a certification mark of the Open Source Initiative.
@@ -177,7 +170,34 @@ MNE-Python is **BSD-licenced** (BSD-3-Clause):
     damage.**
 
 
-.. _MNE-Python software: https://mne.tools/dev/
-.. _MNE documentation: https://mne.tools/dev/overview/index.html
+.. _Documentation: https://mne.tools/dev/
+.. _user forum: https://mne.discourse.group
 .. _installation guide: https://mne.tools/dev/install/index.html
 .. _pip: https://pip.pypa.io/en/stable/
+
+.. |PyPI| image:: https://img.shields.io/pypi/dm/mne.svg?label=PyPI
+.. _PyPI: https://pypi.org/project/mne/
+
+.. |conda-forge| image:: https://img.shields.io/conda/dn/conda-forge/mne.svg?label=Conda
+.. _conda-forge: https://anaconda.org/conda-forge/mne
+
+.. |Docs| image:: https://img.shields.io/badge/Docs-online-green?label=Documentation
+.. _Docs: https://mne.tools/dev/
+
+.. |Zenodo| image:: https://zenodo.org/badge/DOI/10.5281/zenodo.592483.svg
+.. _Zenodo: https://doi.org/10.5281/zenodo.592483
+
+.. |Discourse| image:: https://img.shields.io/discourse/status?label=Forum&server=https%3A%2F%2Fmne.discourse.group%2F
+.. _Discourse: https://mne.discourse.group/
+
+.. |Codecov| image:: https://img.shields.io/codecov/c/github/mne-tools/mne-python?label=Coverage
+.. _Codecov: https://codecov.io/gh/mne-tools/mne-python
+
+.. |Bandit| image:: https://img.shields.io/badge/Security-Bandit-yellow.svg
+.. _Bandit: https://github.com/PyCQA/bandit
+
+.. |OpenSSF| image:: https://www.bestpractices.dev/projects/7783/badge
+.. _OpenSSF: https://www.bestpractices.dev/projects/7783
+
+.. |MNE| image:: https://mne.tools/stable/_static/mne_logo.svg
+.. _MNE: https://mne.tools/dev/

From cc377dabd5a4e449d2bc706470a2ed16ea2339fe Mon Sep 17 00:00:00 2001
From: Eric Larson 
Date: Tue, 7 Nov 2023 11:16:53 -0500
Subject: [PATCH 047/405] ENH: Enable sensor-specific OPM coregistration in mne
 coreg (#11405)

---
 .circleci/config.yml                          |  19 +-
 doc/api/datasets.rst                          |   1 +
 doc/documentation/datasets.rst                |  13 ++
 examples/datasets/kernel_phantom.py           | 103 +++++++++
 mne/_fiff/_digitization.py                    |   3 +
 mne/bem.py                                    |   5 +-
 mne/channels/channels.py                      |   4 +
 mne/coreg.py                                  |  10 +-
 mne/data/helmets/Kernel_Flux.fif.gz           | Bin 0 -> 493001 bytes
 mne/data/helmets/Kernel_Flux_ch_pos.txt       | 202 ++++++++++++++++++
 mne/datasets/__init__.pyi                     |   2 +
 mne/datasets/config.py                        |   8 +
 mne/datasets/phantom_kernel/__init__.py       |   3 +
 mne/datasets/phantom_kernel/phantom_kernel.py |  32 +++
 mne/datasets/utils.py                         |   2 +-
 mne/gui/_coreg.py                             |  59 +++--
 mne/surface.py                                |  79 ++++++-
 mne/tests/test_transforms.py                  |  17 ++
 mne/transforms.py                             |  52 +++++
 mne/utils/config.py                           |   1 +
 mne/utils/docs.py                             |  26 ++-
 mne/viz/_3d.py                                |  93 ++++++--
 mne/viz/_brain/_brain.py                      |  27 +--
 mne/viz/backends/_pyvista.py                  |   2 +
 mne/viz/tests/test_3d.py                      |   4 +-
 mne/viz/utils.py                              |   4 +-
 tools/circleci_download.sh                    |   3 +
 tutorials/inverse/35_dipole_orientations.py   |   4 +-
 .../inverse/80_brainstorm_phantom_elekta.py   |   4 +-
 tutorials/inverse/90_phantom_4DBTi.py         |   4 +-
 30 files changed, 690 insertions(+), 96 deletions(-)
 create mode 100644 examples/datasets/kernel_phantom.py
 create mode 100644 mne/data/helmets/Kernel_Flux.fif.gz
 create mode 100644 mne/data/helmets/Kernel_Flux_ch_pos.txt
 create mode 100644 mne/datasets/phantom_kernel/__init__.py
 create mode 100644 mne/datasets/phantom_kernel/phantom_kernel.py

diff --git a/.circleci/config.yml b/.circleci/config.yml
index 6f11c346045..492454e057a 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -223,13 +223,16 @@ jobs:
             - data-cache-fsaverage
       - restore_cache:
           keys:
-            - data-cache-bst-phantom-ctf
+            - data-cache-bst-raw
       - restore_cache:
           keys:
-            - data-cache-bst-raw
+            - data-cache-bst-phantom-ctf
       - restore_cache:
           keys:
             - data-cache-bst-phantom-elekta
+      - restore_cache:
+          keys:
+            - data-cache-bst-phantom-kernel
       - restore_cache:
           keys:
             - data-cache-bst-auditory
@@ -368,18 +371,22 @@ jobs:
           key: data-cache-fsaverage
           paths:
             - ~/mne_data/MNE-fsaverage-data  # (762 M)
-      - save_cache:
-          key: data-cache-bst-phantom-ctf
-          paths:
-            - ~/mne_data/MNE-brainstorm-data/bst_phantom_ctf  # (177 M)
       - save_cache:
           key: data-cache-bst-raw
           paths:
             - ~/mne_data/MNE-brainstorm-data/bst_raw  # (830 M)
+      - save_cache:
+          key: data-cache-bst-phantom-ctf
+          paths:
+            - ~/mne_data/MNE-brainstorm-data/bst_phantom_ctf  # (177 M)
       - save_cache:
           key: data-cache-bst-phantom-elekta
           paths:
             - ~/mne_data/MNE-brainstorm-data/bst_phantom_elekta  # (1.4 G)
+      - save_cache:
+          key: data-cache-bst-phantom-kernel
+          paths:
+            - ~/mne_data/MNE-phantom-kernel-data  # (362 M)
       - save_cache:
           key: data-cache-bst-auditory
           paths:
diff --git a/doc/api/datasets.rst b/doc/api/datasets.rst
index b46e3e387a9..5f5e3762044 100644
--- a/doc/api/datasets.rst
+++ b/doc/api/datasets.rst
@@ -41,6 +41,7 @@ Datasets
    ucl_opm_auditory.data_path
    visual_92_categories.data_path
    phantom_4dbti.data_path
+   phantom_kernel.data_path
    refmeg_noise.data_path
    ssvep.data_path
    erp_core.data_path
diff --git a/doc/documentation/datasets.rst b/doc/documentation/datasets.rst
index 348da772e90..ef63a3b139e 100644
--- a/doc/documentation/datasets.rst
+++ b/doc/documentation/datasets.rst
@@ -293,6 +293,19 @@ the MEG center in La Timone hospital in Marseille.
 
     * :ref:`tut-phantom-4Dbti`
 
+Kernel OPM phantom dataset
+==========================
+:func:`mne.datasets.phantom_kernel.data_path`.
+
+This dataset was obtained with a Neuromag phantom in a Kernel Flux (720-sensor)
+system at ILABS at the University of Washington. Only 7 out of 42 possible modules
+were active for testing purposes, yielding 121 channels of data with limited coverage
+(mostly occipital and parietal).
+
+.. topic:: Examples
+
+    * :ref:`ex-kernel-opm-phantom`
+
 OPM
 ===
 :func:`mne.datasets.opm.data_path`
diff --git a/examples/datasets/kernel_phantom.py b/examples/datasets/kernel_phantom.py
new file mode 100644
index 00000000000..51c6c847de6
--- /dev/null
+++ b/examples/datasets/kernel_phantom.py
@@ -0,0 +1,103 @@
+"""
+.. _ex-kernel-opm-phantom:
+
+Kernel OPM phantom data
+=======================
+
+In this dataset, a Neuromag phantom was placed inside the Kernel OPM helmet and
+stimulated with 7 modules active (121 channels). Here we show some example traces.
+"""
+
+import numpy as np
+
+import mne
+
+data_path = mne.datasets.phantom_kernel.data_path()
+fname = data_path / "phantom_32_100nam_raw.fif"
+raw = mne.io.read_raw_fif(fname).load_data()
+events = mne.find_events(raw, stim_channel="STI101")
+
+# Bads identified by inspecting averages
+raw.info["bads"] = [
+    "RC2.bx.ave",
+    "LC3.bx.ave",
+    "RC2.by.7",
+    "RC2.bz.7",
+    "RC2.bx.4",
+    "RC2.by.4",
+    "LC3.bx.5",
+]
+# Drop the module-average channels
+raw.drop_channels([ch_name for ch_name in raw.ch_names if ".ave" in ch_name])
+# Add field correction projectors
+raw.add_proj(mne.preprocessing.compute_proj_hfc(raw.info, order=2))
+raw.pick("meg", exclude="bads")
+raw.filter(0.5, 40)
+epochs = mne.Epochs(
+    raw,
+    events,
+    tmin=-0.1,
+    tmax=0.25,
+    decim=5,
+    preload=True,
+    baseline=(None, 0),
+)
+evoked = epochs["17"].average()  # a high-SNR dipole for these data
+fig = evoked.plot()
+t_peak = 0.016  # based on visual inspection of evoked
+fig.axes[0].axvline(t_peak, color="k", ls=":", lw=3, zorder=2)
+
+# %%
+# The data covariance has an interesting structure because of densely packed sensors:
+cov = mne.compute_covariance(epochs, tmax=-0.01)
+mne.viz.plot_cov(cov, raw.info)
+
+# %%
+# So let's be careful and compute rank ahead of time and regularize:
+
+rank = mne.compute_rank(epochs, tol=1e-3, tol_kind="relative")
+cov = mne.compute_covariance(epochs, tmax=-0.01, rank=rank, method="shrunk")
+mne.viz.plot_cov(cov, raw.info)
+
+# %%
+# Look at our alignment:
+
+sphere = mne.make_sphere_model(r0=(0.0, 0.0, 0.0), head_radius=0.08)
+trans = mne.transforms.Transform("head", "mri", np.eye(4))
+align_kwargs = dict(
+    trans=trans,
+    bem=sphere,
+    surfaces={"outer_skin": 0.2},
+    show_axes=True,
+)
+mne.viz.plot_alignment(
+    raw.info,
+    coord_frame="meg",
+    meg=dict(sensors=1.0, helmet=0.05),
+    **align_kwargs,
+)
+
+# %%
+# Let's do dipole fits, which are not great because the dev_head_t is approximate and
+# the sensor coverage is sparse:
+
+data = list()
+for ii in range(1, 33):
+    evoked = epochs[str(ii)][1:-1].average().crop(t_peak, t_peak)
+    data.append(evoked.data[:, 0])
+evoked = mne.EvokedArray(np.array(data).T, evoked.info, tmin=0.0)
+del epochs
+dip, residual = mne.fit_dipole(evoked, cov, sphere, n_jobs=None)
+actual_pos, actual_ori = mne.dipole.get_phantom_dipoles()
+actual_amp = np.ones(len(dip))  # fake amp, needed to create Dipole instance
+actual_gof = np.ones(len(dip))  # fake GOF, needed to create Dipole instance
+dip_true = mne.Dipole(dip.times, actual_pos, actual_amp, actual_ori, actual_gof)
+
+fig = mne.viz.plot_alignment(
+    evoked.info, coord_frame="head", meg="sensors", **align_kwargs
+)
+mne.viz.plot_dipole_locations(
+    dipoles=dip_true, mode="arrow", color=(0.0, 0.0, 0.0), fig=fig
+)
+mne.viz.plot_dipole_locations(dipoles=dip, mode="arrow", color=(0.2, 1.0, 0.5), fig=fig)
+mne.viz.set_3d_view(figure=fig, azimuth=30, elevation=70, distance=0.4)
diff --git a/mne/_fiff/_digitization.py b/mne/_fiff/_digitization.py
index 932429d9a11..b8cae6f3f92 100644
--- a/mne/_fiff/_digitization.py
+++ b/mne/_fiff/_digitization.py
@@ -293,6 +293,7 @@ def _get_data_as_dict_from_dig(dig, exclude_ref_channel=True):
     # Split up the dig points by category
     hsp, hpi, elp = list(), list(), list()
     fids, dig_ch_pos_location = dict(), list()
+    dig = [] if dig is None else dig
 
     for d in dig:
         if d["kind"] == FIFF.FIFFV_POINT_CARDINAL:
@@ -307,6 +308,8 @@ def _get_data_as_dict_from_dig(dig, exclude_ref_channel=True):
                 dig_ch_pos_location.append(d["r"])
 
     dig_coord_frames = set([d["coord_frame"] for d in dig])
+    if len(dig_coord_frames) == 0:
+        dig_coord_frames = set([FIFF.FIFFV_COORD_HEAD])
     if len(dig_coord_frames) != 1:
         raise RuntimeError(
             "Only single coordinate frame in dig is supported, "
diff --git a/mne/bem.py b/mne/bem.py
index e7b9704be1e..eaa5b1f2d4f 100644
--- a/mne/bem.py
+++ b/mne/bem.py
@@ -1751,6 +1751,7 @@ def _add_gamma_multipliers(bem):
     FIFF.FIFFV_BEM_SURF_ID_SKULL: "outer skull",
     FIFF.FIFFV_BEM_SURF_ID_HEAD: "outer skin ",
     FIFF.FIFFV_BEM_SURF_ID_UNKNOWN: "unknown    ",
+    FIFF.FIFFV_MNE_SURF_MEG_HELMET: "MEG helmet ",
 }
 _sm_surf_name = {
     FIFF.FIFFV_BEM_SURF_ID_BRAIN: "brain",
@@ -1758,6 +1759,7 @@ def _add_gamma_multipliers(bem):
     FIFF.FIFFV_BEM_SURF_ID_SKULL: "outer skull",
     FIFF.FIFFV_BEM_SURF_ID_HEAD: "outer skin ",
     FIFF.FIFFV_BEM_SURF_ID_UNKNOWN: "unknown    ",
+    FIFF.FIFFV_MNE_SURF_MEG_HELMET: "helmet",
 }
 
 
@@ -1850,7 +1852,8 @@ def _write_bem_surfaces_block(fid, surfs):
     """Write bem surfaces to open file handle."""
     for surf in surfs:
         start_block(fid, FIFF.FIFFB_BEM_SURF)
-        write_float(fid, FIFF.FIFF_BEM_SIGMA, surf["sigma"])
+        if "sigma" in surf:
+            write_float(fid, FIFF.FIFF_BEM_SIGMA, surf["sigma"])
         write_int(fid, FIFF.FIFF_BEM_SURF_ID, surf["id"])
         write_int(fid, FIFF.FIFF_MNE_COORD_FRAME, surf["coord_frame"])
         write_int(fid, FIFF.FIFF_BEM_SURF_NNODE, surf["np"])
diff --git a/mne/channels/channels.py b/mne/channels/channels.py
index 6e975e1cade..725aa884493 100644
--- a/mne/channels/channels.py
+++ b/mne/channels/channels.py
@@ -104,6 +104,10 @@ def _get_meg_system(info):
                 system = "ARTEMIS123"
                 have_helmet = False
                 break
+            elif coil_type == FIFF.FIFFV_COIL_KERNEL_OPM_MAG_GEN1:
+                system = "Kernel_Flux"
+                have_helmet = True
+                break
     else:
         system = "unknown"
         have_helmet = False
diff --git a/mne/coreg.py b/mne/coreg.py
index c50d3d00274..7591eff1322 100644
--- a/mne/coreg.py
+++ b/mne/coreg.py
@@ -1550,7 +1550,9 @@ def _setup_bem(self):
         low_res_path = _find_head_bem(self._subject, self._subjects_dir, high_res=False)
         if high_res_path is None and low_res_path is None:
             raise RuntimeError(
-                "No standard head model was " f"found for subject {self._subject}"
+                "No standard head model was "
+                f"found for subject {self._subject} in "
+                f"{self._subjects_dir}"
             )
         if high_res_path is not None:
             self._bem_high_res = _read_surface(
@@ -1987,9 +1989,9 @@ def fit_fiducials(
         return self
 
     def _setup_icp(self, n_scale_params):
-        head_pts = list()
-        mri_pts = list()
-        weights = list()
+        head_pts = [np.zeros((0, 3))]
+        mri_pts = [np.zeros((0, 3))]
+        weights = [np.zeros(0)]
         if self._has_dig_data and self._hsp_weight > 0:  # should be true
             head_pts.append(self._filtered_extra_points)
             mri_pts.append(
diff --git a/mne/data/helmets/Kernel_Flux.fif.gz b/mne/data/helmets/Kernel_Flux.fif.gz
new file mode 100644
index 0000000000000000000000000000000000000000..66504d7b4a916e6cf8f068c6478a7f39a94487a1
GIT binary patch
literal 493001
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"VvG@aP`t0Vv44r$l8+G
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%0DP9bYIN>5M5!EMfTmTleu3m+AC
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
z
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

LX1!KEcs6RedcXAJLW?C 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 z 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

sh6~~F& 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�#ed71Pptg`#rE@= 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 +# +# License: BSD Style. + +from ...utils import verbose +from ..utils import _data_path_doc, _download_mne_dataset, _get_version, _version_doc + + +@verbose +def data_path( + path=None, force_update=False, update_path=True, download=True, *, verbose=None +): # noqa: D103 + return _download_mne_dataset( + name="phantom_kernel", + processor="nested_untar", + path=path, + force_update=force_update, + update_path=update_path, + download=download, + ) + + +data_path.__doc__ = _data_path_doc.format( + name="phantom_kernel", conf="MNE_DATASETS_PHANTOM_KERNEL_PATH" +) + + +def get_version(): # noqa: D103 + return _get_version("phantom_kernel") + + +get_version.__doc__ = _version_doc.format(name="phantom_kernel") diff --git a/mne/datasets/utils.py b/mne/datasets/utils.py index 9ff4efd3400..5ee5539ce1b 100644 --- a/mne/datasets/utils.py +++ b/mne/datasets/utils.py @@ -328,7 +328,7 @@ def _download_all_example_data(verbose=True): "refmeg_noise ssvep epilepsy_ecog ucl_opm_auditory eyelink " "erp_core brainstorm.bst_raw brainstorm.bst_auditory " "brainstorm.bst_resting brainstorm.bst_phantom_ctf " - "brainstorm.bst_phantom_elekta" + "brainstorm.bst_phantom_elekta phantom_kernel" ).split(): mod = importlib.import_module(f"mne.datasets.{kind}") data_path_func = getattr(mod, "data_path") diff --git a/mne/gui/_coreg.py b/mne/gui/_coreg.py index e07848db693..2ee787c7c31 100644 --- a/mne/gui/_coreg.py +++ b/mne/gui/_coreg.py @@ -56,7 +56,7 @@ _plot_helmet, _plot_hpi_coils, _plot_mri_fiducials, - _plot_sensors, + _plot_sensors_3d, ) from ..viz.backends._utils import _qt_app_exec, _qt_safe_window from ..viz.utils import safe_event @@ -1092,11 +1092,14 @@ def _update_distance_estimation(self): ) dists = self.coreg.compute_dig_mri_distances() * 1e3 if self._hsp_weight > 0: - value += ( - "\nHSP <-> MRI (mean/min/max): " - f"{np.mean(dists):.2f} " - f"/ {np.min(dists):.2f} / {np.max(dists):.2f} mm" - ) + if len(dists) == 0: + value += "\nNo head shape points found." + else: + value += ( + "\nHSP <-> MRI (mean/min/max): " + f"{np.mean(dists):.2f} " + f"/ {np.min(dists):.2f} / {np.max(dists):.2f} mm" + ) self._forward_widget_command("fit_label", "set_value", value) def _update_parameters(self): @@ -1266,35 +1269,29 @@ def _add_channels(self): plot_types["meg"] = ["sensors"] if self._fnirs_channels: plot_types["fnirs"] = ["sources", "detectors"] - sens_actors = list() - # until opacity can be specified using a dict, we need to iterate - sensor_opacity = dict( + sensor_alpha = dict( eeg=self._defaults["sensor_opacity"], fnirs=self._defaults["sensor_opacity"], meg=0.25, ) - for ch_type, plot_type in plot_types.items(): - picks = pick_types(self._info, ref_meg=False, **{ch_type: True}) - if not (len(picks) and plot_type): - continue - logger.debug(f"Drawing {ch_type} sensors") - these_actors = _plot_sensors( - self._renderer, - self._info, - self._to_cf_t, - picks=picks, - warn_meg=False, - head_surf=self._head_geo, - units="m", - sensor_opacity=sensor_opacity[ch_type], - orient_glyphs=self._orient_glyphs, - scale_by_distance=self._scale_by_distance, - surf=self._head_geo, - check_inside=self._check_inside, - nearest=self._nearest, - **plot_types, - ) - sens_actors.extend(sum(these_actors.values(), list())) + picks = pick_types(self._info, ref_meg=False, meg=True, eeg=True, fnirs=True) + these_actors = _plot_sensors_3d( + self._renderer, + self._info, + self._to_cf_t, + picks=picks, + warn_meg=False, + head_surf=self._head_geo, + units="m", + sensor_alpha=sensor_alpha, + orient_glyphs=self._orient_glyphs, + scale_by_distance=self._scale_by_distance, + surf=self._head_geo, + check_inside=self._check_inside, + nearest=self._nearest, + **plot_types, + ) + sens_actors = sum(these_actors.values(), list()) self._update_actor("sensors", sens_actors) def _add_head_surface(self): diff --git a/mne/surface.py b/mne/surface.py index d0aac3abe0d..93d9f89caca 100644 --- a/mne/surface.py +++ b/mne/surface.py @@ -8,6 +8,7 @@ # Many of the computations in this code were derived from Matti Hämäläinen's # C code. +import json import time import warnings from collections import OrderedDict @@ -15,6 +16,7 @@ from functools import lru_cache, partial from glob import glob from os import path as op +from pathlib import Path import numpy as np from scipy.ndimage import binary_dilation @@ -28,8 +30,11 @@ from .parallel import parallel_func from .transforms import ( Transform, + _angle_between_quats, _cart_to_sph, + _fit_matched_points, _get_trans, + _MatchedDisplacementFieldInterpolator, _pol_to_cart, apply_trans, transform_surface_to, @@ -52,6 +57,9 @@ warn, ) +_helmet_path = Path(__file__).parent / "data" / "helmets" + + ############################################################################### # AUTOMATED SURFACE FINDING @@ -157,8 +165,26 @@ def _get_head_surface(subject, source, subjects_dir, on_defects, raise_error=Tru return surf +# New helmets can be written for example with: +# +# import os.path as op +# import mne +# from mne.io.constants import FIFF +# surf = mne.read_surface('kernel.obj', return_dict=True)[-1] +# surf['rr'] *= 1000 # needs to be in mm +# mne.surface.complete_surface_info(surf, copy=False, do_neighbor_tri=False) +# surf['coord_frame'] = FIFF.FIFFV_COORD_DEVICE +# surfs = mne.bem._surfaces_to_bem( +# [surf], ids=[FIFF.FIFFV_MNE_SURF_MEG_HELMET], sigmas=[1.], +# incomplete='ignore') +# del surfs[0]['sigma'] +# bem_fname = op.join(op.dirname(mne.__file__), 'data', 'helmets', +# 'kernel.fif.gz') +# mne.write_bem_surfaces(bem_fname, surfs, overwrite=True) + + @verbose -def get_meg_helmet_surf(info, trans=None, verbose=None): +def get_meg_helmet_surf(info, trans=None, *, verbose=None): """Load the MEG helmet associated with the MEG sensors. Parameters @@ -186,10 +212,11 @@ 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) - fname = op.join(op.split(__file__)[0], "data", "helmets", system + ".fif.gz") + fname = _helmet_path / f"{system}.fif.gz" surf = read_bem_surfaces( fname, False, FIFF.FIFFV_MNE_SURF_MEG_HELMET, verbose=False ) + surf = _scale_helmet_to_sensors(system, surf, info) else: rr = np.array( [ @@ -229,6 +256,54 @@ def get_meg_helmet_surf(info, trans=None, verbose=None): return surf +def _scale_helmet_to_sensors(system, surf, info): + fname = _helmet_path / f"{system}_ch_pos.txt" + if not fname.is_file(): + return surf + with open(fname) as fid: + ch_pos_from = json.load(fid) + # find correspondence + fro, to = list(), list() + for key, f_ in ch_pos_from.items(): + t_ = [ch["loc"][:3] for ch in info["chs"] if ch["ch_name"].startswith(key)] + if not len(t_): + continue + fro.append(f_) + to.append(np.mean(t_, axis=0)) + if len(fro) < 4: + logger.info( + "Using untransformed helmet, not enough sensors found to deform to match " + f"acquisition based on sensor positions (got {len(fro)}, need at least 4)" + ) + return surf + fro = np.array(fro, float) + to = np.array(to, float) + delta = np.ptp(surf["rr"], axis=0) * 0.1 # 10% beyond bounds + extrema = np.array([surf["rr"].min(0) - delta, surf["rr"].max(0) + delta]) + interp = _MatchedDisplacementFieldInterpolator(fro, to, extrema=extrema) + new_rr = interp(surf["rr"]) + try: + quat, sc = _fit_matched_points(surf["rr"], new_rr) + except np.linalg.LinAlgError as exc: + logger.info( + f"Using untransformed helmet, deformation using {len(fro)} points " + f"failed ({exc})" + ) + return surf + rot = np.rad2deg(_angle_between_quats(quat[:3])) + tr = 1000 * np.linalg.norm(quat[3:]) + logger.info( + f" Deforming CAD helmet to match {len(fro)} acquisition sensor positions:" + ) + 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") + surf["rr"] = new_rr + complete_surface_info(surf, copy=False, verbose=False) + return surf + + def _reorder_ccw(rrs, tris): """Reorder tris of a convex hull to be wound counter-clockwise.""" # This ensures that rendering with front-/back-face culling works properly diff --git a/mne/tests/test_transforms.py b/mne/tests/test_transforms.py index a09071d571b..36c533a59a0 100644 --- a/mne/tests/test_transforms.py +++ b/mne/tests/test_transforms.py @@ -31,6 +31,7 @@ _find_vector_rotation, _fit_matched_points, _get_trans, + _MatchedDisplacementFieldInterpolator, _pol_to_cart, _quat_real, _quat_to_affine, @@ -632,3 +633,19 @@ def test_volume_registration(): ], atol=0.001, ) + + +def test_displacement_field(): + """Test that our matched point deformation works.""" + to = np.array([[5, 4, 1], [6, 1, 0], [4, -1, 1], [3, 3, 0]], float) + fro = np.array([[0, 2, 2], [2, 2, 1], [2, 0, 2], [0, 0, 1]], float) + interp = _MatchedDisplacementFieldInterpolator(fro, to) + fro_t = interp(fro) + assert_allclose(to, fro_t, atol=1e-12) + # check midpoints (should all be decent) + for a in range(len(to)): + for b in range(a + 1, len(to)): + to_ = np.mean(to[[a, b]], axis=0) + fro_ = np.mean(fro[[a, b]], axis=0) + fro_t = interp(fro_) + assert_allclose(to_, fro_t, atol=1e-12) diff --git a/mne/transforms.py b/mne/transforms.py index f8a4c813025..c25ca958cf9 100644 --- a/mne/transforms.py +++ b/mne/transforms.py @@ -2072,3 +2072,55 @@ def apply_volume_registration_points( info2.set_montage(montage2) # converts to head coordinates return info2, trans2 + + +class _MatchedDisplacementFieldInterpolator: + """Interpolate from matched points using a displacement field in ND. + + For a demo, see + https://gist.github.com/larsoner/fbe32d57996848395854d5e59dff1e10 + and related tests. + """ + + def __init__(self, fro, to, *, extrema=None): + from scipy.interpolate import LinearNDInterpolator + + fro = np.array(fro, float) + to = np.array(to, float) + assert fro.shape == to.shape + assert fro.ndim == 2 + # this restriction is only necessary because it's what + # _fit_matched_points requires + assert fro.shape[1] == 3 + + # Prealign using affine + uniform scaling + self._quat, self._scale = _fit_matched_points(fro, to, scale=True) + trans = _quat_to_affine(self._quat) + trans[:3, :3] *= self._scale + self._affine = trans + fro = apply_trans(trans, fro) + + # Add points at extrema + if extrema is None: + delta = (to.max(axis=0) - to.min(axis=0)) / 2.0 + assert (delta > 0).all() + extrema = np.array([fro.min(axis=0) - delta, fro.max(axis=0) + delta]) + assert extrema.shape == (2, 3) # min, max + self._extrema = np.array(np.meshgrid(*extrema.T)).T.reshape(-1, fro.shape[-1]) + fro_concat = np.concatenate((fro, self._extrema)) + to_concat = np.concatenate((to, self._extrema)) + + # Compute the interpolator (which internally uses Delaunay) + self._interp = LinearNDInterpolator(fro_concat, to_concat) + + def __call__(self, x): + assert x.ndim in (1, 2) and x.shape[-1] == 3 + assert np.isfinite(x).all() + singleton = x.ndim == 1 + x = apply_trans(self._affine, x) + assert np.isfinite(x).all() + out = self._interp(x) + assert np.isfinite(out).all() + self._last_deltas = np.linalg.norm(x - out, axis=1) + out = out[0] if singleton else out + return out diff --git a/mne/utils/config.py b/mne/utils/config.py index 7f6267a4e19..9497749b94d 100644 --- a/mne/utils/config.py +++ b/mne/utils/config.py @@ -164,6 +164,7 @@ def set_memmap_min_size(memmap_min_size): "MNE_DATASETS_KILOWORD_PATH": "str, path for kiloword data", "MNE_DATASETS_FIELDTRIP_CMC_PATH": "str, path for fieldtrip_cmc data", "MNE_DATASETS_PHANTOM_4DBTI_PATH": "str, path for phantom_4dbti data", + "MNE_DATASETS_PHANTOM_KERNEL_PATH": "str, path for phantom_kernel data", "MNE_DATASETS_LIMO_PATH": "str, path for limo data", "MNE_DATASETS_REFMEG_NOISE_PATH": "str, path for refmeg_noise data", "MNE_DATASETS_SSVEP_PATH": "str, path for ssvep data", diff --git a/mne/utils/docs.py b/mne/utils/docs.py index 40308a9074d..7875f83feb0 100644 --- a/mne/utils/docs.py +++ b/mne/utils/docs.py @@ -1159,7 +1159,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): docdict[ "eeg" ] = """ -eeg : bool | str | list +eeg : bool | str | list | dict String options are: - "original" (default; equivalent to ``True``) @@ -1169,8 +1169,11 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): The EEG locations projected onto the scalp, as is done in forward modeling - Can also be a list of these options, or an empty list (``[]``, - equivalent of ``False``). + Can also be a list of these options, or a dict to specify the alpha values + to use, e.g. ``dict(original=0.2, projected=0.8)``. + + .. versionchanged:: 1.6 + Added support for specifying alpha values as a dict. """ docdict[ @@ -1769,11 +1772,16 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): docdict[ "fnirs" ] = """ -fnirs : str | list | bool | None +fnirs : str | list | dict | bool | None Can be "channels", "pairs", "detectors", and/or "sources" to show the fNIRS channel locations, optode locations, or line between source-detector pairs, or a combination like ``('pairs', 'channels')``. - True translates to ``('pairs',)``. + True translates to ``('pairs',)``. A dict can also be used to specify + alpha values (but only "channels" and "pairs" will be used), e.g. + ``dict(channels=0.2, pairs=0.7)``. + + .. versionchanged:: 1.6 + Added support for specifying alpha values as a dict. """ docdict[ @@ -2568,11 +2576,15 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): docdict[ "meg" ] = """ -meg : str | list | bool | None +meg : str | list | dict | bool | None Can be "helmet", "sensors" or "ref" to show the MEG helmet, sensors or reference sensors respectively, or a combination like ``('helmet', 'sensors')`` (same as None, default). True translates to - ``('helmet', 'sensors', 'ref')``. + ``('helmet', 'sensors', 'ref')``. Can also be a dict to specify alpha values, + e.g. ``{"helmet": 0.1, "sensors": 0.8}``. + + .. versionchanged:: 1.6 + Added support for specifying alpha values as a dict. """ docdict[ diff --git a/mne/viz/_3d.py b/mne/viz/_3d.py index ece5798b582..0063aa37bdd 100644 --- a/mne/viz/_3d.py +++ b/mne/viz/_3d.py @@ -638,7 +638,7 @@ def plot_alignment( from ..source_space._source_space import _ensure_src from .backends.renderer import _get_renderer - meg, eeg, fnirs, warn_meg = _handle_sensor_types(meg, eeg, fnirs) + meg, eeg, fnirs, warn_meg, sensor_alpha = _handle_sensor_types(meg, eeg, fnirs) _check_option("interaction", interaction, ["trackball", "terrain"]) info = create_info(1, 1000.0, "misc") if info is None else info @@ -837,7 +837,14 @@ def plot_alignment( # plot helmet if "helmet" in meg and pick_types(info, meg=True).size > 0: - _, _, src_surf = _plot_helmet(renderer, info, to_cf_t, head_mri_t, coord_frame) + _, _, src_surf = _plot_helmet( + renderer, + info, + to_cf_t, + head_mri_t, + coord_frame, + alpha=sensor_alpha["meg_helmet"], + ) # plot surfaces if brain and "lh" not in surfs: # one layer sphere @@ -877,7 +884,7 @@ def plot_alignment( # plot sensors (NB snapshot_brain_montage relies on the last thing being # plotted being the sensors, so we need to do this after the surfaces) if picks.size > 0: - _plot_sensors( + _plot_sensors_3d( renderer, info, to_cf_t, @@ -888,6 +895,7 @@ def plot_alignment( warn_meg, head_surf, "m", + sensor_alpha=sensor_alpha, sensor_colors=sensor_colors, ) @@ -970,28 +978,56 @@ def _handle_sensor_types(meg, eeg, fnirs): if isinstance(fnirs, str): fnirs = [fnirs] + alpha_map = dict( + meg=dict(sensors="meg", helmet="meg_helmet", ref="ref_meg"), + eeg=dict(original="eeg", projected="eeg_projected"), + fnirs=dict(channels="fnirs", pairs="fnirs_pairs"), + ) + sensor_alpha = { + key: 0.25 if key == "meg_helmet" else 0.8 + for ch_dict in alpha_map.values() + for key in ch_dict.values() + } for kind, var in zip(("eeg", "meg", "fnirs"), (eeg, meg, fnirs)): - if not isinstance(var, (list, tuple)) or not all( - isinstance(x, str) for x in var - ): - raise TypeError(f"{kind} must be list or tuple of str, got {type(kind)}") + _validate_type(var, (list, tuple, dict), f"{kind}") + for ix, x in enumerate(var): + which = f"{kind} key {ix}" if isinstance(var, dict) else f"{kind}[{ix}]" + _validate_type(x, str, which) + if isinstance(var, dict) and x in alpha_map[kind]: + alpha = var[x] + _validate_type(alpha, "numeric", f"{kind}[{ix}]") + if not 0 <= alpha <= 1: + raise ValueError( + f"{kind}[{ix}] alpha value must be between 0 and 1, got {alpha}" + ) + sensor_alpha[alpha_map[kind][x]] = alpha + meg, eeg, fnirs = tuple(meg), tuple(eeg), tuple(fnirs) for xi, x in enumerate(meg): _check_option(f"meg[{xi}]", x, ("helmet", "sensors", "ref")) for xi, x in enumerate(eeg): _check_option(f"eeg[{xi}]", x, ("original", "projected")) for xi, x in enumerate(fnirs): _check_option(f"fnirs[{xi}]", x, ("channels", "pairs", "sources", "detectors")) - return meg, eeg, fnirs, warn_meg + # Add these for our True-only options, too -- eventually should support dict. + sensor_alpha.update( + seeg=0.8, + ecog=0.8, + source=sensor_alpha["fnirs"], + detector=sensor_alpha["fnirs"], + ) + return meg, eeg, fnirs, warn_meg, sensor_alpha @verbose def _ch_pos_in_coord_frame(info, to_cf_t, warn_meg=True, verbose=None): """Transform positions from head/device/mri to a coordinate frame.""" from ..forward import _create_meg_coils + from ..forward._make_forward import _read_coil_defs chs = dict(ch_pos=dict(), sources=dict(), detectors=dict()) unknown_chs = list() # prepare for chs with unknown coordinate frame type_counts = dict() + coilset = _read_coil_defs(verbose=False) for idx in range(info["nchan"]): ch_type = channel_type(info, idx) if ch_type in type_counts: @@ -1010,9 +1046,13 @@ def _ch_pos_in_coord_frame(info, to_cf_t, warn_meg=True, verbose=None): # example, a straight line / 1D geometry) this_coil = [info["chs"][idx]] try: - coil = _create_meg_coils(this_coil, acc="accurate")[0] + coil = _create_meg_coils( + this_coil, acc="accurate", coilset=coilset + )[0] except RuntimeError: # we don't have an accurate one - coil = _create_meg_coils(this_coil, acc="normal")[0] + coil = _create_meg_coils(this_coil, acc="normal", coilset=coilset)[ + 0 + ] # store verts as ch_coord ch_coord, triangles = _sensor_shape(coil) ch_coord = apply_trans(coil_trans, ch_coord) @@ -1071,14 +1111,22 @@ def _plot_head_surface( def _plot_helmet( - renderer, info, to_cf_t, head_mri_t, coord_frame, alpha=0.25, color=None + renderer, + info, + to_cf_t, + head_mri_t, + coord_frame, + *, + alpha=0.25, + scale=1.0, ): - color = DEFAULTS["coreg"]["helmet_color"] if color is None else color + color = DEFAULTS["coreg"]["helmet_color"] src_surf = get_meg_helmet_surf(info, head_mri_t) assert src_surf["coord_frame"] == FIFF.FIFFV_COORD_MRI - src_surf = transform_surface_to( - src_surf, coord_frame, [to_cf_t["mri"], to_cf_t["head"]], copy=True - ) + if to_cf_t is not None: + src_surf = transform_surface_to( + src_surf, coord_frame, [to_cf_t["mri"], to_cf_t["head"]], copy=True + ) actor, dst_surf = renderer.surface( surface=src_surf, color=color, opacity=alpha, backface_culling=False ) @@ -1404,7 +1452,7 @@ def _plot_forward(renderer, fwd, fwd_trans, fwd_scale=1, scale=1.5e-3, alpha=1): return actors -def _plot_sensors( +def _plot_sensors_3d( renderer, info, to_cf_t, @@ -1415,7 +1463,7 @@ def _plot_sensors( warn_meg, head_surf, units, - sensor_opacity=0.8, + sensor_alpha, orient_glyphs=False, scale_by_distance=False, project_points=False, @@ -1461,6 +1509,7 @@ def _plot_sensors( origin=sources[ch_name][np.newaxis] * unit_scalar, destination=detectors[ch_name][np.newaxis] * unit_scalar, radius=0.001 * unit_scalar, + opacity=sensor_alpha["fnirs_pairs"], ) actors[ch_type].append(actor) del ch_type @@ -1485,6 +1534,7 @@ def _plot_sensors( sensor_colors = dict() assert isinstance(sensor_colors, dict) for ch_type, sens_loc in locs.items(): + logger.debug(f"Drawing {ch_type} sensors") assert len(sens_loc) # should be guaranteed above colors = to_rgba_array(sensor_colors.get(ch_type, defaults[ch_type + "_color"])) _check_option( @@ -1493,6 +1543,7 @@ def _plot_sensors( (len(sens_loc), 1), ) scale = defaults[ch_type + "_scale"] * unit_scalar + this_alpha = sensor_alpha[ch_type] if isinstance(sens_loc[0], dict): # meg coil if len(colors) == 1: colors = [colors[0]] * len(sens_loc) @@ -1500,7 +1551,7 @@ def _plot_sensors( actor, _ = renderer.surface( surface=surface, color=color[:3], - opacity=sensor_opacity * color[3], + opacity=this_alpha * color[3], backface_culling=False, # visible from all sides ) actors[ch_type].append(actor) @@ -1514,7 +1565,7 @@ def _plot_sensors( loc=sens_loc[mask] * unit_scalar, color=colors[0, :3], scale=scale, - opacity=sensor_opacity * colors[0, 3], + opacity=this_alpha * colors[0, 3], orient_glyphs=orient_glyphs, scale_by_distance=scale_by_distance, project_points=project_points, @@ -1533,7 +1584,7 @@ def _plot_sensors( loc=loc * unit_scalar, color=color[:3], scale=scale, - opacity=sensor_opacity * color[3], + opacity=this_alpha * color[3], orient_glyphs=orient_glyphs, scale_by_distance=scale_by_distance, project_points=project_points, @@ -1558,7 +1609,7 @@ def _plot_sensors( color=defaults["eegp_color"], mode="cylinder", scale=defaults["eegp_scale"] * unit_scalar, - opacity=0.6, + opacity=sensor_alpha["eeg_projected"], glyph_height=defaults["eegp_height"], glyph_center=(0.0, -defaults["eegp_height"] / 2.0, 0), glyph_resolution=20, diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 80c9d313924..6d5719186fd 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -32,7 +32,7 @@ vertex_to_mni, ) from ...defaults import DEFAULTS, _handle_default -from ...surface import _marching_cubes, _mesh_borders, get_meg_helmet_surf, mesh_edges +from ...surface import _marching_cubes, _mesh_borders, mesh_edges from ...transforms import ( Transform, _frame_to_str, @@ -62,7 +62,8 @@ _handle_sensor_types, _handle_time, _plot_forward, - _plot_sensors, + _plot_helmet, + _plot_sensors_3d, _process_clim, ) from .._3d_overlay import _LayeredMesh @@ -2791,7 +2792,7 @@ def add_sensors( from ...preprocessing.ieeg._projection import _project_sensors_onto_inflated _validate_type(info, Info, "info") - meg, eeg, fnirs, warn_meg = _handle_sensor_types(meg, eeg, fnirs) + meg, eeg, fnirs, warn_meg, sensor_alpha = _handle_sensor_types(meg, eeg, fnirs) picks = pick_types( info, meg=("sensors" in meg), @@ -2825,7 +2826,7 @@ def add_sensors( # Do the main plotting for _ in self._iter_views("vol"): if picks.size > 0: - sensors_actors = _plot_sensors( + sensors_actors = _plot_sensors_3d( self._renderer, info, to_cf_t, @@ -2836,6 +2837,7 @@ def add_sensors( warn_meg, head_surf, self._units, + sensor_alpha=sensor_alpha, sensor_colors=sensor_colors, ) # sensors_actors can still be None @@ -2844,15 +2846,14 @@ def add_sensors( self._add_actor(item, actor) if "helmet" in meg and pick_types(info, meg=True).size > 0: - surf = get_meg_helmet_surf(info, head_mri_t) - verts = surf["rr"] * (1 if self._units == "m" else 1e3) - actor, _ = self._renderer.mesh( - *verts.T, - surf["tris"], - color=DEFAULTS["coreg"]["helmet_color"], - opacity=0.25, - reset_camera=False, - render=False, + actor, _, _ = _plot_helmet( + self._renderer, + info, + to_cf_t, + head_mri_t, + "mri", + alpha=sensor_alpha["meg_helmet"], + scale=1 if self._units == "m" else 1e3, ) self._add_actor("helmet", actor) diff --git a/mne/viz/backends/_pyvista.py b/mne/viz/backends/_pyvista.py index 5cb89179ef5..1b4bfe6c37b 100644 --- a/mne/viz/backends/_pyvista.py +++ b/mne/viz/backends/_pyvista.py @@ -599,6 +599,7 @@ def tube( colormap="RdBu", normalized_colormap=False, reverse_lut=False, + opacity=None, ): with warnings.catch_warnings(): warnings.filterwarnings("ignore", category=FutureWarning) @@ -622,6 +623,7 @@ def tube( show_scalar_bar=False, cmap=cmap, smooth_shading=self.smooth_shading, + opacity=opacity, ) return actor, tube diff --git a/mne/viz/tests/test_3d.py b/mne/viz/tests/test_3d.py index 8b02b8e228f..2c763275805 100644 --- a/mne/viz/tests/test_3d.py +++ b/mne/viz/tests/test_3d.py @@ -277,10 +277,10 @@ def test_plot_alignment_meg(renderer, system): assert system == "KIT" this_info = read_raw_kit(sqd_fname).info - meg = ["helmet", "sensors"] + meg = {"helmet": 0.1, "sensors": 0.2} sensor_colors = "k" # should be upsampled to correct shape if system == "KIT": - meg.append("ref") + meg["ref"] = 0.3 with pytest.raises(TypeError, match="instance of dict"): plot_alignment(this_info, meg=meg, sensor_colors=sensor_colors) sensor_colors = dict(meg=sensor_colors) diff --git a/mne/viz/utils.py b/mne/viz/utils.py index e9c36281bae..cfdf5aa62a6 100644 --- a/mne/viz/utils.py +++ b/mne/viz/utils.py @@ -1166,7 +1166,7 @@ def plot_sensors( colors[pick_idx] = color_vals[ind] break title = "Sensor positions (%s)" % ch_type if title is None else title - fig = _plot_sensors( + fig = _plot_sensors_2d( pos, info, picks, @@ -1222,7 +1222,7 @@ def _close_event(event, fig): fig.lasso.disconnect() -def _plot_sensors( +def _plot_sensors_2d( pos, info, picks, diff --git a/tools/circleci_download.sh b/tools/circleci_download.sh index 9cf5124fd53..d2dff2eb499 100755 --- a/tools/circleci_download.sh +++ b/tools/circleci_download.sh @@ -58,6 +58,9 @@ else if [[ $(cat $FNAME | grep -x ".*brainstorm.*bst_phantom_elekta.*" | wc -l) -gt 0 ]]; then python -c "import mne; print(mne.datasets.brainstorm.bst_phantom_elekta.data_path(update_path=True, accept=True))"; fi; + if [[ $(cat $FNAME | grep -x ".*datasets.*phantom_kernel.*" | wc -l) -gt 0 ]]; then + python -c "import mne; print(mne.datasets.phantom_kernel.data_path(update_path=True))"; + fi; if [[ $(cat $FNAME | grep -x ".*datasets.*hcp_mmp_parcellation.*" | wc -l) -gt 0 ]]; then python -c "import mne; print(mne.datasets.sample.data_path(update_path=True))"; python -c "import mne; print(mne.datasets.fetch_hcp_mmp_parcellation(subjects_dir=mne.datasets.sample.data_path() / 'subjects', accept=True))"; diff --git a/tutorials/inverse/35_dipole_orientations.py b/tutorials/inverse/35_dipole_orientations.py index bffc5ad1fc0..26a9b5713c3 100644 --- a/tutorials/inverse/35_dipole_orientations.py +++ b/tutorials/inverse/35_dipole_orientations.py @@ -54,8 +54,8 @@ dip_times = [0] white = (1.0, 1.0, 1.0) # RGB values for a white color -actual_amp = np.ones(dip_len) # misc amp to create Dipole instance -actual_gof = np.ones(dip_len) # misc GOF to create Dipole instance +actual_amp = np.ones(dip_len) # fake amp, needed to create Dipole instance +actual_gof = np.ones(dip_len) # fake GOF, needed to create Dipole instance dipoles = mne.Dipole(dip_times, dip_pos, actual_amp, dip_ori, actual_gof) trans = mne.read_trans(trans_fname) diff --git a/tutorials/inverse/80_brainstorm_phantom_elekta.py b/tutorials/inverse/80_brainstorm_phantom_elekta.py index 2da04a19c0d..40585254aea 100644 --- a/tutorials/inverse/80_brainstorm_phantom_elekta.py +++ b/tutorials/inverse/80_brainstorm_phantom_elekta.py @@ -169,8 +169,8 @@ # Let's plot the positions and the orientations of the actual and the estimated # dipoles -actual_amp = np.ones(len(dip)) # misc amp to create Dipole instance -actual_gof = np.ones(len(dip)) # misc GOF to create Dipole instance +actual_amp = np.ones(len(dip)) # fake amp, needed to create Dipole instance +actual_gof = np.ones(len(dip)) # fake GOF, needed to create Dipole instance dip_true = mne.Dipole(dip.times, actual_pos, actual_amp, actual_ori, actual_gof) fig = mne.viz.plot_alignment( diff --git a/tutorials/inverse/90_phantom_4DBTi.py b/tutorials/inverse/90_phantom_4DBTi.py index 12b4643049f..1efc932dab5 100644 --- a/tutorials/inverse/90_phantom_4DBTi.py +++ b/tutorials/inverse/90_phantom_4DBTi.py @@ -69,8 +69,8 @@ # %% # Plot the dipoles in 3D -actual_amp = np.ones(len(dip)) # misc amp to create Dipole instance -actual_gof = np.ones(len(dip)) # misc GOF to create Dipole instance +actual_amp = np.ones(len(dip)) # fake amp, needed to create Dipole instance +actual_gof = np.ones(len(dip)) # fake GOF, needed to create Dipole instance dip = mne.Dipole(dip.times, pos, actual_amp, ori, actual_gof) dip_true = mne.Dipole(dip.times, actual_pos, actual_amp, ori, actual_gof) From b55df6397459245d280ffed9fce27ed3e808187c Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Tue, 7 Nov 2023 13:19:15 -0500 Subject: [PATCH 048/405] ENH: Improve Covariance.__repr__ (#12181) --- doc/changes/devel.rst | 1 + mne/cov.py | 12 +++++------- mne/tests/test_cov.py | 2 ++ mne/utils/__init__.pyi | 2 ++ mne/utils/numerics.py | 6 ++++++ 5 files changed, 16 insertions(+), 7 deletions(-) diff --git a/doc/changes/devel.rst b/doc/changes/devel.rst index 8d02c056445..d11bb3ad6b5 100644 --- a/doc/changes/devel.rst +++ b/doc/changes/devel.rst @@ -38,6 +38,7 @@ Enhancements - Add support for writing forward solutions to HDF5 and convenience function :meth:`mne.Forward.save` (:gh:`12036` by `Eric Larson`_) - Refactored internals of :func:`mne.read_annotations` (:gh:`11964` by `Paul Roujansky`_) - Add support for drawing MEG sensors in :ref:`mne coreg` (:gh:`12098` by `Eric Larson`_) +- Improve string representation of :class:`mne.Covariance` (:gh:`12181` by `Eric Larson`_) - Add ``check_version=True`` to :ref:`mne sys_info` to check for a new release on GitHub (:gh:`12146` by `Eric Larson`_) - Bad channels are now colored gray in addition to being dashed when spatial colors are used in :func:`mne.viz.plot_evoked` and related functions (:gh:`12142` by `Eric Larson`_) - By default MNE-Python creates matplotlib figures with ``layout='constrained'`` rather than the default ``layout='tight'`` (:gh:`12050`, :gh:`12103` by `Mathieu Scheltienne`_ and `Eric Larson`_) diff --git a/mne/cov.py b/mne/cov.py index 376cd6a8a59..64d82492023 100644 --- a/mne/cov.py +++ b/mne/cov.py @@ -60,6 +60,7 @@ ) from .rank import compute_rank from .utils import ( + _array_repr, _check_fname, _check_on_missing, _check_option, @@ -273,13 +274,10 @@ def _get_square(self): return np.diag(self.data) if self["diag"] else self.data.copy() def __repr__(self): # noqa: D105 - if self.data.ndim == 2: - s = "size : %s x %s" % self.data.shape - else: # ndim == 1 - s = "diagonal : %s" % self.data.size - s += ", n_samples : %s" % self.nfree - s += ", data : %s" % self.data - return "" % s + s = "" + return s def __add__(self, cov): """Add Covariance taking into account number of degrees of freedom.""" diff --git a/mne/tests/test_cov.py b/mne/tests/test_cov.py index f7dd2ffce9e..dcbe1b30ad9 100644 --- a/mne/tests/test_cov.py +++ b/mne/tests/test_cov.py @@ -250,6 +250,8 @@ def test_io_cov(tmp_path): assert_equal(cov["method"], cov2["method"]) assert_equal(cov["loglik"], cov2["loglik"]) assert "Covariance" in repr(cov) + assert "range :" in repr(cov) + assert "\n" not in repr(cov) cov2 = read_cov(cov_gz_fname) assert_array_almost_equal(cov.data, cov2.data) diff --git a/mne/utils/__init__.pyi b/mne/utils/__init__.pyi index b2c611f7d1c..42694921f00 100644 --- a/mne/utils/__init__.pyi +++ b/mne/utils/__init__.pyi @@ -18,6 +18,7 @@ __all__ = [ "_apply_scaling_cov", "_arange_div", "_array_equal_nan", + "_array_repr", "_assert_no_instances", "_auto_weakref", "_build_data_frame", @@ -347,6 +348,7 @@ from .numerics import ( _apply_scaling_cov, _arange_div, _array_equal_nan, + _array_repr, _cal_to_julian, _check_dt, _compute_row_norms, diff --git a/mne/utils/numerics.py b/mne/utils/numerics.py index 44cf8210771..fa78f24bb7d 100644 --- a/mne/utils/numerics.py +++ b/mne/utils/numerics.py @@ -1152,3 +1152,9 @@ def cache_fun(*args): return cache_fun return dec + + +def _array_repr(x): + """Produce compact info about float ndarray x.""" + assert isinstance(x, np.ndarray), type(x) + return f"shape : {x.shape}, range : [{np.nanmin(x):+0.2g}, {np.nanmax(x):+0.2g}]" From ffbce015ed8e6c069e00d68114079251781f7fb8 Mon Sep 17 00:00:00 2001 From: Clemens Brunner Date: Tue, 7 Nov 2023 22:42:55 +0100 Subject: [PATCH 049/405] Tweak logo for dark mode (#12176) Co-authored-by: Eric Larson --- doc/_static/mne_logo.svg | 1571 +++++++++++------------ doc/_static/mne_logo_dark.svg | 1571 +++++++++++------------ doc/_static/mne_logo_gray.svg | 783 +++++++++++ doc/_static/mne_logo_small.svg | 12 +- doc/sphinxext/gen_commands.py | 4 +- logo/generate_mne_logos.py | 112 +- mne/icons/mne_default_icon.png | Bin 10369 -> 12710 bytes mne/icons/mne_splash.png | Bin 22283 -> 31518 bytes mne/utils/config.py | 2 +- mne/viz/backends/_utils.py | 2 +- tools/check_mne_location.py | 2 +- tools/generate_codemeta.py | 2 +- tutorials/evoked/20_visualize_evoked.py | 2 +- 13 files changed, 2356 insertions(+), 1707 deletions(-) create mode 100644 doc/_static/mne_logo_gray.svg diff --git a/doc/_static/mne_logo.svg b/doc/_static/mne_logo.svg index 470b72f0ab6..dc9ff39d029 100644 --- a/doc/_static/mne_logo.svg +++ b/doc/_static/mne_logo.svg @@ -1,16 +1,16 @@ - + - 2022-04-13T15:28:22.434101 + 2023-11-07T13:26:54.126105 image/svg+xml - Matplotlib v3.5.1, https://matplotlib.org/ + Matplotlib v3.8.0.dev1998+gc5707d9c79, https://matplotlib.org/ @@ -22,843 +22,760 @@ - + - +" clip-path="url(#p379c9ce79c)" style="fill: #ffffff"/> - + +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" id="imagecb18c62e3b" transform="scale(1 -1) translate(0 -135.12)" x="0" y="-4.66125" width="360" height="135.12"/> - - - + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - + - - + + - - + diff --git a/doc/_static/mne_logo_dark.svg b/doc/_static/mne_logo_dark.svg index abf08002f93..1afbae82f9a 100644 --- a/doc/_static/mne_logo_dark.svg +++ b/doc/_static/mne_logo_dark.svg @@ -1,16 +1,16 @@ - + - 2022-04-13T15:28:22.830772 + 2023-11-07T13:26:53.926101 image/svg+xml - Matplotlib v3.5.1, https://matplotlib.org/ + Matplotlib v3.8.0.dev1998+gc5707d9c79, https://matplotlib.org/ @@ -22,843 +22,760 @@ - + - +" clip-path="url(#pa1d6d36807)" style="fill: #808080"/> - + +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" id="image73e6625c3e" transform="scale(1 -1) translate(0 -135.12)" x="0" y="-4.66125" width="360" height="135.12"/> - - - + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - + - - + + - - + diff --git a/doc/_static/mne_logo_gray.svg b/doc/_static/mne_logo_gray.svg new file mode 100644 index 00000000000..1cabd8f5932 --- /dev/null +++ b/doc/_static/mne_logo_gray.svg @@ -0,0 +1,783 @@ + + + + + + + + 2023-11-07T13:26:54.034781 + image/svg+xml + + + Matplotlib v3.8.0.dev1998+gc5707d9c79, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/doc/_static/mne_logo_small.svg b/doc/_static/mne_logo_small.svg index 506d2c0f4c0..0f62fdf3ce5 100644 --- a/doc/_static/mne_logo_small.svg +++ b/doc/_static/mne_logo_small.svg @@ -6,11 +6,11 @@ - 2022-04-13T15:28:23.367808 + 2023-11-07T13:26:54.345596 image/svg+xml - Matplotlib v3.5.1, https://matplotlib.org/ + Matplotlib v3.8.0.dev1998+gc5707d9c79, https://matplotlib.org/ @@ -21,9 +21,9 @@ - + +iVBORw0KGgoAAAANSUhEUgAAAEIAAAAQCAYAAACiEqkUAAAAsElEQVR4nO2QW2rEMBAEa8Ri2WLXd8rJcyeH+Bnc+VCHwJ5hCoYeNZI+KiR0r4H2gK2gve86Am2BzoC9/HdODp+P0vN0fwa6vDt1FXQG62PuU2fW8cXaZvbpyac++N4ry9b42hrL2ViuxvLT0BDcQ6Ah0IAz0AM0eq++MwaqoNo7jX3u0e+mt37yH61QSABSxB8pwqQIkyJMijApwqQIkyJMijApwqQIkyJMijApwvwCdAFgHcVQk7IAAAAASUVORK5CYII=" id="imagec02f1fbe38" transform="scale(1 -1) translate(0 -12)" x="86" y="-10.75" width="49.5" height="12"/> +" clip-path="url(#pf7563a16a6)" style="fill: #808080"/> - + diff --git a/doc/sphinxext/gen_commands.py b/doc/sphinxext/gen_commands.py index 0ca15319d36..21c31d2829b 100644 --- a/doc/sphinxext/gen_commands.py +++ b/doc/sphinxext/gen_commands.py @@ -51,8 +51,8 @@ def generate_commands_rst(app=None): from sphinx.util.display import status_iterator except Exception: from sphinx.util import status_iterator - root = Path(__file__).parent.parent.parent.absolute() - out_dir = (root / "doc" / "generated").absolute() + root = Path(__file__).parents[2] + out_dir = root / "doc" / "generated" out_dir.mkdir(exist_ok=True) out_fname = out_dir / "commands.rst.new" diff --git a/logo/generate_mne_logos.py b/logo/generate_mne_logos.py index 34b77788750..419dbe4279d 100644 --- a/logo/generate_mne_logos.py +++ b/logo/generate_mne_logos.py @@ -1,23 +1,19 @@ -""" -=============================================================================== -Script 'mne logo' -=============================================================================== +"""Generate the MNE-Python logos.""" -This script makes the logo for MNE. -""" # @author: drmccloy # Created on Mon Jul 20 11:28:16 2015 # License: BSD-3-Clause -import numpy as np -import os.path as op +import pathlib + import matplotlib.pyplot as plt -from matplotlib import rcParams -from scipy.stats import multivariate_normal +import numpy as np +from matplotlib import font_manager, rcParams +from matplotlib.colors import LinearSegmentedColormap +from matplotlib.patches import Ellipse, FancyBboxPatch, PathPatch, Rectangle from matplotlib.path import Path from matplotlib.text import TextPath -from matplotlib.patches import PathPatch, Ellipse, FancyBboxPatch, Rectangle -from matplotlib.colors import LinearSegmentedColormap +from scipy.stats import multivariate_normal # manually set values dpi = 300 @@ -25,7 +21,7 @@ tagline_scale_fudge = 0.97 # to get justification right tagline_offset_fudge = np.array([0, -100.0]) -# font, etc +# font, etc (default to MNE font) rcp = { "font.sans-serif": ["Primetime"], "font.style": "normal", @@ -38,6 +34,9 @@ plt.rcdefaults() rcParams.update(rcp) +# %% +# mne_logo.svg and mne_logo_dark.svg + # initialize figure (no axes, margins, etc) fig = plt.figure(1, figsize=(5, 2.25), frameon=False, dpi=dpi) ax = plt.Axes(fig, [0.0, 0.0, 1.0, 1.0]) @@ -108,14 +107,21 @@ im.set_clip_path(mne_clip, transform=im.get_transform()) ax.add_patch(rect) rect.set_clip_path(mne_clip, transform=im.get_transform()) -for coll in cs.collections: - coll.set_clip_path(mne_clip, transform=im.get_transform()) +cs.set_clip_path(mne_clip, transform=im.get_transform()) # get final position of clipping mask mne_corners = mne_clip.get_extents().corners() +# For this make sure that this gives something like "" +fnt = font_manager.findfont("Cooper Hewitt:style=normal:weight=book") +if "Book" not in fnt or "CooperHewitt" not in fnt: + print( + f"WARNING: Might not use correct Cooper Hewitt, got {fnt} but want " + "CooperHewitt-Book.otf or similar" + ) + # add tagline -rcParams.update({"font.sans-serif": ["Cooper Hewitt"], "font.weight": "300"}) -tag_path = TextPath((0, 0), "MEG + EEG ANALYSIS & VISUALIZATION") +with plt.rc_context({"font.sans-serif": ["Cooper Hewitt"], "font.weight": "book"}): + tag_path = TextPath((0, 0), "MEG + EEG ANALYSIS & VISUALIZATION") dims = tag_path.vertices.max(0) - tag_path.vertices.min(0) vert = tag_path.vertices - dims / 2.0 mult = tagline_scale_fudge * (plot_dims / dims).min() @@ -126,26 +132,36 @@ - tagline_offset_fudge ) tag_clip = Path(offset + vert * mult, tag_path.codes) -tag_patch = PathPatch(tag_clip, facecolor="k", edgecolor="none", zorder=10) +tag_patch = PathPatch(tag_clip, facecolor="0.6", edgecolor="none", zorder=10) ax.add_patch(tag_patch) yl = ax.get_ylim() yy = np.max([tag_clip.vertices.max(0)[-1], tag_clip.vertices.min(0)[-1]]) ax.set_ylim(np.ceil(yy), yl[-1]) # only save actual image extent plus a bit of padding -plt.draw() -static_dir = op.join(op.dirname(__file__), "..", "doc", "_static") -assert op.isdir(static_dir) -plt.savefig(op.join(static_dir, "mne_logo.svg"), transparent=True) -tag_patch.set_facecolor("w") -rect.set_facecolor("0.5") -plt.savefig(op.join(static_dir, "mne_logo_dark.svg"), transparent=True) -tag_patch.set_facecolor("k") -rect.set_facecolor("w") +fig.canvas.draw_idle() +static_dir = pathlib.Path(__file__).parents[1] / "doc" / "_static" +assert static_dir.is_dir() +kind_color = dict( + mne_logo_dark=("0.8", "0.5"), + mne_logo_gray=("0.6", "0.75"), + mne_logo=("0.3", "w"), # always last +) +for kind, (tag_color, rect_color) in kind_color.items(): + tag_patch.set_facecolor(tag_color) + rect.set_facecolor(rect_color) + fig.savefig( + static_dir / f"{kind}.svg", + transparent=True, + ) + +# %% +# mne_splash.png # modify to make the splash screen -data_dir = op.join(op.dirname(__file__), "..", "mne", "icons") -ax.patches[-1].set_facecolor("w") +data_dir = pathlib.Path(__file__).parents[1] / "mne" / "icons" +assert data_dir.is_dir() +tag_patch.set_facecolor("0.7") for coll in list(ax.collections): coll.remove() bounds = np.array( @@ -159,7 +175,10 @@ r = np.diff(bounds, axis=1).max() * 1.2 w, h = r, r * (2 / 3) box_xy = [xy[0] - w * 0.5, xy[1] - h * (2 / 5)] -ax.set_ylim(box_xy[1] + h * 1.001, box_xy[1] - h * 0.001) +ax.set( + ylim=(box_xy[1] + h * 1.001, box_xy[1] - h * 0.001), + xlim=(box_xy[0] - w * 0.001, box_xy[0] + w * 1.001), +) patch = FancyBboxPatch( box_xy, w, @@ -174,19 +193,30 @@ ) ax.add_patch(patch) fig.set_size_inches((512 / dpi, 512 * (h / w) / dpi)) -plt.savefig(op.join(data_dir, "mne_splash.png"), transparent=True) +fig.savefig( + data_dir / "mne_splash.png", + transparent=True, +) patch.remove() +# %% +# mne_default_icon.png + # modify to make an icon -ax.patches.pop(-1) # no tag line for our icon +ax.patches[-1].remove() # no tag line for our icon patch = Ellipse(xy, r, r, clip_on=False, zorder=-1, fc="k") ax.add_patch(patch) -ax.set_ylim(xy[1] + r / 1.9, xy[1] - r / 1.9) +ax.set_ylim(xy[1] + r / 1.99, xy[1] - r / 1.99) fig.set_size_inches((256 / dpi, 256 / dpi)) # Qt does not support clip paths in SVG rendering so we have to use PNG here # then use "optipng -o7" on it afterward (14% reduction in file size) -plt.savefig(op.join(data_dir, "mne_default_icon.png"), transparent=True) -plt.close() +fig.savefig( + data_dir / "mne_default_icon.png", + transparent=True, +) + +# %% +# mne_logo_small.svg # 188x45 image dpi = 96 # for SVG it's different @@ -194,13 +224,12 @@ h_px = 45 center_fudge = np.array([60, 0]) scale_fudge = 2.1 -rcParams.update({"font.sans-serif": ["Primetime"], "font.weight": "black"}) x = np.linspace(-1.0, 1.0, w_px // 2) y = np.linspace(-1.0, 1.0, h_px // 2) X, Y = np.meshgrid(x, y) # initialize figure (no axes, margins, etc) fig = plt.figure( - 1, figsize=(w_px / dpi, h_px / dpi), facecolor="k", frameon=False, dpi=dpi + 2, figsize=(w_px / dpi, h_px / dpi), facecolor="k", frameon=False, dpi=dpi ) ax = plt.Axes(fig, [0.0, 0.0, 1.0, 1.0]) ax.set_axis_off() @@ -232,6 +261,9 @@ ypad = np.abs(np.diff([ymax, ymin])) / 20.0 ax.set_xlim(xmin - xpad, xl[1] + xpad) ax.set_ylim(ymax + ypad, ymin - ypad) -plt.draw() -plt.savefig(op.join(static_dir, "mne_logo_small.svg"), dpi=dpi, transparent=True) -plt.close() +fig.canvas.draw_idle() +fig.savefig( + static_dir / "mne_logo_small.svg", + dpi=dpi, + transparent=True, +) diff --git a/mne/icons/mne_default_icon.png b/mne/icons/mne_default_icon.png index 83a187ecc9397c64c75c1ae176ffbf0b9ee40f25..22a90e2275c0874c5adaf295b9b75754c273a75d 100644 GIT binary patch literal 12710 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 literal 10369 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 diff --git a/mne/icons/mne_splash.png b/mne/icons/mne_splash.png index 6ba7ee49b514749c7554d3f6b37377f516e30bf1..76aa18ea574d1746138fe83430dc81889e7f2451 100644 GIT binary patch literal 31518 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⋘X$iZKAS_yBZP~M5IvSD8krOC|w;?ZG&Q6aX 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 P2%@Z@aidV~cIf{Bh7;Eb literal 22283 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 diff --git a/mne/utils/config.py b/mne/utils/config.py index 9497749b94d..453cdb5c084 100644 --- a/mne/utils/config.py +++ b/mne/utils/config.py @@ -537,7 +537,7 @@ def _get_stim_channel(stim_channel, info, raise_error=True): def _get_root_dir(): """Get as close to the repo root as possible.""" - root_dir = Path(__file__).parent.parent.expanduser().absolute() + root_dir = Path(__file__).parents[1] up_dir = root_dir.parent if (up_dir / "setup.py").is_file() and all( (up_dir / x).is_dir() for x in ("mne", "examples", "doc") diff --git a/mne/viz/backends/_utils.py b/mne/viz/backends/_utils.py index b114e99e349..fc42538335f 100644 --- a/mne/viz/backends/_utils.py +++ b/mne/viz/backends/_utils.py @@ -89,7 +89,7 @@ def _alpha_blend_background(ctable, background_color): def _qt_init_icons(): from qtpy.QtGui import QIcon - icons_path = f"{Path(__file__).parent.parent.parent}/icons" + icons_path = str(Path(__file__).parents[2] / "icons") QIcon.setThemeSearchPaths([icons_path]) return icons_path diff --git a/tools/check_mne_location.py b/tools/check_mne_location.py index 75991ac2743..8691cad7d72 100755 --- a/tools/check_mne_location.py +++ b/tools/check_mne_location.py @@ -3,7 +3,7 @@ from pathlib import Path import mne -want_mne_dir = Path(__file__).parent.parent / "mne" +want_mne_dir = Path(__file__).parents[1] / "mne" got_mne_dir = Path(mne.__file__).parent want_mne_dir = want_mne_dir.resolve() got_mne_dir = got_mne_dir.resolve() diff --git a/tools/generate_codemeta.py b/tools/generate_codemeta.py index e547261dc4d..f53d2ae856a 100644 --- a/tools/generate_codemeta.py +++ b/tools/generate_codemeta.py @@ -7,7 +7,7 @@ parser.add_argument("release_version", type=str) release_version = parser.parse_args().release_version -out_dir = Path(__file__).parent.parent +out_dir = Path(__file__).parents[1] # NOTE: ../codemeta.json and ../citation.cff should not be continuously # updated. Run this script only at release time. diff --git a/tutorials/evoked/20_visualize_evoked.py b/tutorials/evoked/20_visualize_evoked.py index e07b1e2e601..4f7e2ba4967 100644 --- a/tutorials/evoked/20_visualize_evoked.py +++ b/tutorials/evoked/20_visualize_evoked.py @@ -279,7 +279,7 @@ def custom_func(x): # compute 3D field maps without a ``trans`` file, but it will only work for # calculating the field *on the MEG helmet from the MEG sensors*. -subjects_dir = root.parent.parent / "subjects" +subjects_dir = root.parents[1] / "subjects" trans_file = root / "sample_audvis_raw-trans.fif" # %% From bb93c0a20cc936f7cdfbadc8aa5ee40a974a6e80 Mon Sep 17 00:00:00 2001 From: Clemens Brunner Date: Wed, 8 Nov 2023 14:55:43 +0100 Subject: [PATCH 050/405] Use gray logo (works in light and dark modes) (#12184) --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index fdc620acd9f..e8690281bcb 100644 --- a/README.rst +++ b/README.rst @@ -199,5 +199,5 @@ MNE-Python is **BSD-licensed** (BSD-3-Clause): .. |OpenSSF| image:: https://www.bestpractices.dev/projects/7783/badge .. _OpenSSF: https://www.bestpractices.dev/projects/7783 -.. |MNE| image:: https://mne.tools/stable/_static/mne_logo.svg +.. |MNE| image:: https://mne.tools/dev/_static/mne_logo_gray.svg .. _MNE: https://mne.tools/dev/ From 8d86df6780dd39dfcd55544ee358424a69d4b9be Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Wed, 8 Nov 2023 09:58:14 -0500 Subject: [PATCH 051/405] BUG: Fix bug with logging and n_jobs>1 (#12154) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Richard Höchenberger --- doc/changes/devel.rst | 1 + mne/parallel.py | 8 +++++++- mne/utils/tests/test_logging.py | 22 ++++++++++++++++++++++ 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/doc/changes/devel.rst b/doc/changes/devel.rst index d11bb3ad6b5..698e033b758 100644 --- a/doc/changes/devel.rst +++ b/doc/changes/devel.rst @@ -57,6 +57,7 @@ Bugs - Fix bug with :func:`mne.channels.read_ch_adjacency` (:gh:`11608` by :newcontrib:`Ivan Zubarev`) - Fix bugs with saving splits for :class:`~mne.Epochs` (:gh:`11876` by `Dmitrii Altukhov`_) - Fix bug with multi-plot 3D rendering where only one plot was updated (:gh:`11896` by `Eric Larson`_) +- Fix bug where ``verbose`` level was not respected inside parallel jobs (:gh:`12154` by `Eric Larson`_) - Fix bug where subject birthdays were not correctly read by :func:`mne.io.read_raw_snirf` (:gh:`11912` by `Eric Larson`_) - Fix bug with :func:`mne.chpi.compute_head_pos` for CTF data where digitization points were modified in-place, producing an incorrect result during a save-load round-trip (:gh:`11934` by `Eric Larson`_) - Fix bug where non-compliant stimulus data streams were not ignored by :func:`mne.io.read_raw_snirf` (:gh:`11915` by `Johann Benerradi`_) diff --git a/mne/parallel.py b/mne/parallel.py index 2598d93ae92..bb8a14d381d 100644 --- a/mne/parallel.py +++ b/mne/parallel.py @@ -14,6 +14,7 @@ _validate_type, get_config, logger, + use_log_level, verbose, warn, ) @@ -120,7 +121,12 @@ def parallel_func( logger.debug(f"Got {n_jobs} parallel jobs after requesting {n_jobs_orig}") if max_jobs is not None: n_jobs = min(n_jobs, max(_ensure_int(max_jobs, "max_jobs"), 1)) - my_func = delayed(func) + + def run_verbose(*args, verbose=logger.level, **kwargs): + with use_log_level(verbose=verbose): + return func(*args, **kwargs) + + my_func = delayed(run_verbose) if total is not None: diff --git a/mne/utils/tests/test_logging.py b/mne/utils/tests/test_logging.py index 2b9b6a8aeee..a091bea0f83 100644 --- a/mne/utils/tests/test_logging.py +++ b/mne/utils/tests/test_logging.py @@ -1,3 +1,4 @@ +import logging import os import re import warnings @@ -8,6 +9,7 @@ from mne import Epochs, create_info, read_evokeds from mne.io import RawArray, read_raw_fif +from mne.parallel import parallel_func from mne.utils import ( _get_call_line, catch_logging, @@ -252,3 +254,23 @@ def meth_2(self, verbose=None): o.meth_2(verbose=False) log = log.getvalue() assert log == "" + + +@pytest.mark.parametrize("n_jobs", (1, 2)) +def test_verbose_threads(n_jobs): + """Test that our verbose level propagates to threads.""" + + def my_fun(): + from mne.utils import logger + + return logger.level + + with use_log_level("info"): + assert logger.level == logging.INFO + with use_log_level("warning"): + assert logger.level == logging.WARNING + parallel, p_fun, got_jobs = parallel_func(my_fun, n_jobs=n_jobs) + assert got_jobs in (1, n_jobs) # FORCE_SERIAL could be set + out = parallel(p_fun() for _ in range(5)) + want_levels = [logging.WARNING] * 5 + assert out == want_levels From 14f4f865e6fa28231962b8c7582cac5f41798f9f Mon Sep 17 00:00:00 2001 From: Mathieu Scheltienne Date: Wed, 8 Nov 2023 17:13:17 +0100 Subject: [PATCH 052/405] Add argument splash to disable splash-screen from Qt-browser (#12185) Co-authored-by: Eric Larson --- doc/changes/devel.rst | 7 ++++--- mne/epochs.py | 2 ++ mne/io/base.py | 2 ++ mne/preprocessing/ica.py | 2 ++ mne/utils/docs.py | 8 ++++++++ mne/viz/_figure.py | 2 +- mne/viz/epochs.py | 5 +++++ mne/viz/ica.py | 7 +++++++ mne/viz/raw.py | 5 +++++ 9 files changed, 36 insertions(+), 4 deletions(-) diff --git a/doc/changes/devel.rst b/doc/changes/devel.rst index 698e033b758..2819e361a25 100644 --- a/doc/changes/devel.rst +++ b/doc/changes/devel.rst @@ -47,10 +47,11 @@ Enhancements - :func:`~mne.epochs.make_metadata` now accepts ``tmin=None`` and ``tmax=None``, which will bound the time window used for metadata generation by event names (instead of a fixed time). That way, you can now for example generate metadata spanning from one cue or fixation cross to the next, even if trial durations vary throughout the recording (:gh:`12118` by `Richard Höchenberger`_) - Add support for passing multiple labels to :func:`mne.minimum_norm.source_induced_power` (:gh:`12026` by `Erica Peterson`_, `Eric Larson`_, and `Daniel McCloy`_ ) - Added documentation to :meth:`mne.io.Raw.set_montage` and :func:`mne.add_reference_channels` to specify that montages should be set after adding reference channels (:gh:`12160` by `Jacob Woessner`_) +- Add argument ``splash`` to the function using the ``qt`` browser backend to allow enabling/disabling the splash screen (:gh:`12185` by `Mathieu Scheltienne`_) Bugs ~~~~ -- Fix bug where :func:`mne.io.read_raw_gdf` would fail due to improper usage of ``np.clip`` (:gh:`12168` by :newcontrib:`Rasmus Aagaard`) +- Fix bug where :func:`mne.io.read_raw_gdf` would fail due to improper usage of ``np.clip`` (:gh:`12168` by :newcontrib:`Rasmus Aagaard`) - Fix bugs with :func:`mne.preprocessing.realign_raw` where the start of ``other`` was incorrectly cropped; and onsets and durations in ``other.annotations`` were left unsynced with the resampled data (:gh:`11950` by :newcontrib:`Qian Chu`) - Fix bug where ``encoding`` argument was ignored when reading annotations from an EDF file (:gh:`11958` by :newcontrib:`Andrew Gilbert`) - Mark tests ``test_adjacency_matches_ft`` and ``test_fetch_uncompressed_file`` as network tests (:gh:`12041` by :newcontrib:`Maksym Balatsko`) @@ -70,7 +71,7 @@ Bugs - Fix bug with reported component number and errant reporting of PCA explained variance as ICA explained variance in :meth:`mne.Report.add_ica` (:gh:`12155`, :gh:`12167` by `Eric Larson`_ and `Richard Höchenberger`_) - Fix bug with axis clip box boundaries in :func:`mne.viz.plot_evoked_topo` and related functions (:gh:`11999` by `Eric Larson`_) - Fix bug with ``subject_info`` when loading data from and exporting to EDF file (:gh:`11952` by `Paul Roujansky`_) -- Fix bug where :class:`mne.Info` HTML representations listed all channel counts instead of good channel counts under the heading "Good channels" (:gh:`12145` by `Eric Larson`_) +- Fix bug where :class:`mne.Info` HTML representations listed all channel counts instead of good channel counts under the heading "Good channels" (:gh:`12145` by `Eric Larson`_) - Fix rendering glitches when plotting Neuromag/TRIUX sensors in :func:`mne.viz.plot_alignment` and related functions (:gh:`12098` by `Eric Larson`_) - Fix bug with delayed checking of :class:`info["bads"] ` (:gh:`12038` by `Eric Larson`_) - Fix bug with :ref:`mne coreg` where points inside the head surface were not shown (:gh:`12147`, :gh:`12164` by `Eric Larson`_) @@ -86,7 +87,7 @@ Bugs - Fix bug with :func:`~mne.viz.plot_raw` where changing ``MNE_BROWSER_BACKEND`` via :func:`~mne.set_config` would have no effect within a Python session (:gh:`12078` by `Santeri Ruuskanen`_) - Improve handling of ``method`` argument in the channel interpolation function to support :class:`str` and raise helpful error messages (:gh:`12113` by `Mathieu Scheltienne`_) - Fix combination of ``DIN`` event channels into a single synthetic trigger channel ``STI 014`` by the MFF reader of :func:`mne.io.read_raw_egi` (:gh:`12122` by `Mathieu Scheltienne`_) -- Fix bug with :func:`mne.io.read_raw_eeglab` and :func:`mne.read_epochs_eeglab` where automatic fiducial detection would fail for certain files (:gh:`12165` by `Clemens Brunner`_) +- Fix bug with :func:`mne.io.read_raw_eeglab` and :func:`mne.read_epochs_eeglab` where automatic fiducial detection would fail for certain files (:gh:`12165` by `Clemens Brunner`_) API changes ~~~~~~~~~~~ diff --git a/mne/epochs.py b/mne/epochs.py index 510161f99bc..50ecf10ee64 100644 --- a/mne/epochs.py +++ b/mne/epochs.py @@ -1299,6 +1299,7 @@ def plot( *, theme=None, overview_mode=None, + splash=True, ): return plot_epochs( self, @@ -1324,6 +1325,7 @@ def plot( use_opengl=use_opengl, theme=theme, overview_mode=overview_mode, + splash=splash, ) @copy_function_doc_to_method_doc(plot_topo_image_epochs) diff --git a/mne/io/base.py b/mne/io/base.py index 4c1d13ffaf7..658eb0e4ea2 100644 --- a/mne/io/base.py +++ b/mne/io/base.py @@ -1801,6 +1801,7 @@ def plot( *, theme=None, overview_mode=None, + splash=True, verbose=None, ): return plot_raw( @@ -1838,6 +1839,7 @@ def plot( use_opengl=use_opengl, theme=theme, overview_mode=overview_mode, + splash=splash, verbose=verbose, ) diff --git a/mne/preprocessing/ica.py b/mne/preprocessing/ica.py index c0139427a4a..df1952669a5 100644 --- a/mne/preprocessing/ica.py +++ b/mne/preprocessing/ica.py @@ -2552,6 +2552,7 @@ def plot_sources( *, theme=None, overview_mode=None, + splash=True, ): return plot_ica_sources( self, @@ -2569,6 +2570,7 @@ def plot_sources( use_opengl=use_opengl, theme=theme, overview_mode=overview_mode, + splash=splash, ) @copy_function_doc_to_method_doc(plot_ica_scores) diff --git a/mne/utils/docs.py b/mne/utils/docs.py index 7875f83feb0..573066ca3c2 100644 --- a/mne/utils/docs.py +++ b/mne/utils/docs.py @@ -4201,6 +4201,14 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): .. versionchanged:: 1.1 Added ``'eeglab'`` option. """ +docdict[ + "splash" +] = """ +splash : bool + If True (default), a splash screen is shown during the application startup. Only + applicable to the ``qt`` backend. +""" + docdict[ "split_naming" ] = """ diff --git a/mne/viz/_figure.py b/mne/viz/_figure.py index f53e079c6b5..7bd58f4ee9e 100644 --- a/mne/viz/_figure.py +++ b/mne/viz/_figure.py @@ -659,7 +659,7 @@ def _get_browser(show, block, **kwargs): figsize = kwargs.setdefault("figsize", _get_figsize_from_config()) if figsize is None or np.any(np.array(figsize) < 8): kwargs["figsize"] = (8, 8) - kwargs["splash"] = True if show else False + kwargs["splash"] = kwargs.get("splash", True) and show if kwargs.get("theme", None) is None: kwargs["theme"] = get_config("MNE_BROWSER_THEME", "auto") if kwargs.get("overview_mode", None) is None: diff --git a/mne/viz/epochs.py b/mne/viz/epochs.py index 613f3a2b62a..22d686c9b95 100644 --- a/mne/viz/epochs.py +++ b/mne/viz/epochs.py @@ -786,6 +786,7 @@ def plot_epochs( *, theme=None, overview_mode=None, + splash=True, ): """Visualize epochs. @@ -881,6 +882,9 @@ def plot_epochs( %(overview_mode)s .. versionadded:: 1.1 + %(splash)s + + .. versionadded:: 1.6 Returns ------- @@ -1086,6 +1090,7 @@ def plot_epochs( use_opengl=use_opengl, theme=theme, overview_mode=overview_mode, + splash=splash, ) fig = _get_browser(show=show, block=block, **params) diff --git a/mne/viz/ica.py b/mne/viz/ica.py index 2b16c5f6837..24a9af42b12 100644 --- a/mne/viz/ica.py +++ b/mne/viz/ica.py @@ -55,6 +55,7 @@ def plot_ica_sources( *, theme=None, overview_mode=None, + splash=True, ): """Plot estimated latent sources given the unmixing matrix. @@ -99,6 +100,9 @@ def plot_ica_sources( %(overview_mode)s .. versionadded:: 1.1 + %(splash)s + + .. versionadded:: 1.6 Returns ------- @@ -139,6 +143,7 @@ def plot_ica_sources( use_opengl=use_opengl, theme=theme, overview_mode=overview_mode, + splash=splash, ) elif isinstance(inst, Evoked): if start is not None or stop is not None: @@ -1266,6 +1271,7 @@ def _plot_sources( *, theme=None, overview_mode=None, + splash=True, ): """Plot the ICA components as a RawArray or EpochsArray.""" from ..epochs import BaseEpochs, EpochsArray @@ -1410,6 +1416,7 @@ def _plot_sources( use_opengl=use_opengl, theme=theme, overview_mode=overview_mode, + splash=splash, ) if is_epo: params.update( diff --git a/mne/viz/raw.py b/mne/viz/raw.py index ec5c95f57ab..cdefc285c19 100644 --- a/mne/viz/raw.py +++ b/mne/viz/raw.py @@ -65,6 +65,7 @@ def plot_raw( *, theme=None, overview_mode=None, + splash=True, verbose=None, ): """Plot raw data. @@ -196,6 +197,9 @@ def plot_raw( %(overview_mode)s .. versionadded:: 1.1 + %(splash)s + + .. versionadded:: 1.6 %(verbose)s Returns @@ -394,6 +398,7 @@ def plot_raw( use_opengl=use_opengl, theme=theme, overview_mode=overview_mode, + splash=splash, ) fig = _get_browser(show=show, block=block, **params) From 6874483f6033743510a1ab0766c56ea007db1c20 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Wed, 8 Nov 2023 11:38:17 -0500 Subject: [PATCH 053/405] BUG: Fix bug with spectrum warning (#12186) --- doc/changes/devel.rst | 1 + mne/time_frequency/spectrum.py | 12 +++++++----- mne/time_frequency/tests/test_spectrum.py | 13 ++++++++----- .../preprocessing/50_artifact_correction_ssp.py | 3 ++- 4 files changed, 18 insertions(+), 11 deletions(-) diff --git a/doc/changes/devel.rst b/doc/changes/devel.rst index 2819e361a25..d5089fd95ee 100644 --- a/doc/changes/devel.rst +++ b/doc/changes/devel.rst @@ -60,6 +60,7 @@ Bugs - Fix bug with multi-plot 3D rendering where only one plot was updated (:gh:`11896` by `Eric Larson`_) - Fix bug where ``verbose`` level was not respected inside parallel jobs (:gh:`12154` by `Eric Larson`_) - Fix bug where subject birthdays were not correctly read by :func:`mne.io.read_raw_snirf` (:gh:`11912` by `Eric Larson`_) +- Fix bug where warnings were emitted when computing spectra for channels marked as bad (:gh:`12186` by `Eric Larson`_) - Fix bug with :func:`mne.chpi.compute_head_pos` for CTF data where digitization points were modified in-place, producing an incorrect result during a save-load round-trip (:gh:`11934` by `Eric Larson`_) - Fix bug where non-compliant stimulus data streams were not ignored by :func:`mne.io.read_raw_snirf` (:gh:`11915` by `Johann Benerradi`_) - Fix bug with ``pca=False`` in :func:`mne.minimum_norm.compute_source_psd` (:gh:`11927` by `Alex Gramfort`_) diff --git a/mne/time_frequency/spectrum.py b/mne/time_frequency/spectrum.py index 3d6acbc3914..c39d5f8a078 100644 --- a/mne/time_frequency/spectrum.py +++ b/mne/time_frequency/spectrum.py @@ -439,15 +439,17 @@ def _check_values(self): """Check PSD results for correct shape and bad values.""" assert len(self._dims) == self._data.ndim, (self._dims, self._data.ndim) assert self._data.shape == self._shape - # negative values OK if the spectrum is really fourier coefficients - if "taper" in self._dims: - return # TODO: should this be more fine-grained (report "chan X in epoch Y")? ch_dim = self._dims.index("channel") - dims = np.arange(self._data.ndim).tolist() + dims = list(range(self._data.ndim)) dims.pop(ch_dim) # take min() across all but the channel axis - bad_value = self._data.min(axis=tuple(dims)) <= 0 + # (if the abs becomes memory intensive we could iterate over channels) + use_data = self._data + if use_data.dtype.kind == "c": + use_data = np.abs(use_data) + bad_value = use_data.min(axis=tuple(dims)) == 0 + bad_value &= ~np.isin(self.ch_names, self.info["bads"]) if bad_value.any(): chs = np.array(self.ch_names)[bad_value].tolist() s = _pl(bad_value.sum()) diff --git a/mne/time_frequency/tests/test_spectrum.py b/mne/time_frequency/tests/test_spectrum.py index 96fe89a2e6d..75768aff130 100644 --- a/mne/time_frequency/tests/test_spectrum.py +++ b/mne/time_frequency/tests/test_spectrum.py @@ -1,4 +1,3 @@ -from contextlib import nullcontext from functools import partial import numpy as np @@ -359,7 +358,6 @@ def test_spectrum_complex(method, average): assert len(epochs) == 5 assert len(epochs.times) == 2 * sfreq kwargs = dict(output="complex", method=method) - ctx = pytest.warns(UserWarning, match="Zero value") if method == "welch": kwargs["n_fft"] = sfreq want_dims = ("epoch", "channel", "freq") @@ -371,11 +369,9 @@ def test_spectrum_complex(method, average): else: assert method == "multitaper" assert not average - ctx = nullcontext() want_dims = ("epoch", "channel", "taper", "freq") want_shape = (5, 1, 7, sfreq + 1) - with ctx: - spectrum = epochs.compute_psd(**kwargs) + spectrum = epochs.compute_psd(**kwargs) idx = np.argmin(np.abs(spectrum.freqs - freq)) assert spectrum.freqs[idx] == freq assert spectrum._dims == want_dims @@ -389,6 +385,13 @@ def test_spectrum_complex(method, average): coef = coef.mean(-1) # over segments coef = coef.item() assert_allclose(np.angle(coef), phase, rtol=1e-4) + # Now test that it warns appropriately + epochs._data[0, 0, :] = 0 # actually zero for one epoch and ch + with pytest.warns(UserWarning, match="Zero value.*channel 0"): + epochs.compute_psd(**kwargs) + # But not if we mark that channel as bad + epochs.info["bads"] = epochs.ch_names[:1] + epochs.compute_psd(**kwargs) def test_spectrum_kwarg_triaging(raw): diff --git a/tutorials/preprocessing/50_artifact_correction_ssp.py b/tutorials/preprocessing/50_artifact_correction_ssp.py index 6adcacfb4e0..2d18367efe8 100644 --- a/tutorials/preprocessing/50_artifact_correction_ssp.py +++ b/tutorials/preprocessing/50_artifact_correction_ssp.py @@ -111,7 +111,8 @@ # individual spectrum for each sensor, or an average (with confidence band) # across sensors: -spectrum = empty_room_raw.compute_psd(verbose="error") # ignore zero value warning +raw.info["bads"] = ["MEG 2443"] +spectrum = empty_room_raw.compute_psd() for average in (False, True): spectrum.plot(average=average, dB=False, xscale="log", picks="data", exclude="bads") From 2bc47d078999203a23a6c78ac55952c9e699c861 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Wed, 8 Nov 2023 14:56:12 -0500 Subject: [PATCH 054/405] BUG: Fix bug with default alpha and axes (#12187) --- mne/viz/_3d.py | 2 +- mne/viz/backends/_pyvista.py | 2 +- tutorials/forward/20_source_alignment.py | 3 +-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/mne/viz/_3d.py b/mne/viz/_3d.py index 0063aa37bdd..41d6f0476b7 100644 --- a/mne/viz/_3d.py +++ b/mne/viz/_3d.py @@ -984,7 +984,7 @@ def _handle_sensor_types(meg, eeg, fnirs): fnirs=dict(channels="fnirs", pairs="fnirs_pairs"), ) sensor_alpha = { - key: 0.25 if key == "meg_helmet" else 0.8 + key: dict(meg_helmet=0.25, meg=0.25).get(key, 0.8) for ch_dict in alpha_map.values() for key in ch_dict.values() } diff --git a/mne/viz/backends/_pyvista.py b/mne/viz/backends/_pyvista.py index 1b4bfe6c37b..221672d9915 100644 --- a/mne/viz/backends/_pyvista.py +++ b/mne/viz/backends/_pyvista.py @@ -733,7 +733,7 @@ def quiver3d( mesh=mesh, color=color, opacity=opacity, - scalars=mesh_scalars, + scalars=mesh_scalars if colormap is not None else None, colormap=colormap, show_scalar_bar=False, backface_culling=backface_culling, diff --git a/tutorials/forward/20_source_alignment.py b/tutorials/forward/20_source_alignment.py index 292e5346415..83ad252bfda 100644 --- a/tutorials/forward/20_source_alignment.py +++ b/tutorials/forward/20_source_alignment.py @@ -66,8 +66,7 @@ # to their equivalent locations in another. The three main coordinate frames # are: # -# * :blue:`"meg"`: the coordinate frame for the physical locations of MEG -# sensors +# * :blue:`"meg"`: the coordinate frame for the physical locations of MEG sensors # * :gray:`"mri"`: the coordinate frame for MRI images, and scalp/skull/brain # surfaces derived from the MRI images # * :pink:`"head"`: the coordinate frame for digitized sensor locations and From fd53fc44915ee3bea2f18c468eece4ed84476e1d Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Wed, 8 Nov 2023 15:18:34 -0500 Subject: [PATCH 055/405] MAINT: Fix CIs (#12188) --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ac95ac1f591..6d45a299fdc 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -106,7 +106,7 @@ jobs: - name: 'Install OSMesa VTK variant' run: | # TODO: As of 2023/02/28, notebook tests need a pinned mesalib - mamba install -c conda-forge "vtk>=9.2=*osmesa*" "vtk-base>=9.2=*osmesa*" "mesalib=23.1.4" + mamba install -c conda-forge "vtk>=9.2=*osmesa*" "vtk-base>=9.2=*osmesa*" "mesalib=23.1.4" "numpy=1.24.4" "numba=0.57.1" mamba list if: matrix.kind == 'notebook' - run: ./tools/github_actions_dependencies.sh From 7b3e3c931914ee655486e7b8d5a5a30668ce136f Mon Sep 17 00:00:00 2001 From: Pablo Mainar Date: Thu, 9 Nov 2023 22:51:54 +0100 Subject: [PATCH 056/405] Fix copy-view issue in epochs (#12121) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Daniel McCloy Co-authored-by: Eric Larson --- doc/changes/devel.rst | 2 + doc/changes/names.inc | 2 + examples/datasets/kernel_phantom.py | 1 + examples/datasets/limo_data.py | 1 - examples/decoding/decoding_csp_eeg.py | 4 +- examples/decoding/decoding_csp_timefreq.py | 4 +- ...decoding_time_generalization_conditions.py | 4 +- .../decoding_unsupervised_spatial_filter.py | 2 +- examples/decoding/ems_filtering.py | 2 +- examples/decoding/linear_model_patterns.py | 2 +- examples/decoding/ssd_spatial_filters.py | 2 +- examples/preprocessing/otp.py | 3 +- examples/stats/sensor_regression.py | 15 +- mne/_fiff/tests/test_reference.py | 14 +- mne/beamformer/_dics.py | 2 +- mne/beamformer/_lcmv.py | 2 +- mne/channels/channels.py | 5 +- mne/channels/tests/test_channels.py | 2 +- mne/channels/tests/test_interpolation.py | 4 +- mne/decoding/tests/test_base.py | 2 +- mne/decoding/tests/test_csp.py | 6 +- mne/decoding/tests/test_ems.py | 2 +- mne/decoding/tests/test_transformer.py | 10 +- mne/epochs.py | 131 ++++++++++++++---- mne/forward/_field_interpolation.py | 6 +- mne/forward/tests/test_field_interpolation.py | 2 +- mne/io/eeglab/tests/test_eeglab.py | 2 +- mne/io/fieldtrip/tests/test_fieldtrip.py | 4 +- mne/io/kit/tests/test_kit.py | 4 +- mne/minimum_norm/time_frequency.py | 2 +- mne/preprocessing/ica.py | 10 +- mne/preprocessing/tests/test_csd.py | 4 +- mne/preprocessing/tests/test_ica.py | 18 +-- mne/preprocessing/tests/test_regress.py | 2 +- mne/preprocessing/tests/test_stim.py | 8 +- mne/preprocessing/tests/test_xdawn.py | 9 +- mne/preprocessing/xdawn.py | 6 +- mne/rank.py | 3 +- mne/stats/regression.py | 2 +- mne/stats/tests/test_regression.py | 6 + mne/tests/test_epochs.py | 30 +++- mne/tests/test_evoked.py | 4 +- mne/tests/test_filter.py | 2 +- mne/tests/test_rank.py | 22 ++- mne/time_frequency/csd.py | 6 +- mne/time_frequency/tests/test_csd.py | 6 +- mne/time_frequency/tfr.py | 2 +- mne/utils/tests/test_mixin.py | 6 +- mne/viz/_figure.py | 6 +- mne/viz/ica.py | 4 +- mne/viz/tests/test_epochs.py | 2 +- mne/viz/topo.py | 2 +- tutorials/epochs/10_epochs_overview.py | 4 +- tutorials/epochs/15_baseline_regression.py | 2 +- .../epochs/60_make_fixed_length_epochs.py | 2 +- tutorials/machine-learning/50_decoding.py | 2 +- tutorials/stats-sensor-space/20_erp_stats.py | 4 +- .../75_cluster_ftest_spatiotemporal.py | 2 +- 58 files changed, 286 insertions(+), 134 deletions(-) diff --git a/doc/changes/devel.rst b/doc/changes/devel.rst index d5089fd95ee..f6108f65b30 100644 --- a/doc/changes/devel.rst +++ b/doc/changes/devel.rst @@ -56,6 +56,7 @@ Bugs - Fix bug where ``encoding`` argument was ignored when reading annotations from an EDF file (:gh:`11958` by :newcontrib:`Andrew Gilbert`) - Mark tests ``test_adjacency_matches_ft`` and ``test_fetch_uncompressed_file`` as network tests (:gh:`12041` by :newcontrib:`Maksym Balatsko`) - Fix bug with :func:`mne.channels.read_ch_adjacency` (:gh:`11608` by :newcontrib:`Ivan Zubarev`) +- Fix bug where ``epochs.get_data(..., scalings=...)`` would errantly modify the preloaded data (:gh:`12121` by :newcontrib:`Pablo Mainar` and `Eric Larson`_) - Fix bugs with saving splits for :class:`~mne.Epochs` (:gh:`11876` by `Dmitrii Altukhov`_) - Fix bug with multi-plot 3D rendering where only one plot was updated (:gh:`11896` by `Eric Larson`_) - Fix bug where ``verbose`` level was not respected inside parallel jobs (:gh:`12154` by `Eric Larson`_) @@ -92,6 +93,7 @@ Bugs API changes ~~~~~~~~~~~ +- The default for :meth:`mne.Epochs.get_data` of ``copy=False`` will change to ``copy=True`` in 1.7. Set it explicitly to avoid a warning (:gh:`12121` by :newcontrib:`Pablo Mainar` and `Eric Larson`_) - ``mne.preprocessing.apply_maxfilter`` and ``mne maxfilter`` have been deprecated and will be removed in 1.7. Use :func:`mne.preprocessing.maxwell_filter` (see :ref:`this tutorial `) in Python or the command-line utility from MEGIN ``maxfilter`` and :func:`mne.bem.fit_sphere_to_headshape` instead (:gh:`11938` by `Eric Larson`_) - :func:`mne.io.kit.read_mrk` reading pickled files is deprecated using something like ``np.savetxt(fid, pts, delimiter="\t", newline="\n")`` to save your points instead (:gh:`11937` by `Eric Larson`_) - Replace legacy ``inst.pick_channels`` and ``inst.pick_types`` with ``inst.pick`` (where ``inst`` is an instance of :class:`~mne.io.Raw`, :class:`~mne.Epochs`, or :class:`~mne.Evoked`) wherever possible (:gh:`11907` by `Clemens Brunner`_) diff --git a/doc/changes/names.inc b/doc/changes/names.inc index 925e38bfd22..3d441e2289f 100644 --- a/doc/changes/names.inc +++ b/doc/changes/names.inc @@ -406,6 +406,8 @@ .. _Pablo-Arias: https://github.com/Pablo-Arias +.. _Pablo Mainar: https://github.com/pablomainar + .. _Padma Sundaram: https://www.nmr.mgh.harvard.edu/user/8071 .. _Paul Pasler: https://github.com/ppasler diff --git a/examples/datasets/kernel_phantom.py b/examples/datasets/kernel_phantom.py index 51c6c847de6..f71a07ce7d5 100644 --- a/examples/datasets/kernel_phantom.py +++ b/examples/datasets/kernel_phantom.py @@ -49,6 +49,7 @@ # %% # The data covariance has an interesting structure because of densely packed sensors: + cov = mne.compute_covariance(epochs, tmax=-0.01) mne.viz.plot_cov(cov, raw.info) diff --git a/examples/datasets/limo_data.py b/examples/datasets/limo_data.py index 62fb0322295..4f5291358c3 100644 --- a/examples/datasets/limo_data.py +++ b/examples/datasets/limo_data.py @@ -37,7 +37,6 @@ # License: BSD-3-Clause # %% - import matplotlib.pyplot as plt import numpy as np diff --git a/examples/decoding/decoding_csp_eeg.py b/examples/decoding/decoding_csp_eeg.py index 896489252f2..9be079d928f 100644 --- a/examples/decoding/decoding_csp_eeg.py +++ b/examples/decoding/decoding_csp_eeg.py @@ -78,8 +78,8 @@ # Define a monte-carlo cross-validation generator (reduce variance): scores = [] -epochs_data = epochs.get_data() -epochs_data_train = epochs_train.get_data() +epochs_data = epochs.get_data(copy=False) +epochs_data_train = epochs_train.get_data(copy=False) cv = ShuffleSplit(10, test_size=0.2, random_state=42) cv_split = cv.split(epochs_data_train) diff --git a/examples/decoding/decoding_csp_timefreq.py b/examples/decoding/decoding_csp_timefreq.py index c1f88588326..cfeaf326ce6 100644 --- a/examples/decoding/decoding_csp_timefreq.py +++ b/examples/decoding/decoding_csp_timefreq.py @@ -105,7 +105,7 @@ epochs.drop_bad() y = le.fit_transform(epochs.events[:, 2]) - X = epochs.get_data() + X = epochs.get_data(copy=False) # Save mean scores over folds for each frequency and time window freq_scores[freq] = np.mean( @@ -165,7 +165,7 @@ w_tmax = w_time + w_size / 2.0 # Crop data into time-window of interest - X = epochs.copy().crop(w_tmin, w_tmax).get_data() + X = epochs.get_data(tmin=w_tmin, tmax=w_tmax, copy=False) # Save mean scores over folds for each frequency and time window tf_scores[freq, t] = np.mean( diff --git a/examples/decoding/decoding_time_generalization_conditions.py b/examples/decoding/decoding_time_generalization_conditions.py index beb69831b8b..a81123c73d7 100644 --- a/examples/decoding/decoding_time_generalization_conditions.py +++ b/examples/decoding/decoding_time_generalization_conditions.py @@ -77,12 +77,12 @@ # Fit classifiers on the epochs where the stimulus was presented to the left. # Note that the experimental condition y indicates auditory or visual -time_gen.fit(X=epochs["Left"].get_data(), y=epochs["Left"].events[:, 2] > 2) +time_gen.fit(X=epochs["Left"].get_data(copy=False), y=epochs["Left"].events[:, 2] > 2) # %% # Score on the epochs where the stimulus was presented to the right. scores = time_gen.score( - X=epochs["Right"].get_data(), y=epochs["Right"].events[:, 2] > 2 + X=epochs["Right"].get_data(copy=False), y=epochs["Right"].events[:, 2] > 2 ) # %% diff --git a/examples/decoding/decoding_unsupervised_spatial_filter.py b/examples/decoding/decoding_unsupervised_spatial_filter.py index 07c18813ab8..b27c16e003a 100644 --- a/examples/decoding/decoding_unsupervised_spatial_filter.py +++ b/examples/decoding/decoding_unsupervised_spatial_filter.py @@ -58,7 +58,7 @@ verbose=False, ) -X = epochs.get_data() +X = epochs.get_data(copy=False) ############################################################################## # Transform data with PCA computed on the average ie evoked response diff --git a/examples/decoding/ems_filtering.py b/examples/decoding/ems_filtering.py index ff6296bf1f1..0273643c61a 100644 --- a/examples/decoding/ems_filtering.py +++ b/examples/decoding/ems_filtering.py @@ -64,7 +64,7 @@ epochs.pick("grad") # Setup the data to use it a scikit-learn way: -X = epochs.get_data() # The MEG data +X = epochs.get_data(copy=False) # The MEG data y = epochs.events[:, 2] # The conditions indices n_epochs, n_channels, n_times = X.shape diff --git a/examples/decoding/linear_model_patterns.py b/examples/decoding/linear_model_patterns.py index 4b23e5d1e56..caf53603f97 100644 --- a/examples/decoding/linear_model_patterns.py +++ b/examples/decoding/linear_model_patterns.py @@ -60,7 +60,7 @@ # get MEG data meg_epochs = epochs.copy().pick(picks="meg", exclude="bads") -meg_data = meg_epochs.get_data().reshape(len(labels), -1) +meg_data = meg_epochs.get_data(copy=False).reshape(len(labels), -1) # %% # Decoding in sensor space using a LogisticRegression classifier diff --git a/examples/decoding/ssd_spatial_filters.py b/examples/decoding/ssd_spatial_filters.py index a2bdcabf9a1..c3165a7110f 100644 --- a/examples/decoding/ssd_spatial_filters.py +++ b/examples/decoding/ssd_spatial_filters.py @@ -146,7 +146,7 @@ h_trans_bandwidth=1, ), ) -ssd_epochs.fit(X=epochs.get_data()) +ssd_epochs.fit(X=epochs.get_data(copy=False)) # Plot topographies. pattern_epochs = mne.EvokedArray(data=ssd_epochs.patterns_[:4].T, info=ssd_epochs.info) diff --git a/examples/preprocessing/otp.py b/examples/preprocessing/otp.py index 7e5e28561fb..afef134c61d 100644 --- a/examples/preprocessing/otp.py +++ b/examples/preprocessing/otp.py @@ -13,7 +13,6 @@ # License: BSD-3-Clause # %% - import numpy as np import mne @@ -70,7 +69,7 @@ def compute_bias(raw): sphere = mne.make_sphere_model(r0=(0.0, 0.0, 0.0), head_radius=None, verbose=False) cov = mne.compute_covariance(epochs, tmax=0, method="oas", rank=None, verbose=False) idx = epochs.time_as_index(0.036)[0] - data = epochs.get_data()[:, :, idx].T + data = epochs.get_data(copy=False)[:, :, idx].T evoked = mne.EvokedArray(data, epochs.info, tmin=0.0) dip = fit_dipole(evoked, cov, sphere, n_jobs=None, verbose=False)[0] actual_pos = mne.dipole.get_phantom_dipoles()[0][dipole_number - 1] diff --git a/examples/stats/sensor_regression.py b/examples/stats/sensor_regression.py index 4d5b02782f3..28d63360776 100644 --- a/examples/stats/sensor_regression.py +++ b/examples/stats/sensor_regression.py @@ -18,10 +18,6 @@ of the words for which we have EEG activity. For the general methodology, see e.g. :footcite:`HaukEtAl2006`. - -References ----------- -.. footbibliography:: """ # Authors: Tal Linzen # Denis A. Engemann @@ -43,7 +39,7 @@ epochs = mne.read_epochs(path) print(epochs.metadata.head()) -############################################################################## +# %% # Psycholinguistically relevant word characteristics are continuous. I.e., # concreteness or imaginability is a graded property. In the metadata, # we have concreteness ratings on a 5-point scale. We can show the dependence @@ -59,7 +55,7 @@ evokeds, colors=colors, split_legend=True, cmap=(name + " Percentile", "viridis") ) -############################################################################## +# %% # We observe that there appears to be a monotonic dependence of EEG on # concreteness. We can also conduct a continuous analysis: single-trial level # regression with concreteness as a continuous (although here, binned) @@ -72,7 +68,7 @@ title=cond, ts_args=dict(time_unit="s"), topomap_args=dict(time_unit="s") ) -############################################################################## +# %% # Because the :func:`~mne.stats.linear_regression` function also estimates # p values, we can -- # after applying FDR correction for multiple comparisons -- also visualise the @@ -85,3 +81,8 @@ reject_H0, fdr_pvals = fdr_correction(res["Concreteness"].p_val.data) evoked = res["Concreteness"].beta evoked.plot_image(mask=reject_H0, time_unit="s") + +# %% +# References +# ---------- +# .. footbibliography:: diff --git a/mne/_fiff/tests/test_reference.py b/mne/_fiff/tests/test_reference.py index 4b94b4a8665..3bd540779ed 100644 --- a/mne/_fiff/tests/test_reference.py +++ b/mne/_fiff/tests/test_reference.py @@ -620,12 +620,10 @@ def test_add_reference(): assert_equal(epochs_ref._data.shape[1], epochs._data.shape[1] + 1) _check_channel_names(epochs_ref, "Ref") ref_idx = epochs_ref.ch_names.index("Ref") - ref_data = epochs_ref.get_data()[:, ref_idx, :] + ref_data = epochs_ref.get_data(picks=[ref_idx])[:, 0] assert_array_equal(ref_data, 0) picks_eeg = pick_types(epochs.info, meg=False, eeg=True) - assert_array_equal( - epochs.get_data()[:, picks_eeg, :], epochs_ref.get_data()[:, picks_eeg, :] - ) + assert_array_equal(epochs.get_data(picks_eeg), epochs_ref.get_data(picks_eeg)) # add two reference channels to epochs raw = read_raw_fif(fif_fname, preload=True) @@ -650,12 +648,10 @@ def test_add_reference(): ref_idy = epochs_ref.ch_names.index("M2") assert_equal(epochs_ref.info["chs"][ref_idx]["ch_name"], "M1") assert_equal(epochs_ref.info["chs"][ref_idy]["ch_name"], "M2") - ref_data = epochs_ref.get_data()[:, [ref_idx, ref_idy], :] + ref_data = epochs_ref.get_data([ref_idx, ref_idy]) assert_array_equal(ref_data, 0) picks_eeg = pick_types(epochs.info, meg=False, eeg=True) - assert_array_equal( - epochs.get_data()[:, picks_eeg, :], epochs_ref.get_data()[:, picks_eeg, :] - ) + assert_array_equal(epochs.get_data(picks_eeg), epochs_ref.get_data(picks_eeg)) # add reference channel to evoked raw = read_raw_fif(fif_fname, preload=True) @@ -725,7 +721,7 @@ def test_add_reference(): data = data.get_data() epochs = make_fixed_length_epochs(raw).load_data() data_2 = epochs.copy().add_reference_channels(["REF"]).pick(picks="eeg") - data_2 = data_2.get_data()[0] + data_2 = data_2.get_data(copy=False)[0] assert_allclose(data, data_2) evoked = epochs.average() data_3 = evoked.copy().add_reference_channels(["REF"]).pick(picks="eeg") diff --git a/mne/beamformer/_dics.py b/mne/beamformer/_dics.py index 5fe73244485..a368b7fce0a 100644 --- a/mne/beamformer/_dics.py +++ b/mne/beamformer/_dics.py @@ -493,7 +493,7 @@ def apply_dics_epochs(epochs, filters, return_generator=False, verbose=None): tmin = epochs.times[0] sel = _check_channels_spatial_filter(epochs.ch_names, filters) - data = epochs.get_data()[:, sel, :] + data = epochs.get_data(sel) stcs = _apply_dics(data=data, filters=filters, info=info, tmin=tmin) diff --git a/mne/beamformer/_lcmv.py b/mne/beamformer/_lcmv.py index c096582cc1a..d89fbc35342 100644 --- a/mne/beamformer/_lcmv.py +++ b/mne/beamformer/_lcmv.py @@ -402,7 +402,7 @@ def apply_lcmv_epochs(epochs, filters, *, return_generator=False, verbose=None): tmin = epochs.times[0] sel = _check_channels_spatial_filter(epochs.ch_names, filters) - data = epochs.get_data()[:, sel, :] + data = epochs.get_data(sel) stcs = _apply_lcmv(data=data, filters=filters, info=info, tmin=tmin) if not return_generator: diff --git a/mne/channels/channels.py b/mne/channels/channels.py index 725aa884493..85484fa26f1 100644 --- a/mne/channels/channels.py +++ b/mne/channels/channels.py @@ -1898,7 +1898,10 @@ def combine_channels( ch_idx = list(range(inst.info["nchan"])) ch_names = inst.info["ch_names"] ch_types = inst.get_channel_types() - inst_data = inst.data if isinstance(inst, Evoked) else inst.get_data() + kwargs = dict() + if isinstance(inst, BaseEpochs): + kwargs["copy"] = False + inst_data = inst.get_data(**kwargs) groups = OrderedDict(deepcopy(groups)) # Convert string values of ``method`` into callables diff --git a/mne/channels/tests/test_channels.py b/mne/channels/tests/test_channels.py index 7e27f301048..8e3e482659c 100644 --- a/mne/channels/tests/test_channels.py +++ b/mne/channels/tests/test_channels.py @@ -615,7 +615,7 @@ def test_equalize_channels(): assert raw2.ch_names == ["CH1", "CH2"] assert_array_equal(raw2.get_data(), [[1.0], [2.0]]) assert epochs2.ch_names == ["CH1", "CH2"] - assert_array_equal(epochs2.get_data(), [[[3.0], [2.0]]]) + assert_array_equal(epochs2.get_data(copy=False), [[[3.0], [2.0]]]) assert cov2.ch_names == ["CH1", "CH2"] assert cov2["bads"] == cov["bads"] assert ave2.ch_names == ave.ch_names diff --git a/mne/channels/tests/test_interpolation.py b/mne/channels/tests/test_interpolation.py index 58cb6a1e669..4f37494e652 100644 --- a/mne/channels/tests/test_interpolation.py +++ b/mne/channels/tests/test_interpolation.py @@ -232,10 +232,10 @@ def test_interpolation_meg(): assert len(raw_meg.info["bads"]) == len(raw_meg.info["bads"]) # MEG -- epochs - data1 = epochs_meg.get_data()[:, pick, :].ravel() + data1 = epochs_meg.get_data(pick).ravel() epochs_meg.info.normalize_proj() epochs_meg.interpolate_bads(mode="fast") - data2 = epochs_meg.get_data()[:, pick, :].ravel() + data2 = epochs_meg.get_data(pick).ravel() assert np.corrcoef(data1, data2)[0, 1] > thresh assert len(epochs_meg.info["bads"]) == 0 diff --git a/mne/decoding/tests/test_base.py b/mne/decoding/tests/test_base.py index 09dc43c1f8d..885d7ff04f5 100644 --- a/mne/decoding/tests/test_base.py +++ b/mne/decoding/tests/test_base.py @@ -317,7 +317,7 @@ def test_get_coef_multiclass_full(n_classes, n_channels, n_times): ) scorer = "roc_auc_ovr_weighted" time_gen = GeneralizingEstimator(clf, scorer, verbose=True) - X = epochs.get_data() + X = epochs.get_data(copy=False) y = epochs.events[:, 2] n_splits = 3 cv = StratifiedKFold(n_splits=n_splits) diff --git a/mne/decoding/tests/test_csp.py b/mne/decoding/tests/test_csp.py index 788a4040de8..5ad66885392 100644 --- a/mne/decoding/tests/test_csp.py +++ b/mne/decoding/tests/test_csp.py @@ -123,7 +123,7 @@ def test_csp(): preload=True, proj=False, ) - epochs_data = epochs.get_data() + epochs_data = epochs.get_data(copy=False) n_channels = epochs_data.shape[1] y = epochs.events[:, -1] @@ -182,7 +182,7 @@ def test_csp(): proj=False, preload=True, ) - epochs_data = epochs.get_data() + epochs_data = epochs.get_data(copy=False) n_channels = epochs_data.shape[1] n_channels = epochs_data.shape[1] @@ -256,7 +256,7 @@ def test_regularized_csp(): epochs = Epochs( raw, events, event_id, tmin, tmax, picks=picks, baseline=(None, 0), preload=True ) - epochs_data = epochs.get_data() + epochs_data = epochs.get_data(copy=False) n_channels = epochs_data.shape[1] n_components = 3 diff --git a/mne/decoding/tests/test_ems.py b/mne/decoding/tests/test_ems.py index 6238058658d..44bb0f86135 100644 --- a/mne/decoding/tests/test_ems.py +++ b/mne/decoding/tests/test_ems.py @@ -76,7 +76,7 @@ def test_ems(): raw.close() # EMS transformer, check that identical to compute_ems - X = epochs.get_data() + X = epochs.get_data(copy=False) y = epochs.events[:, 2] X = X / np.std(X) # X scaled outside cv in compute_ems Xt, coefs = list(), list() diff --git a/mne/decoding/tests/test_transformer.py b/mne/decoding/tests/test_transformer.py index f1a84c5d41d..cbd586601db 100644 --- a/mne/decoding/tests/test_transformer.py +++ b/mne/decoding/tests/test_transformer.py @@ -55,7 +55,7 @@ def test_scaler(info, method): epochs = Epochs( raw, events, event_id, tmin, tmax, picks=picks, baseline=(None, 0), preload=True ) - epochs_data = epochs.get_data() + epochs_data = epochs.get_data(copy=False) y = epochs.events[:, -1] epochs_data_t = epochs_data.transpose([1, 0, 2]) @@ -115,7 +115,7 @@ def test_scaler(info, method): picks=np.arange(len(raw.ch_names)), ) # non-data chs scaler = Scaler(epochs_bad.info, None) - pytest.raises(ValueError, scaler.fit, epochs_bad.get_data(), y) + pytest.raises(ValueError, scaler.fit, epochs_bad.get_data(copy=False), y) def test_filterestimator(): @@ -129,7 +129,7 @@ def test_filterestimator(): epochs = Epochs( raw, events, event_id, tmin, tmax, picks=picks, baseline=(None, 0), preload=True ) - epochs_data = epochs.get_data() + epochs_data = epochs.get_data(copy=False) # Add tests for different combinations of l_freq and h_freq filt = FilterEstimator(epochs.info, l_freq=40, h_freq=80) @@ -180,7 +180,7 @@ def test_psdestimator(): epochs = Epochs( raw, events, event_id, tmin, tmax, picks=picks, baseline=(None, 0), preload=True ) - epochs_data = epochs.get_data() + epochs_data = epochs.get_data(copy=False) psd = PSDEstimator(2 * np.pi, 0, np.inf) y = epochs.events[:, -1] X = psd.fit_transform(epochs_data, y) @@ -244,7 +244,7 @@ def test_unsupervised_spatial_filter(): pytest.raises(ValueError, UnsupervisedSpatialFilter, KernelRidge(2)) # Test fit - X = epochs.get_data() + X = epochs.get_data(copy=False) n_components = 4 usf = UnsupervisedSpatialFilter(PCA(n_components)) usf.fit(X) diff --git a/mne/epochs.py b/mne/epochs.py index 50ecf10ee64..b7afada3d1a 100644 --- a/mne/epochs.py +++ b/mne/epochs.py @@ -154,7 +154,7 @@ def _save_part(fid, epochs, fmt, n_parts, next_fname, next_idx): start_block(fid, FIFF.FIFFB_MNE_EPOCHS) # write events out after getting data to ensure bad events are dropped - data = epochs.get_data() + data = epochs.get_data(copy=False) _check_option("fmt", fmt, ["single", "double"]) @@ -1590,6 +1590,7 @@ def _get_data( units=None, tmin=None, tmax=None, + copy=False, on_empty="warn", verbose=None, ): @@ -1612,6 +1613,9 @@ def _get_data( """ from .io.base import _get_ch_factors + if copy is not None: + _validate_type(copy, bool, "copy") + # Handle empty epochs self._handle_empty(on_empty, "_get_data") # if called with 'out=False', the call came from 'drop_bad()' @@ -1673,19 +1677,23 @@ def _get_data( # handle units param only if we are going to return data (out==True) if (units is not None) and out: ch_factors = _get_ch_factors(self, units, picks) + else: + ch_factors = None if self._bad_dropped: if not out: return if self.preload: - data = data[select] - if orig_picks is not None: - data = data[:, picks] - if units is not None: - data *= ch_factors[:, np.newaxis] - if start != 0 or stop != self.times.size: - data = data[..., start:stop] - return data + return self._data_sel_copy_scale( + data, + select=select, + orig_picks=orig_picks, + picks=picks, + ch_factors=ch_factors, + start=start, + stop=stop, + copy=copy, + ) # we need to load from disk, drop, and return data detrend_picks = self._detrend_picks @@ -1777,16 +1785,63 @@ def _get_data( good_idx, None, copy=False, drop_event_id=False, select_data=False ) - if out: - if orig_picks is not None: - data = data[:, picks] - if units is not None: - data *= ch_factors[:, np.newaxis] - if start != 0 or stop != self.times.size: - data = data[..., start:stop] - return data + if not out: + return + return self._data_sel_copy_scale( + data, + select=slice(None), + orig_picks=orig_picks, + picks=picks, + ch_factors=ch_factors, + start=start, + stop=stop, + copy=copy, + ) + + def _data_sel_copy_scale( + self, data, *, select, orig_picks, picks, ch_factors, start, stop, copy + ): + # data arg starts out as self._data when data is preloaded + data_is_self_data = bool(self.preload) + logger.debug(f"Data is self data: {data_is_self_data}") + # only two types of epoch subselection allowed + assert isinstance(select, (slice, np.ndarray)), type(select) + if not isinstance(select, slice): + logger.debug(" Copying, fancy indexed epochs") + data_is_self_data = False # copy (fancy indexing) + elif select != slice(None): + logger.debug(" Slicing epochs") + if orig_picks is not None: + logger.debug(" Copying, fancy indexed picks") + assert isinstance(picks, np.ndarray), type(picks) + data_is_self_data = False # copy (fancy indexing) else: - return None + picks = slice(None) + if not all(isinstance(x, slice) and x == slice(None) for x in (select, picks)): + data = data[select][:, picks] + del picks + if start != 0 or stop != self.times.size: + logger.debug(" Slicing time") + data = data[..., start:stop] # view (slice) + if ch_factors is not None: + if data_is_self_data: + logger.debug(" Copying, scale factors applied") + data = data.copy() + data_is_self_data = False + 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() + return data @property def _detrend_picks(self): @@ -1797,8 +1852,18 @@ def _detrend_picks(self): else: return [] - @fill_doc - def get_data(self, picks=None, item=None, units=None, tmin=None, tmax=None): + @verbose + def get_data( + self, + picks=None, + item=None, + units=None, + tmin=None, + tmax=None, + *, + copy=None, + verbose=None, + ): """Get all epochs as a 3D array. Parameters @@ -1823,13 +1888,31 @@ def get_data(self, picks=None, item=None, units=None, tmin=None, tmax=None): End time of data to get in seconds. .. versionadded:: 0.24.0 + copy : bool + Whether to return a copy of the object's data, or (if possible) a view. + See :ref:`the NumPy docs ` for an + explanation. Default is ``False`` in 1.6 but will change to ``True`` in 1.7, + set it explicitly to avoid a warning in some cases. A view is only possible + when ``item is None``, ``picks is None``, ``units is None``, and data are + preloaded. + + .. warning:: + Using ``copy=False`` and then modifying the returned ``data`` will in + turn modify the Epochs object. Use with caution! + + .. versionchanged:: 1.7 + The default changed from ``False`` to ``True``. + %(verbose)s Returns ------- data : array of shape (n_epochs, n_channels, n_times) - A view on epochs data. + The epochs data. Will be a copy when ``copy=True`` and will be a view + when possible when ``copy=False``. """ - return self._get_data(picks=picks, item=item, units=units, tmin=tmin, tmax=tmax) + return self._get_data( + picks=picks, item=item, units=units, tmin=tmin, tmax=tmax, copy=copy + ) @verbose def apply_function( @@ -2095,7 +2178,7 @@ def save( warn("Saving epochs with no data") total_size = 0 else: - d = self[0].get_data() + d = self[0].get_data(copy=False) # this should be guaranteed by subclasses assert d.dtype in (">f8", "c16", " clean_norm > orig_norm / 10 diff --git a/mne/preprocessing/tests/test_stim.py b/mne/preprocessing/tests/test_stim.py index a639ad5e5d1..e4934488a45 100644 --- a/mne/preprocessing/tests/test_stim.py +++ b/mne/preprocessing/tests/test_stim.py @@ -41,18 +41,18 @@ def test_fix_stim_artifact(): epochs = fix_stim_artifact( epochs, tmin=tmin, tmax=tmax, mode="linear", picks=("eeg", "eog") ) - data = epochs.copy().pick(("eeg", "eog")).get_data()[:, :, tmin_samp:tmax_samp] + data = epochs.get_data(("eeg", "eog"))[:, :, tmin_samp:tmax_samp] diff_data0 = np.diff(data[0][0]) diff_data0 -= np.mean(diff_data0) assert_array_almost_equal(diff_data0, np.zeros(len(diff_data0))) - data = epochs.copy().pick(("meg")).get_data()[:, :, tmin_samp:tmax_samp] + data = epochs.get_data("meg")[:, :, tmin_samp:tmax_samp] diff_data0 = np.diff(data[0][0]) diff_data0 -= np.mean(diff_data0) assert np.all(diff_data0 != 0) epochs = fix_stim_artifact(epochs, tmin=tmin, tmax=tmax, mode="window") - data_from_epochs_fix = epochs.get_data()[:, :, tmin_samp:tmax_samp] + data_from_epochs_fix = epochs.get_data(copy=False)[:, :, tmin_samp:tmax_samp] assert not np.all(data_from_epochs_fix != 0) # use window before stimulus in raw @@ -99,7 +99,7 @@ def test_fix_stim_artifact(): e_start = int(np.ceil(epochs.info["sfreq"] * epochs.tmin)) tmin_samp = int(-0.035 * epochs.info["sfreq"]) - e_start tmax_samp = int(-0.015 * epochs.info["sfreq"]) - e_start - data_from_raw_fix = epochs.get_data()[:, :, tmin_samp:tmax_samp] + data_from_raw_fix = epochs.get_data(copy=False)[:, :, tmin_samp:tmax_samp] assert np.all(data_from_raw_fix) == 0.0 # use window after stimulus diff --git a/mne/preprocessing/tests/test_xdawn.py b/mne/preprocessing/tests/test_xdawn.py index 047a35d75dd..59822799853 100644 --- a/mne/preprocessing/tests/test_xdawn.py +++ b/mne/preprocessing/tests/test_xdawn.py @@ -59,8 +59,8 @@ def test_xdawn_picks(): xd.fit(epochs) epochs_out = xd.apply(epochs)["1"] assert epochs_out.info["ch_names"] == epochs.ch_names - assert not (epochs_out.get_data()[:, 0] != data[:, 0]).any() - assert_array_equal(epochs_out.get_data()[:, 1], data[:, 1]) + assert not (epochs_out.get_data([0])[:, 0] != data[:, 0]).any() + assert_array_equal(epochs_out.get_data([1])[:, 0], data[:, 1]) def test_xdawn_fit(): @@ -375,7 +375,10 @@ def test_xdawn_decoding_performance(): ) cv = KFold(n_splits=3, shuffle=False) - for pipe, X in ((xdawn_pipe, epochs), (xdawn_trans_pipe, epochs.get_data())): + for pipe, X in ( + (xdawn_pipe, epochs), + (xdawn_trans_pipe, epochs.get_data(copy=False)), + ): predictions = np.empty_like(y, dtype=float) for train, test in cv.split(X, y): pipe.fit(X[train], y[train]) diff --git a/mne/preprocessing/xdawn.py b/mne/preprocessing/xdawn.py index ab9684cd07d..aed801068ca 100644 --- a/mne/preprocessing/xdawn.py +++ b/mne/preprocessing/xdawn.py @@ -451,7 +451,7 @@ def fit(self, epochs, y=None): raise ValueError("epochs must be an Epochs object.") picks = _pick_data_channels(epochs.info) use_info = pick_info(epochs.info, picks) - X = epochs.get_data()[:, picks, :] + X = epochs.get_data(picks) y = epochs.events[:, 2] if y is None else y self.event_id_ = epochs.event_id @@ -525,7 +525,7 @@ def transform(self, inst): Spatially filtered signals. """ # noqa: E501 if isinstance(inst, BaseEpochs): - X = inst.get_data() + X = inst.get_data(copy=False) elif isinstance(inst, Evoked): X = inst.data elif isinstance(inst, np.ndarray): @@ -636,7 +636,7 @@ def _apply_epochs(self, epochs, include, exclude, event_id, picks): # special case where epochs come picked but fit was 'unpicked'. epochs_dict = dict() - data = np.hstack(epochs.get_data()[:, picks]) + data = np.hstack(epochs.get_data(picks)) for eid in event_id: data_r = self._pick_sources(data, include, exclude, eid) diff --git a/mne/rank.py b/mne/rank.py index 0cab1f3a563..34284db50de 100644 --- a/mne/rank.py +++ b/mne/rank.py @@ -442,8 +442,7 @@ def compute_rank( if isinstance(inst, BaseRaw): data = inst.get_data(picks, reject_by_annotation="omit") else: # isinstance(inst, BaseEpochs): - data = inst.get_data()[:, picks, :] - data = np.concatenate(data, axis=1) + data = np.concatenate(inst.get_data(picks), axis=1) if proj: data = np.dot(proj_op, data) this_rank = _estimate_rank_meeg_signals( diff --git a/mne/stats/regression.py b/mne/stats/regression.py index e005832824b..5240f3c61cb 100644 --- a/mne/stats/regression.py +++ b/mne/stats/regression.py @@ -76,7 +76,7 @@ def linear_regression(inst, design_matrix, names=None): if [inst.ch_names[p] for p in picks] != inst.ch_names: warn("Fitting linear model to non-data or bad channels. " "Check picking") msg = "Fitting linear model to epochs" - data = inst.get_data() + data = inst.get_data(copy=False) out = EvokedArray(np.zeros(data.shape[1:]), inst.info, inst.tmin) elif isgenerator(inst): msg = "Fitting linear model to source estimates (generator input)" diff --git a/mne/stats/tests/test_regression.py b/mne/stats/tests/test_regression.py index 190e3ceff87..d36bd75f65b 100644 --- a/mne/stats/tests/test_regression.py +++ b/mne/stats/tests/test_regression.py @@ -71,6 +71,12 @@ def test_regression(): for v1, v2 in zip(lm1[k], lm2[k]): assert_array_equal(v1.data, v2.data) + # Smoke test for fitting on epochs + epochs.load_data() + with pytest.warns(RuntimeWarning, match="non-data"): + linear_regression(epochs, design_matrix) + linear_regression(epochs.copy().pick("eeg"), design_matrix) + @testing.requires_testing_data def test_continuous_regression_no_overlap(): diff --git a/mne/tests/test_epochs.py b/mne/tests/test_epochs.py index 423fe556365..e5ac3892ca8 100644 --- a/mne/tests/test_epochs.py +++ b/mne/tests/test_epochs.py @@ -85,6 +85,13 @@ 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 @@ -276,7 +283,7 @@ def _get_data(preload=False): flat = dict(grad=1e-15, mag=1e-15) -def test_get_data(): +def test_get_data_copy(): """Test the .get_data() method.""" raw, events, picks = _get_data() event_id = {"a/1": 1, "a/2": 2, "b/1": 3, "b/2": 4} @@ -321,6 +328,25 @@ def test_get_data(): with pytest.raises(TypeError, match="tmax .* float, None"): epochs.get_data(tmin=1, tmax=np.ones(5)) + # Test 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") + assert np.shares_memory(data, epochs._data) + assert data is epochs._data + data_orig = data.copy() + # picks, item, and units must be None + data = epochs.get_data(copy=False, picks=[1]) + assert not np.shares_memory(data, epochs._data) + data = epochs.get_data(copy=False, item=[0]) + assert not np.shares_memory(data, epochs._data) + data = epochs.get_data(copy=False, units=dict(eeg="uV")) + assert not np.shares_memory(data, epochs._data) + # Make sure we didn't mess up our values + assert_allclose(data_orig, epochs._data) + def test_hierarchical(): """Test hierarchical access.""" @@ -1033,7 +1059,7 @@ def test_epochs_baseline_basic(preload, tmp_path): epochs = mne.Epochs(raw, events, None, 0, 1e-3, baseline=None, preload=preload) epochs.drop_bad() epochs_nobl = epochs.copy() - epochs_data = epochs.get_data() + epochs_data = epochs.get_data(copy=False) assert epochs_data.shape == (1, 2, 2) expected = data.copy() assert_array_equal(epochs_data[0], expected) diff --git a/mne/tests/test_evoked.py b/mne/tests/test_evoked.py index 9820dcdb5e3..fe3f41f141c 100644 --- a/mne/tests/test_evoked.py +++ b/mne/tests/test_evoked.py @@ -135,7 +135,7 @@ def test_decim(): expected_times = epochs.times[offset::decim] assert_allclose(ev_decim.times, expected_times) assert_allclose(ev_ep_decim.times, expected_times) - expected_data = epochs.get_data()[:, :, offset::decim].mean(axis=0) + expected_data = epochs.get_data(copy=False)[:, :, offset::decim].mean(axis=0) assert_allclose(ev_decim.data, expected_data) assert_allclose(ev_ep_decim.data, expected_data) assert_equal(ev_decim.info["sfreq"], sfreq_new) @@ -911,7 +911,7 @@ def test_hilbert(): raw_hilb = raw.apply_hilbert() epochs_hilb = epochs.apply_hilbert() evoked_hilb = evoked.copy().apply_hilbert() - evoked_hilb_2_data = epochs_hilb.get_data().mean(0) + evoked_hilb_2_data = epochs_hilb.get_data(copy=False).mean(0) assert_allclose(evoked_hilb.data, evoked_hilb_2_data) # This one is only approximate because of edge artifacts evoked_hilb_3 = Epochs(raw_hilb, events).average() diff --git a/mne/tests/test_filter.py b/mne/tests/test_filter.py index 552489f45d1..f2b5ec1b2e7 100644 --- a/mne/tests/test_filter.py +++ b/mne/tests/test_filter.py @@ -487,7 +487,7 @@ def test_resample_below_1_sample(): ) epochs.resample(1) assert len(epochs.times) == 1 - assert epochs.get_data().shape[2] == 1 + assert epochs.get_data(copy=False).shape[2] == 1 @pytest.mark.slowtest diff --git a/mne/tests/test_rank.py b/mne/tests/test_rank.py index f88dd68e282..bde640e276c 100644 --- a/mne/tests/test_rank.py +++ b/mne/tests/test_rank.py @@ -5,7 +5,14 @@ import pytest from numpy.testing import assert_array_equal -from mne import compute_raw_covariance, pick_info, pick_types, read_cov, read_evokeds +from mne import ( + compute_raw_covariance, + make_fixed_length_epochs, + pick_info, + pick_types, + read_cov, + read_evokeds, +) from mne._fiff.pick import _picks_by_type from mne._fiff.proj import _has_eeg_average_ref_proj from mne.cov import prepare_noise_cov @@ -190,6 +197,19 @@ def test_cov_rank_estimation(rank_method, proj, meg): assert rank[ch_type] == expected_rank +@pytest.mark.parametrize( + "rank_method, proj", [("info", True), ("info", False), (None, True), (None, False)] +) +def test_rank_epochs(rank_method, proj): + """Test that raw and epochs give the same results in a simple case.""" + # And a smoke test for epochs + raw = read_raw_fif(raw_fname, preload=True) + epochs = make_fixed_length_epochs(raw, preload=True, proj=False) + rank_raw = compute_rank(raw, rank_method, proj=proj) + rank_epochs = compute_rank(epochs, rank_method, proj=proj) + assert rank_raw == rank_epochs + + @pytest.mark.slowtest # ~3 s apiece on Azure means overall it's slow @testing.requires_testing_data @pytest.mark.parametrize("fname, rank_orig", ((hp_fif_fname, 120), (mf_fif_fname, 67))) diff --git a/mne/time_frequency/csd.py b/mne/time_frequency/csd.py index 78ff9d95bcc..e3499f45e2e 100644 --- a/mne/time_frequency/csd.py +++ b/mne/time_frequency/csd.py @@ -717,7 +717,7 @@ def csd_fourier( """ epochs, projs = _prepare_csd(epochs, tmin, tmax, picks, projs) return csd_array_fourier( - epochs.get_data(), + epochs.get_data(copy=False), sfreq=epochs.info["sfreq"], t0=epochs.tmin, fmin=fmin, @@ -900,7 +900,7 @@ def csd_multitaper( """ epochs, projs = _prepare_csd(epochs, tmin, tmax, picks, projs) return csd_array_multitaper( - epochs.get_data(), + epochs.get_data(copy=False), sfreq=epochs.info["sfreq"], t0=epochs.tmin, fmin=fmin, @@ -1109,7 +1109,7 @@ def csd_morlet( """ epochs, projs = _prepare_csd(epochs, tmin, tmax, picks, projs) return csd_array_morlet( - epochs.get_data(), + epochs.get_data(copy=False), sfreq=epochs.info["sfreq"], frequencies=frequencies, t0=epochs.tmin, diff --git a/mne/time_frequency/tests/test_csd.py b/mne/time_frequency/tests/test_csd.py index 6c306188699..3763e6bdbb4 100644 --- a/mne/time_frequency/tests/test_csd.py +++ b/mne/time_frequency/tests/test_csd.py @@ -454,7 +454,7 @@ def test_csd_fourier(): for (tmin, tmax), as_array in parameters: if as_array: csd = csd_array_fourier( - epochs.get_data(), + epochs.get_data(copy=False), sfreq, epochs.tmin, fmin=9, @@ -510,7 +510,7 @@ def test_csd_multitaper(): for (tmin, tmax), as_array, adaptive in parameters: if as_array: csd = csd_array_multitaper( - epochs.get_data(), + epochs.get_data(copy=False), sfreq, epochs.tmin, adaptive=adaptive, @@ -578,7 +578,7 @@ def test_csd_morlet(): for (tmin, tmax), as_array in parameters: if as_array: csd = csd_array_morlet( - epochs.get_data(), + epochs.get_data(copy=False), sfreq, freqs, t0=epochs.tmin, diff --git a/mne/time_frequency/tfr.py b/mne/time_frequency/tfr.py index 09745e9a1b1..bd3a02865c1 100644 --- a/mne/time_frequency/tfr.py +++ b/mne/time_frequency/tfr.py @@ -2930,7 +2930,7 @@ def _get_data(inst, return_itc): if not isinstance(inst, (BaseEpochs, Evoked)): raise TypeError("inst must be Epochs or Evoked") if isinstance(inst, BaseEpochs): - data = inst.get_data() + data = inst.get_data(copy=False) else: if return_itc: raise ValueError("return_itc must be False for evoked data") diff --git a/mne/utils/tests/test_mixin.py b/mne/utils/tests/test_mixin.py index 32c7abe6a32..aa13f705f14 100644 --- a/mne/utils/tests/test_mixin.py +++ b/mne/utils/tests/test_mixin.py @@ -29,7 +29,11 @@ def test_decimate(): epo_3=mne.make_fixed_length_epochs(raw, preload=False).decimate(2).decimate(3), ) for key, other in others.items(): - assert_allclose(epo.get_data(), other.get_data(), err_msg=key) + assert_allclose( + epo.get_data(copy=False), + other.get_data(copy=False), + err_msg=key, + ) assert_allclose(epo.times, other.times, err_msg=key) evo = epo.average() epo_full = mne.make_fixed_length_epochs(raw, preload=True) diff --git a/mne/viz/_figure.py b/mne/viz/_figure.py index 7bd58f4ee9e..7f958657876 100644 --- a/mne/viz/_figure.py +++ b/mne/viz/_figure.py @@ -327,7 +327,9 @@ def _load_data(self, start=None, stop=None): ) ix_stop = ix_start + self.mne.n_epochs item = slice(ix_start, ix_stop) - data = np.concatenate(self.mne.inst.get_data(item=item), axis=-1) + data = np.concatenate( + self.mne.inst.get_data(item=item, copy=False), axis=-1 + ) times = np.arange(start, stop) / self.mne.info["sfreq"] return data, times @@ -554,7 +556,7 @@ def _create_epoch_histogram(self): """Create peak-to-peak histogram of channel amplitudes.""" epochs = self.mne.inst data = OrderedDict() - ptp = np.ptp(epochs.get_data(), axis=2) + ptp = np.ptp(epochs.get_data(copy=False), axis=2) for ch_type in ("eeg", "mag", "grad"): if ch_type in epochs: data[ch_type] = ptp.T[self.mne.ch_types == ch_type].ravel() diff --git a/mne/viz/ica.py b/mne/viz/ica.py index 24a9af42b12..12002b1cd56 100644 --- a/mne/viz/ica.py +++ b/mne/viz/ica.py @@ -226,7 +226,7 @@ def _plot_ica_properties( # image and erp # we create a new epoch with dropped rows - epoch_data = epochs_src.get_data() + epoch_data = epochs_src.get_data(copy=False) epoch_data = np.insert( arr=epoch_data, obj=(dropped_indices - np.arange(len(dropped_indices))).astype(int), @@ -744,7 +744,7 @@ def _prepare_data_ica_properties(inst, ica, reject_by_annotation=True, reject="a epochs_src = ica.get_sources(inst) dropped_indices = [] kind = "Epochs" - return kind, dropped_indices, epochs_src, epochs_src.get_data() + return kind, dropped_indices, epochs_src, epochs_src.get_data(copy=False) def _plot_ica_sources_evoked(evoked, picks, exclude, title, show, ica, labels=None): diff --git a/mne/viz/tests/test_epochs.py b/mne/viz/tests/test_epochs.py index 77f45ed1598..d3dd90d224d 100644 --- a/mne/viz/tests/test_epochs.py +++ b/mne/viz/tests/test_epochs.py @@ -415,7 +415,7 @@ def test_plot_psd_epochs(epochs): 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] - epochs.get_data()[0, 2, :] = 0 + epochs.get_data(copy=False)[0, 2, :] = 0 for dB in [True, False]: with pytest.warns(UserWarning, match=err_str): epochs.compute_psd().plot(dB=dB) diff --git a/mne/viz/topo.py b/mne/viz/topo.py index 8e363d117e9..3cae18c3ed7 100644 --- a/mne/viz/topo.py +++ b/mne/viz/topo.py @@ -1242,7 +1242,7 @@ def plot_topo_image_epochs( scale_coeffs = [scalings.get(ch_type, 1) for ch_type in ch_types] # scale the data epochs._data *= np.array(scale_coeffs)[:, np.newaxis] - data = epochs.get_data() + data = epochs.get_data(copy=False) # get vlims for each channel type vlim_dict = dict() for ch_type in set(ch_types): diff --git a/tutorials/epochs/10_epochs_overview.py b/tutorials/epochs/10_epochs_overview.py index 8ec3af8b065..7778110b6a5 100644 --- a/tutorials/epochs/10_epochs_overview.py +++ b/tutorials/epochs/10_epochs_overview.py @@ -312,7 +312,9 @@ shorter_epochs = epochs.copy().crop(tmin=-0.1, tmax=0.1, include_tmax=True) for name, obj in dict(Original=epochs, Cropped=shorter_epochs).items(): - print("{} epochs has {} time samples".format(name, obj.get_data().shape[-1])) + print( + "{} epochs has {} time samples".format(name, obj.get_data(copy=False).shape[-1]) + ) # %% # Cropping removed part of the baseline. When printing the diff --git a/tutorials/epochs/15_baseline_regression.py b/tutorials/epochs/15_baseline_regression.py index 7d62a3cae1b..4f3c3456760 100644 --- a/tutorials/epochs/15_baseline_regression.py +++ b/tutorials/epochs/15_baseline_regression.py @@ -142,7 +142,7 @@ epochs.copy() .crop(*baseline) .pick([ch]) - .get_data() # convert to NumPy array + .get_data(copy=False) # convert to NumPy array .mean(axis=-1) # average across timepoints .squeeze() # only 1 channel, so remove singleton dimension ) diff --git a/tutorials/epochs/60_make_fixed_length_epochs.py b/tutorials/epochs/60_make_fixed_length_epochs.py index a311842312a..108435c092e 100644 --- a/tutorials/epochs/60_make_fixed_length_epochs.py +++ b/tutorials/epochs/60_make_fixed_length_epochs.py @@ -94,7 +94,7 @@ # (for more information on filtering, please see :ref:`tut-filter-resample`). epochs.load_data().filter(l_freq=8, h_freq=12) -alpha_data = epochs.get_data() +alpha_data = epochs.get_data(copy=False) # %% # If desired, separate correlation matrices for each epoch can be obtained. diff --git a/tutorials/machine-learning/50_decoding.py b/tutorials/machine-learning/50_decoding.py index fe2addc87f3..a15cff55695 100644 --- a/tutorials/machine-learning/50_decoding.py +++ b/tutorials/machine-learning/50_decoding.py @@ -82,7 +82,7 @@ epochs.pick(picks="meg", exclude="bads") # remove stim and EOG del raw -X = epochs.get_data() # MEG signals: n_epochs, n_meg_channels, n_times +X = epochs.get_data(copy=False) # MEG signals: n_epochs, n_meg_channels, n_times y = epochs.events[:, 2] # target: auditory left vs visual left # %% diff --git a/tutorials/stats-sensor-space/20_erp_stats.py b/tutorials/stats-sensor-space/20_erp_stats.py index 08b5a583ffc..cba1d3bbf0f 100644 --- a/tutorials/stats-sensor-space/20_erp_stats.py +++ b/tutorials/stats-sensor-space/20_erp_stats.py @@ -90,8 +90,8 @@ # In this case, inference is done over items. In the same manner, we could # also conduct the test over, e.g., subjects. X = [ - long_words.get_data().transpose(0, 2, 1), - short_words.get_data().transpose(0, 2, 1), + long_words.get_data(copy=False).transpose(0, 2, 1), + short_words.get_data(copy=False).transpose(0, 2, 1), ] tfce = dict(start=0.4, step=0.4) # ideally start and step would be smaller diff --git a/tutorials/stats-sensor-space/75_cluster_ftest_spatiotemporal.py b/tutorials/stats-sensor-space/75_cluster_ftest_spatiotemporal.py index 7c3e272a933..dda2ab29255 100644 --- a/tutorials/stats-sensor-space/75_cluster_ftest_spatiotemporal.py +++ b/tutorials/stats-sensor-space/75_cluster_ftest_spatiotemporal.py @@ -85,7 +85,7 @@ # Obtain the data as a 3D matrix and transpose it such that # the dimensions are as expected for the cluster permutation test: # n_epochs × n_times × n_channels -X = [epochs[event_name].get_data() for event_name in event_id] +X = [epochs[event_name].get_data(copy=False) for event_name in event_id] X = [np.transpose(x, (0, 2, 1)) for x in X] From 9cbdc7b3eb0ff1e96e79c5cabd3fc7d8a8c27a92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20H=C3=B6chenberger?= Date: Mon, 13 Nov 2023 18:12:40 +0100 Subject: [PATCH 057/405] MRG: Add ICA's `fit_params` to HTML representation (#12194) --- doc/changes/devel.rst | 1 + mne/html_templates/repr/ica.html.jinja | 4 ++++ mne/preprocessing/ica.py | 6 +++++- mne/preprocessing/tests/test_ica.py | 1 + 4 files changed, 11 insertions(+), 1 deletion(-) diff --git a/doc/changes/devel.rst b/doc/changes/devel.rst index f6108f65b30..970d4bde393 100644 --- a/doc/changes/devel.rst +++ b/doc/changes/devel.rst @@ -48,6 +48,7 @@ Enhancements - Add support for passing multiple labels to :func:`mne.minimum_norm.source_induced_power` (:gh:`12026` by `Erica Peterson`_, `Eric Larson`_, and `Daniel McCloy`_ ) - Added documentation to :meth:`mne.io.Raw.set_montage` and :func:`mne.add_reference_channels` to specify that montages should be set after adding reference channels (:gh:`12160` by `Jacob Woessner`_) - Add argument ``splash`` to the function using the ``qt`` browser backend to allow enabling/disabling the splash screen (:gh:`12185` by `Mathieu Scheltienne`_) +- :class:`~mne.preprocessing.ICA`'s HTML representation (displayed in Jupyter notebooks and :class:`mne.Report`) now includes all optional fit parameters (e.g., max. number of iterations) (:gh:`12194`, by `Richard Höchenberger`_) Bugs ~~~~ diff --git a/mne/html_templates/repr/ica.html.jinja b/mne/html_templates/repr/ica.html.jinja index 080c3cd5e95..69bd5803a59 100644 --- a/mne/html_templates/repr/ica.html.jinja +++ b/mne/html_templates/repr/ica.html.jinja @@ -3,6 +3,10 @@ Method {{ method }} + + Fit parameters + {% if fit_params %}{% for key, value in fit_params.items() %}{{ key }}={{ value }}
{% endfor %}{% else %}—{% endif %} + Fit {% if fit_on %}{{ n_iter }} iterations on {{ fit_on }} ({{ n_samples }} samples){% else %}no{% endif %} diff --git a/mne/preprocessing/ica.py b/mne/preprocessing/ica.py index 9d32eabea53..f12c937bd72 100644 --- a/mne/preprocessing/ica.py +++ b/mne/preprocessing/ica.py @@ -15,7 +15,7 @@ from inspect import Parameter, isfunction, signature from numbers import Integral from time import time -from typing import List, Literal, Optional +from typing import Dict, List, Literal, Optional, Union import numpy as np from scipy import linalg, stats @@ -507,6 +507,7 @@ def _get_infos_for_repr(self): class _InfosForRepr: fit_on: Optional[Literal["raw data", "epochs"]] fit_method: Literal["fastica", "infomax", "extended-infomax", "picard"] + fit_params: Dict[str, Union[str, float]] fit_n_iter: Optional[int] fit_n_samples: Optional[int] fit_n_components: Optional[int] @@ -522,6 +523,7 @@ class _InfosForRepr: fit_on = "epochs" fit_method = self.method + fit_params = self.fit_params fit_n_iter = getattr(self, "n_iter_", None) fit_n_samples = getattr(self, "n_samples_", None) fit_n_components = getattr(self, "n_components_", None) @@ -542,6 +544,7 @@ class _InfosForRepr: infos_for_repr = _InfosForRepr( fit_on=fit_on, fit_method=fit_method, + fit_params=fit_params, fit_n_iter=fit_n_iter, fit_n_samples=fit_n_samples, fit_n_components=fit_n_components, @@ -576,6 +579,7 @@ def _repr_html_(self): html = t.render( fit_on=infos.fit_on, method=infos.fit_method, + fit_params=infos.fit_params, n_iter=infos.fit_n_iter, n_samples=infos.fit_n_samples, n_components=infos.fit_n_components, diff --git a/mne/preprocessing/tests/test_ica.py b/mne/preprocessing/tests/test_ica.py index 62aab79a3c2..1625d5d493c 100644 --- a/mne/preprocessing/tests/test_ica.py +++ b/mne/preprocessing/tests/test_ica.py @@ -490,6 +490,7 @@ def test_ica_core(method, n_components, noise_cov, n_pca_components, browser_bac repr_html_ = ica._repr_html_() assert repr_ == f"" assert method in repr_html_ + assert "max_iter=1" in repr_html_ # test fit checker with pytest.raises(RuntimeError, match="No fit available"): From 26a0cdcfc0448ce7617f6ada8e0c324b2ceada0e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 13 Nov 2023 15:23:57 -0500 Subject: [PATCH 058/405] [pre-commit.ci] pre-commit autoupdate (#12203) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c41418ed4b4..ea9e3c852e0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,13 +1,13 @@ repos: - repo: https://github.com/psf/black - rev: 23.10.1 + rev: 23.11.0 hooks: - id: black args: [--quiet] # Ruff mne - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.4 + rev: v0.1.5 hooks: - id: ruff name: ruff mne @@ -16,7 +16,7 @@ repos: # Ruff tutorials and examples - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.4 + rev: v0.1.5 hooks: - id: ruff name: ruff tutorials and examples @@ -37,7 +37,7 @@ repos: # yamllint - repo: https://github.com/adrienverge/yamllint.git - rev: v1.32.0 + rev: v1.33.0 hooks: - id: yamllint args: [--strict, -c, .yamllint.yml] From fa0e8cfc42e8a80f6018df02372bb9f709bd993f Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Mon, 13 Nov 2023 16:40:27 -0500 Subject: [PATCH 059/405] MAINT: Update roadmap (#12202) Co-authored-by: Daniel McCloy --- .pre-commit-config.yaml | 3 - doc/_includes/channel_interpolation.rst | 11 +- doc/_includes/inverse.rst | 24 +-- doc/_includes/ssp.rst | 12 +- doc/conf.py | 2 + doc/development/roadmap.rst | 160 +++++++++--------- doc/links.inc | 5 +- tutorials/machine-learning/50_decoding.py | 10 +- .../preprocessing/25_background_filtering.py | 2 +- 9 files changed, 116 insertions(+), 113 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ea9e3c852e0..6891065b92c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -48,6 +48,3 @@ repos: hooks: - id: rstcheck files: ^doc/.*\.(rst|inc)$ - # https://github.com/rstcheck/rstcheck/issues/199 - # https://github.com/rstcheck/rstcheck/issues/200 - exclude: ^doc/(help/faq|install/manual_install|install/mne_c|install/advanced|install/updating|_includes/channel_interpolation|_includes/inverse|_includes/ssp)\.rst$ diff --git a/doc/_includes/channel_interpolation.rst b/doc/_includes/channel_interpolation.rst index da2ae1dc12f..4639604af58 100644 --- a/doc/_includes/channel_interpolation.rst +++ b/doc/_includes/channel_interpolation.rst @@ -22,12 +22,11 @@ In short, data repair using spherical spline interpolation :footcite:`PerrinEtAl Spherical splines assume that the potential :math:`V(\boldsymbol{r_i})` at any point :math:`\boldsymbol{r_i}` on the surface of the sphere can be represented by: .. math:: V(\boldsymbol{r_i}) = c_0 + \sum_{j=1}^{N}c_{i}g_{m}(cos(\boldsymbol{r_i}, \boldsymbol{r_{j}})) - :label: model + :name: model where the :math:`C = (c_{1}, ..., c_{N})^{T}` are constants which must be estimated. The function :math:`g_{m}(\cdot)` of order :math:`m` is given by: .. math:: g_{m}(x) = \frac{1}{4 \pi}\sum_{n=1}^{\infty} \frac{2n + 1}{(n(n + 1))^m}P_{n}(x) - :label: legendre where :math:`P_{n}(x)` are `Legendre polynomials`_ of order :math:`n`. @@ -36,22 +35,22 @@ where :math:`P_{n}(x)` are `Legendre polynomials`_ of order :math:`n`. To estimate the constants :math:`C`, we must solve the following two equations simultaneously: .. math:: G_{ss}C + T_{s}c_0 = X - :label: matrix_form + :name: matrix_form .. math:: {T_s}^{T}C = 0 - :label: constraint + :name: constraint where :math:`G_{ss} \in R^{N \times N}` is a matrix whose entries are :math:`G_{ss}[i, j] = g_{m}(cos(\boldsymbol{r_i}, \boldsymbol{r_j}))` and :math:`X \in R^{N \times 1}` are the potentials :math:`V(\boldsymbol{r_i})` measured at the good channels. :math:`T_{s} = (1, 1, ..., 1)^\top` is a column vector of dimension :math:`N`. Equation :eq:`matrix_form` is the matrix formulation of Equation :eq:`model` and equation :eq:`constraint` is like applying an average reference to the data. From equation :eq:`matrix_form` and :eq:`constraint`, we get: .. math:: \begin{bmatrix} c_0 \\ C \end{bmatrix} = {\begin{bmatrix} {T_s}^{T} && 0 \\ T_s && G_{ss} \end{bmatrix}}^{-1} \begin{bmatrix} 0 \\ X \end{bmatrix} = C_{i}X - :label: estimate_constant + :name: estimate_constant :math:`C_{i}` is the same as matrix :math:`{\begin{bmatrix} {T_s}^{T} && 0 \\ T_s && G_{ss} \end{bmatrix}}^{-1}` but with its first column deleted, therefore giving a matrix of dimension :math:`(N + 1) \times N`. Now, to estimate the potentials :math:`\hat{X} \in R^{M \times 1}` at the bad channels, we have to do: .. math:: \hat{X} = G_{ds}C + T_{d}c_0 - :label: estimate_data + :name: estimate_data where :math:`G_{ds} \in R^{M \times N}` computes :math:`g_{m}(\boldsymbol{r_i}, \boldsymbol{r_j})` between the bad and good channels. :math:`T_{d} = (1, 1, ..., 1)^\top` is a column vector of dimension :math:`M`. Plugging in equation :eq:`estimate_constant` in :eq:`estimate_data`, we get diff --git a/doc/_includes/inverse.rst b/doc/_includes/inverse.rst index cf0eacffecf..6d0d77ed8bb 100644 --- a/doc/_includes/inverse.rst +++ b/doc/_includes/inverse.rst @@ -70,7 +70,7 @@ this by writing :math:`R' = R/ \lambda^2 = R \lambda^{-2}`, which yields the inverse operator .. math:: - :label: inv_m + :name: inv_m M &= R' G^\top (G R' G^\top + C)^{-1} \\ &= R \lambda^{-2} G^\top (G R \lambda^{-2} G^\top + C)^{-1} \\ @@ -106,12 +106,12 @@ The MNE software employs data whitening so that a 'whitened' inverse operator assumes the form .. math:: \tilde{M} = M C^{^1/_2} = R \tilde{G}^\top (\tilde{G} R \tilde{G}^\top + \lambda^2 I)^{-1}\ , - :label: inv_m_tilde + :name: inv_m_tilde where .. math:: \tilde{G} = C^{-^1/_2}G - :label: inv_g_tilde + :name: inv_g_tilde is the spatially whitened gain matrix. We arrive at the whitened inverse operator equation :eq:`inv_m_tilde` by making the substitution for @@ -128,7 +128,7 @@ operator equation :eq:`inv_m_tilde` by making the substitution for The expected current values are .. math:: - :label: inv_j_hat_t + :name: inv_j_hat_t \hat{j}(t) &= Mx(t) \\ &= M C^{^1/_2} C^{-^1/_2} x(t) \\ @@ -137,7 +137,7 @@ The expected current values are knowing :eq:`inv_m_tilde` and taking .. math:: - :label: inv_tilde_x_t + :name: inv_tilde_x_t \tilde{x}(t) = C^{-^1/_2}x(t) @@ -151,7 +151,7 @@ to raw data. To reflect the decrease of noise due to averaging, this matrix, C_0 / L`. .. note:: - When EEG data are included, the gain matrix :math:`G` needs to be average referenced when computing the linear inverse operator :math:`M`. This is incorporated during creating the spatial whitening operator :math:`C^{-^1/_2}`, which includes any projectors on the data. EEG data average reference (using a projector) is mandatory for source modeling and is checked when calculating the inverse operator. + When EEG data are included, the gain matrix :math:`G` needs to be average referenced when computing the linear inverse operator :math:`M`. This is incorporated during creating the spatial whitening operator :math:`C^{-^1/_2}`, which includes any projectors on the data. EEG data average reference (using a projector) is mandatory for source modeling and is checked when calculating the inverse operator. As shown above, regularization of the inverse solution is equivalent to a change in the variance of the current amplitudes in the Bayesian *a priori* @@ -224,7 +224,7 @@ computational convenience we prefer to take another route, which employs the singular-value decomposition (SVD) of the matrix .. math:: - :label: inv_a + :name: inv_a A &= \tilde{G} R^{^1/_2} \\ &= U \Lambda V^\top @@ -238,7 +238,7 @@ Combining the SVD from :eq:`inv_a` with the inverse equation :eq:`inv_m` it is easy to show that .. math:: - :label: inv_m_tilde_svd + :name: inv_m_tilde_svd \tilde{M} &= R \tilde{G}^\top (\tilde{G} R \tilde{G}^\top + \lambda^2 I)^{-1} \\ &= R^{^1/_2} A^\top (A A^\top + \lambda^2 I)^{-1} \\ @@ -253,7 +253,7 @@ where the elements of the diagonal matrix :math:`\Gamma` are simply .. `reginv` in our code: .. math:: - :label: inv_gamma_k + :name: inv_gamma_k \gamma_k = \frac{\lambda_k}{\lambda_k^2 + \lambda^2}\ . @@ -261,7 +261,7 @@ From our expected current equation :eq:`inv_j_hat_t` and our whitened measurement equation :eq:`inv_tilde_x_t`, if we take .. math:: - :label: inv_w_t + :name: inv_w_t w(t) &= U^\top \tilde{x}(t) \\ &= U^\top C^{-^1/_2} x(t)\ , @@ -269,7 +269,7 @@ measurement equation :eq:`inv_tilde_x_t`, if we take we can see that the expression for the expected current is just .. math:: - :label: inv_j_hat_t_svd + :name: inv_j_hat_t_svd \hat{j}(t) &= R^{^1/_2} V \Gamma w(t) \\ &= \sum_k {\bar{v_k} \gamma_k w_k(t)}\ , @@ -314,7 +314,7 @@ normalization factors, it's convenient to reuse our "weighted eigenleads" definition from equation :eq:`inv_j_hat_t` in matrix form as .. math:: - :label: inv_eigenleads_weighted + :name: inv_eigenleads_weighted \bar{V} = R^{^1/_2} V\ . diff --git a/doc/_includes/ssp.rst b/doc/_includes/ssp.rst index f28c91ddd5c..1bc860d15db 100644 --- a/doc/_includes/ssp.rst +++ b/doc/_includes/ssp.rst @@ -30,13 +30,13 @@ Without loss of generality we can always decompose any :math:`n`-channel measurement :math:`b(t)` into its signal and noise components as .. math:: b(t) = b_s(t) + b_n(t) - :label: additive_model + :name: additive_model Further, if we know that :math:`b_n(t)` is well characterized by a few field patterns :math:`b_1 \dotso b_m`, we can express the disturbance as .. math:: b_n(t) = Uc_n(t) + e(t)\ , - :label: pca + :name: pca where the columns of :math:`U` constitute an orthonormal basis for :math:`b_1 \dotso b_m`, :math:`c_n(t)` is an :math:`m`-component column vector, and the @@ -48,12 +48,12 @@ such that the conditions described above are satisfied. We can now construct the orthogonal complement operator .. math:: P_{\perp} = I - UU^\top - :label: projector + :name: projector and apply it to :math:`b(t)` in Equation :eq:`additive_model` yielding .. math:: b_{s}(t) \approx P_{\perp}b(t)\ , - :label: result + :name: result since :math:`P_{\perp}b_n(t) = P_{\perp}(Uc_n(t) + e(t)) \approx 0` and :math:`P_{\perp}b_{s}(t) \approx b_{s}(t)`. The projection operator @@ -102,12 +102,12 @@ typical in EEG analysis to subtract the average reference from all the sensor signals :math:`b^{1}(t), ..., b^{n}(t)`. That is: .. math:: {b}^{j}_{s}(t) = b^{j}(t) - \frac{1}{n}\sum_{k}{b^k(t)} - :label: eeg_proj + :name: eeg_proj where the noise term :math:`b_{n}^{j}(t)` is given by .. math:: b_{n}^{j}(t) = \frac{1}{n}\sum_{k}{b^k(t)} - :label: noise_term + :name: noise_term Thus, the projector vector :math:`P_{\perp}` will be given by :math:`P_{\perp}=\frac{1}{n}[1, 1, ..., 1]` diff --git a/doc/conf.py b/doc/conf.py index df835e4a088..c1afdedb4f2 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -1324,6 +1324,8 @@ def reset_warnings(gallery_conf, fname): # nilearn "pkg_resources is deprecated as an API", r"The .* was deprecated in Matplotlib 3\.7", + # scipy + r"scipy.signal.morlet2 is deprecated in SciPy 1\.12", ): warnings.filterwarnings( # deal with other modules having bad imports "ignore", message=".*%s.*" % key, category=DeprecationWarning diff --git a/doc/development/roadmap.rst b/doc/development/roadmap.rst index 03a66bd6f51..ced61c7e4a1 100644 --- a/doc/development/roadmap.rst +++ b/doc/development/roadmap.rst @@ -16,55 +16,60 @@ Clustering statistics API ^^^^^^^^^^^^^^^^^^^^^^^^^ The current clustering statistics code has limited functionality. It should be re-worked to create a new ``cluster_based_statistic`` or similar function. -In particular, the new API should: - -1. Support mixed within- and between-subjects designs, different statistical - functions, etc. This should be done via a ``design`` argument that mirrors - :func:`patsy.dmatrices` or similar community standard (e.g., this is what - is used by :class:`statsmodels.regression.linear_model.OLS`). -2. Have clear tutorials showing how different contrasts can be done (toy data). -3. Have clear tutorials showing some common analyses on real data (time-freq, - sensor space, source space, etc.) -4. Not introduce any significant speed penalty (e.g., < 10% slower) compared - to the existing, more specialized/limited functions. + +The new API will likely be along the lines of:: + + cluster_stat(obs, design, *, alpha=0.05, cluster_alpha=0.05, ...) + +with: + +``obs`` : :class:`pandas.DataFrame` + Has columns like "subject", "condition", and "data". + The "data" column holds things like :class:`mne.Evoked`, + :class:`mne.SourceEstimate`, :class:`mne.time_frequency.Spectrum`, etc. +``design`` : `str` + Likely Wilkinson notation to mirror :func:`patsy.dmatrices` (e.g., this is + is used by :class:`statsmodels.regression.linear_model.OLS`). Getting from the + string to the design matrix could be done via Patsy or more likely + `Formulaic `__. + +This generic API will support mixed within- and between-subjects designs, +different statistical functions/tests, etc. This should be achievable without +introducing any significant speed penalty (e.g., < 10% slower) compared to the existing +more specialized/limited functions, since most computation cost is in clustering rather +than statistical testing. + +The clustering function will return a user-friendly ``ClusterStat`` object or similar +that retains information about dimensionality, significance, etc. and facilitates +plotting and interpretation of results. + +Clear tutorials will be needed to: + +1. Show how different contrasts can be done (toy data). +2. Show some common analyses on real data (time-freq, sensor space, source space, etc.) + +Regression tests will be written to ensure equivalent outputs when compared to FieldTrip +for cases that FieldTrip also supports. More details are in :gh:`4859`. -Access to open EEG/MEG databases -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -We should improve the access to open EEG/MEG databases via the -:mod:`mne.datasets` module, in other words improve our dataset fetchers. -We have physionet, but much more. Having a consistent API to access multiple -data sources would be great. See :gh:`2852` and :gh:`3585` for some ideas, -as well as: - -- `OpenNEURO `__ - "A free and open platform for sharing MRI, MEG, EEG, iEEG, and ECoG data." - See for example :gh:`6687`. -- `Human Connectome Project Datasets `__ - Over a 3-year span (2012-2015), the Human Connectome Project (HCP) scanned - 1,200 healthy adult subjects. The available data includes MR structural - scans, behavioral data and (on a subset of the data) resting state and/or - task MEG data. -- `MMN dataset `__ - Used for tutorial/publications applying DCM for ERP analysis using SPM. -- Kymata datasets - Current and archived EMEG measurement data, used to test hypotheses in the - Kymata atlas. The participants are healthy human adults listening to the - radio and/or watching films, and the data is comprised of (averaged) EEG - and MEG sensor data and source current reconstructions. -- `BNCI Horizon `__ - BCI datasets. +Modernization of realtime processing +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +LSL has become the de facto standard for streaming data from EEG/MEG systems. +We should deprecate `MNE-Realtime`_ in favor of the newly minted `MNE-LSL`_. +We should then fully support MNE-LSL using modern coding best practices such as CI +integration. + +Core components of commonly used real-time processing pipelines should be implemented in +MNE-LSL, including but not limited to realtime IIR filtering, artifact rejection, +montage and reference setting, and online averaging. Integration with standard +MNE-Python plotting routines (evoked joint plots, topomaps, etc.) should be +supported with continuous updating. In progress ----------- -Eye-tracking support -^^^^^^^^^^^^^^^^^^^^ -We had a GSoC student funded to improve support for eye-tracking data, see -`the GSoC proposal `__ -for details. - Diversity, Equity, and Inclusion (DEI) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ MNE-Python is committed to recruiting and retaining a diverse pool of @@ -72,26 +77,17 @@ contributors, see :gh:`8221`. First-class OPM support ^^^^^^^^^^^^^^^^^^^^^^^ -MNE-Python has support for reading some OPM data formats such as FIF, but -support is still rudimentary. Support should be added for other manufacturers, -and standard (and/or novel) preprocessing routines should be added to deal with -coregistration adjustment, forward modeling, and OPM-specific artifacts. +MNE-Python has support for reading some OPM data formats such as FIF and FIL/QuSpin. +Support should be added for other manufacturers, and standard preprocessing routines +should be added to deal with coregistration adjustment and OPM-specific artifacts. +See for example :gh:`11275`, :gh:`11276`, :gh:`11579`, :gh:`12179`. Deep source modeling ^^^^^^^^^^^^^^^^^^^^ Existing source modeling and inverse routines are not explicitly designed to deal with deep sources. Advanced algorithms exist from MGH for enhancing deep source localization, and these should be implemented and vetted in -MNE-Python. - -Better sEEG/ECoG/DBS support -^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Some support already exists for iEEG electrodes in MNE-Python thanks in part -to standard abstractions. However, iEEG-specific pipeline steps (e.g., -electrode localization) and visualizations (e.g., per-shaft topo plots, -:ref:`time-frequency-viz`) are missing. MNE-Python should work with members of -the ECoG/sEEG community to work with or build in existing tools, and extend -native functionality for depth electrodes. +MNE-Python. See :gh:`6784`. Time-frequency classes ^^^^^^^^^^^^^^^^^^^^^^ @@ -107,25 +103,6 @@ See related issues :gh:`6290`, :gh:`7671`, :gh:`8026`, :gh:`8724`, :gh:`9045`, and PRs :gh:`6609`, :gh:`6629`, :gh:`6672`, :gh:`6673`, :gh:`8397`, and :gh:`8892`. -Pediatric and clinical MEG pipelines -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -MNE-Python is in the process of providing automated analysis of BIDS-compliant -datasets, see `MNE-BIDS-Pipeline`_. By incorporating functionality from the -`mnefun `__ pipeline, -which has been used extensively for pediatric data analysis at `I-LABS`_, -better support for pediatric and clinical data processing can be achieved. -Multiple processing steps (e.g., eSSS), sanity checks (e.g., cHPI quality), -and reporting (e.g., SSP joint plots, SNR plots) will be implemented. - -Statistics efficiency -^^^^^^^^^^^^^^^^^^^^^ -A key technique in functional neuroimaging analysis is clustering brain -activity in adjacent regions prior to statistical analysis. An important -clustering algorithm — threshold-free cluster enhancement (TFCE) — currently -relies on computationally expensive permutations for hypothesis testing. -A faster, probabilistic version of TFCE (pTFCE) is available, and we are in the -process of implementing this new algorithm. - 3D visualization ^^^^^^^^^^^^^^^^ Historically we have used Mayavi for 3D visualization, but have faced @@ -155,12 +132,38 @@ Our documentation has many minor issues, which can be found under the tag Completed --------- +Improved sEEG/ECoG/DBS support +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +iEEG-specific pipeline steps such as electrode localization and visualizations +are now available in `MNE-gui-addons`_. + +Access to open EEG/MEG databases +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Open EEG/MEG databases are now more easily accessible via standardized tools such as +`openneuro-py`_. + +Eye-tracking support +^^^^^^^^^^^^^^^^^^^^ +We had a GSoC student funded to improve support for eye-tracking data, see +`the GSoC proposal `__ +for details. An EyeLink data reader and analysis/plotting functions are now available. + +Pediatric and clinical MEG pipelines +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +MNE-Python provides automated analysis of BIDS-compliant datasets via +`MNE-BIDS-Pipeline`_. Functionality from the +`mnefun `__ pipeline, +which has been used extensively for pediatric data analysis at `I-LABS`_, +now provides better support for pediatric and clinical data processing. +Multiple processing steps (e.g., eSSS), sanity checks (e.g., cHPI quality), +and reporting (e.g., SSP joint plots, SNR plots) have been added. + Integrate OpenMEEG via improved Python bindings ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -`OpenMEEG `__ is a state-of-the art solver for +`OpenMEEG`_ is a state-of-the art solver for forward modeling in the field of brain imaging with MEG/EEG. It solves numerically partial differential equations (PDE). It is written in C++ with -Python bindings written in `SWIG `__. +Python bindings written in SWIG. The ambition of the project is to integrate OpenMEEG into MNE offering to MNE the ability to solve more forward problems (cortical mapping, intracranial recordings, etc.). Tasks that have been completed: @@ -170,8 +173,7 @@ recordings, etc.). Tasks that have been completed: - Understand how MNE encodes info about sensors (location, orientation, integration points etc.) and allow OpenMEEG to be used. - Modernize CI systems (e.g., using ``cibuildwheel``). - -See `OpenMEEG`_ for details. +- Automated deployment on PyPI and conda-forge. .. _time-frequency-viz: diff --git a/doc/links.inc b/doc/links.inc index 388144d3ddf..170d91b553a 100644 --- a/doc/links.inc +++ b/doc/links.inc @@ -19,12 +19,15 @@ .. _`MNE-BIDS-Pipeline`: https://mne.tools/mne-bids-pipeline .. _`MNE-HCP`: http://mne.tools/mne-hcp .. _`MNE-Realtime`: https://mne.tools/mne-realtime +.. _`MNE-LSL`: https://mne.tools/mne-realtime +.. _`MNE-gui-addons`: https://mne.tools/mne-gui-addons .. _`MNE-MATLAB`: https://github.com/mne-tools/mne-matlab .. _`MNE-Docker`: https://github.com/mne-tools/mne-docker .. _`MNE-ICAlabel`: https://github.com/mne-tools/mne-icalabel .. _`MNE-Connectivity`: https://github.com/mne-tools/mne-connectivity .. _`MNE-NIRS`: https://github.com/mne-tools/mne-nirs -.. _OpenMEEG: http://openmeeg.github.io +.. _OpenMEEG: https://openmeeg.github.io +.. _openneuro-py: https://pypi.org/project/openneuro-py .. _EOSS2: https://chanzuckerberg.com/eoss/proposals/improving-usability-of-core-neuroscience-analysis-tools-with-mne-python .. _EOSS4: https://chanzuckerberg.com/eoss/proposals/building-pediatric-and-clinical-data-pipelines-for-mne-python/ .. _`code of conduct`: https://github.com/mne-tools/.github/blob/main/CODE_OF_CONDUCT.md diff --git a/tutorials/machine-learning/50_decoding.py b/tutorials/machine-learning/50_decoding.py index a15cff55695..c99ad50cb83 100644 --- a/tutorials/machine-learning/50_decoding.py +++ b/tutorials/machine-learning/50_decoding.py @@ -179,7 +179,7 @@ # in the original sensor space to CSP space using the following transformation: # # .. math:: x_{CSP}(t) = W^{T}x(t) -# :label: csp +# :name: csp # # where each column of :math:`W \in R^{C\times C}` is a spatial filter and each # row of :math:`x_{CSP}` is a CSP component. The matrix :math:`W` is also @@ -190,15 +190,15 @@ # covariance matrices # # .. math:: W^{T}\Sigma^{+}W = \lambda^{+} -# :label: diagonalize_p +# :name: diagonalize_p # .. math:: W^{T}\Sigma^{-}W = \lambda^{-} -# :label: diagonalize_n +# :name: diagonalize_n # # where :math:`\lambda^{C}` is a diagonal matrix whose entries are the # eigenvalues of the following generalized eigenvalue problem # # .. math:: \Sigma^{+}w = \lambda \Sigma^{-}w -# :label: eigen_problem +# :name: eigen_problem # # Large entries in the diagonal matrix corresponds to a spatial filter which # gives high variance in one class but low variance in the other. Thus, the @@ -274,7 +274,7 @@ # rewrite Equation :eq:`csp` as follows: # # .. math:: x(t) = (W^{-1})^{T}x_{CSP}(t) -# :label: patterns +# :name: patterns # # The columns of the matrix :math:`(W^{-1})^T` are called spatial patterns. # This is also called the mixing matrix. The example :ref:`ex-linear-patterns` diff --git a/tutorials/preprocessing/25_background_filtering.py b/tutorials/preprocessing/25_background_filtering.py index 0f60e8f1eb3..aac46559850 100644 --- a/tutorials/preprocessing/25_background_filtering.py +++ b/tutorials/preprocessing/25_background_filtering.py @@ -61,7 +61,7 @@ :math:`y(n)` in terms of our input data :math:`x(n)` as: .. math:: - :label: summations + :name: summations y(n) &= b_0 x(n) + \ldots + b_M x(n-M) - a_1 y(n-1) - \ldots - a_N y(n - N)\\ From a91f582874a29b32b558d8a55d60c2b5e22b0812 Mon Sep 17 00:00:00 2001 From: Jacob Woessner Date: Tue, 14 Nov 2023 08:29:35 -0600 Subject: [PATCH 060/405] [WIP] [BUG] Display title in plot_compare_evokeds when axes='topo' (#12192) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Eric Larson --- doc/changes/devel.rst | 1 + mne/viz/evoked.py | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/doc/changes/devel.rst b/doc/changes/devel.rst index 970d4bde393..c375a62e7a9 100644 --- a/doc/changes/devel.rst +++ b/doc/changes/devel.rst @@ -91,6 +91,7 @@ Bugs - Improve handling of ``method`` argument in the channel interpolation function to support :class:`str` and raise helpful error messages (:gh:`12113` by `Mathieu Scheltienne`_) - Fix combination of ``DIN`` event channels into a single synthetic trigger channel ``STI 014`` by the MFF reader of :func:`mne.io.read_raw_egi` (:gh:`12122` by `Mathieu Scheltienne`_) - Fix bug with :func:`mne.io.read_raw_eeglab` and :func:`mne.read_epochs_eeglab` where automatic fiducial detection would fail for certain files (:gh:`12165` by `Clemens Brunner`_) +- Fix bug with :func:`mne.viz.plot_compare_evokeds` where the title was not displayed when ``axes='topo'`` (:gh:`12192` by `Jacob Woessner`_) API changes ~~~~~~~~~~~ diff --git a/mne/viz/evoked.py b/mne/viz/evoked.py index 1c6712a6bec..8e42212e11b 100644 --- a/mne/viz/evoked.py +++ b/mne/viz/evoked.py @@ -2929,7 +2929,7 @@ def plot_compare_evokeds( title = _title_helper_pce( title, picked_types, picks=orig_picks, ch_names=ch_names, combine=combine ) - + topo_disp_title = False # setup axes if do_topo: show_sensors = False @@ -2939,6 +2939,8 @@ def plot_compare_evokeds( "sensors. This can be extremely slow. Consider using " "mne.viz.plot_topo, which is optimized for speed." ) + topo_title = title + topo_disp_title = True axes = ["topo"] * len(ch_types) else: if axes is None: @@ -3217,5 +3219,7 @@ def click_func( if cmap is not None: _draw_colorbar_pce(ax, _colors, _cmap, colorbar_title, colorbar_ticks) # finish + if topo_disp_title: + ax.figure.suptitle(topo_title) plt_show(show) return [ax.figure] From a9a94d92b2040f1455d36403bd10822cf6770c8f Mon Sep 17 00:00:00 2001 From: Mathieu Scheltienne Date: Tue, 14 Nov 2023 15:38:37 +0100 Subject: [PATCH 061/405] Fix concatenation of raws with np.nan in the dev-head-transform (#12198) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Daniel McCloy Co-authored-by: Eric Larson --- doc/changes/devel.rst | 1 + mne/_fiff/meas_info.py | 7 +++++-- mne/io/tests/test_raw.py | 14 ++++++++++++-- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/doc/changes/devel.rst b/doc/changes/devel.rst index c375a62e7a9..ec950180cd6 100644 --- a/doc/changes/devel.rst +++ b/doc/changes/devel.rst @@ -91,6 +91,7 @@ Bugs - Improve handling of ``method`` argument in the channel interpolation function to support :class:`str` and raise helpful error messages (:gh:`12113` by `Mathieu Scheltienne`_) - Fix combination of ``DIN`` event channels into a single synthetic trigger channel ``STI 014`` by the MFF reader of :func:`mne.io.read_raw_egi` (:gh:`12122` by `Mathieu Scheltienne`_) - Fix bug with :func:`mne.io.read_raw_eeglab` and :func:`mne.read_epochs_eeglab` where automatic fiducial detection would fail for certain files (:gh:`12165` by `Clemens Brunner`_) +- Fix concatenation of ``raws`` with ``np.nan`` in the device to head transformation (:gh:`12198` by `Mathieu Scheltienne`_) - Fix bug with :func:`mne.viz.plot_compare_evokeds` where the title was not displayed when ``axes='topo'`` (:gh:`12192` by `Jacob Woessner`_) API changes diff --git a/mne/_fiff/meas_info.py b/mne/_fiff/meas_info.py index 5d67b77470c..8c2395be480 100644 --- a/mne/_fiff/meas_info.py +++ b/mne/_fiff/meas_info.py @@ -3726,10 +3726,13 @@ def _ensure_infos_match(info1, info2, name, *, on_mismatch="raise"): raise ValueError(f"SSP projectors in {name} must be the same") if any(not _proj_equal(p1, p2) for p1, p2 in zip(info2["projs"], info1["projs"])): raise ValueError(f"SSP projectors in {name} must be the same") - if (info1["dev_head_t"] is None) != (info2["dev_head_t"] is None) or ( + if (info1["dev_head_t"] is None) ^ (info2["dev_head_t"] is None) or ( info1["dev_head_t"] is not None and not np.allclose( - info1["dev_head_t"]["trans"], info2["dev_head_t"]["trans"], rtol=1e-6 + info1["dev_head_t"]["trans"], + info2["dev_head_t"]["trans"], + rtol=1e-6, + equal_nan=True, ) ): msg = ( diff --git a/mne/io/tests/test_raw.py b/mne/io/tests/test_raw.py index 5cc017588e3..c292de4c6c0 100644 --- a/mne/io/tests/test_raw.py +++ b/mne/io/tests/test_raw.py @@ -29,10 +29,10 @@ 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.datasets import testing 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 from mne.utils import ( _import_h5io_funcs, _raw_annot, @@ -603,7 +603,6 @@ def _test_concat(reader, *args): assert_allclose(data, raw1[:, :][0]) -@testing.requires_testing_data def test_time_as_index(): """Test indexing of raw times.""" raw = read_raw_fif(raw_fname) @@ -1011,3 +1010,14 @@ def test_resamp_noop(): data_before = raw.get_data() data_after = raw.resample(sfreq=raw.info["sfreq"]).get_data() assert_array_equal(data_before, data_after) + + +def test_concatenate_raw_dev_head_t(): + """Test concatenating raws with dev-head-t including nans.""" + data = np.random.randn(3, 10) + info = create_info(3, 1000.0, ["mag", "grad", "grad"]) + raw = RawArray(data, info) + raw.info["dev_head_t"] = Transform("meg", "head", np.eye(4)) + raw.info["dev_head_t"]["trans"][0, 0] = np.nan + raw2 = raw.copy() + concatenate_raws([raw, raw2]) From 16f4411162b9d4a90fcecb2559c45d1191b09fb5 Mon Sep 17 00:00:00 2001 From: Judy D Zhu <38392787+JD-Zhu@users.noreply.github.com> Date: Wed, 15 Nov 2023 01:58:53 +1100 Subject: [PATCH 062/405] Add KIT phantom dataset (#12105) Co-authored-by: Eric Larson --- .circleci/config.yml | 7 + .pre-commit-config.yaml | 2 +- doc/api/datasets.rst | 3 +- doc/changes/devel.rst | 1 + doc/documentation/datasets.rst | 12 ++ doc/references.bib | 13 ++ mne/datasets/__init__.pyi | 2 + mne/datasets/config.py | 8 + mne/datasets/phantom_kit/__init__.py | 3 + mne/datasets/phantom_kit/phantom_kit.py | 28 ++++ mne/datasets/utils.py | 2 +- mne/dipole.py | 69 ++++++++- mne/event.py | 22 +-- mne/tests/test_dipole.py | 25 +++- mne/transforms.py | 27 +--- mne/utils/config.py | 8 +- tools/circleci_download.sh | 3 + tutorials/inverse/95_phantom_KIT.py | 186 ++++++++++++++++++++++++ 18 files changed, 371 insertions(+), 50 deletions(-) create mode 100644 mne/datasets/phantom_kit/__init__.py create mode 100644 mne/datasets/phantom_kit/phantom_kit.py create mode 100644 tutorials/inverse/95_phantom_KIT.py diff --git a/.circleci/config.yml b/.circleci/config.yml index 492454e057a..cead5bbaaab 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -266,6 +266,9 @@ jobs: - restore_cache: keys: - data-cache-ucl-opm-auditory + - restore_cache: + keys: + - data-cache-phantom-kit - run: name: Get data # This limit could be increased, but this is helpful for finding slow ones @@ -431,6 +434,10 @@ jobs: key: data-cache-ucl-opm-auditory paths: - ~/mne_data/auditory_OPM_stationary # (4 G) + - save_cache: + key: data-cache-phantom-kit + paths: + - ~/mne_data/MNE-phantom-KIT-data # (1 G) linkcheck: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6891065b92c..3755c67222b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -22,7 +22,7 @@ repos: name: ruff tutorials and examples # D103: missing docstring in public function # D400: docstring first line must end with period - args: ["--ignore=D103,D400"] + args: ["--ignore=D103,D400", "--fix"] files: ^tutorials/|^examples/ # Codespell diff --git a/doc/api/datasets.rst b/doc/api/datasets.rst index 5f5e3762044..2b2c92c8654 100644 --- a/doc/api/datasets.rst +++ b/doc/api/datasets.rst @@ -40,10 +40,11 @@ Datasets spm_face.data_path ucl_opm_auditory.data_path visual_92_categories.data_path + phantom_kit.data_path phantom_4dbti.data_path phantom_kernel.data_path refmeg_noise.data_path ssvep.data_path erp_core.data_path epilepsy_ecog.data_path - eyelink.data_path \ No newline at end of file + eyelink.data_path diff --git a/doc/changes/devel.rst b/doc/changes/devel.rst index ec950180cd6..61a9b3c3cbc 100644 --- a/doc/changes/devel.rst +++ b/doc/changes/devel.rst @@ -44,6 +44,7 @@ Enhancements - By default MNE-Python creates matplotlib figures with ``layout='constrained'`` rather than the default ``layout='tight'`` (:gh:`12050`, :gh:`12103` by `Mathieu Scheltienne`_ and `Eric Larson`_) - Enhance :func:`~mne.viz.plot_evoked_field` with a GUI that has controls for time, colormap, and contour lines (:gh:`11942` by `Marijn van Vliet`_) - Add :class:`mne.viz.ui_events.UIEvent` linking for interactive colorbars, allowing users to link figures and change the colormap and limits interactively. This supports :func:`~mne.viz.plot_evoked_topomap`, :func:`~mne.viz.plot_ica_components`, :func:`~mne.viz.plot_tfr_topomap`, :func:`~mne.viz.plot_projs_topomap`, :meth:`~mne.Evoked.plot_image`, and :meth:`~mne.Epochs.plot_image` (:gh:`12057` by `Santeri Ruuskanen`_) +- Add example KIT phantom dataset in :func:`mne.datasets.phantom_kit.data_path` and :ref:`tut-phantom-kit` (:gh:`12105` by `Judy D Zhu`_ and `Eric Larson`_) - :func:`~mne.epochs.make_metadata` now accepts ``tmin=None`` and ``tmax=None``, which will bound the time window used for metadata generation by event names (instead of a fixed time). That way, you can now for example generate metadata spanning from one cue or fixation cross to the next, even if trial durations vary throughout the recording (:gh:`12118` by `Richard Höchenberger`_) - Add support for passing multiple labels to :func:`mne.minimum_norm.source_induced_power` (:gh:`12026` by `Erica Peterson`_, `Eric Larson`_, and `Daniel McCloy`_ ) - Added documentation to :meth:`mne.io.Raw.set_montage` and :func:`mne.add_reference_channels` to specify that montages should be set after adding reference channels (:gh:`12160` by `Jacob Woessner`_) diff --git a/doc/documentation/datasets.rst b/doc/documentation/datasets.rst index ef63a3b139e..063d06da363 100644 --- a/doc/documentation/datasets.rst +++ b/doc/documentation/datasets.rst @@ -282,6 +282,18 @@ are richly annotated, and can be used for e.g. multiple regression estimation of EEG correlates of printed word processing. +KIT phantom dataset +============================= +:func:`mne.datasets.phantom_kit.data_path`. + +This dataset was obtained with a phantom on a KIT system at +Macquarie University in Sydney, Australia. + +.. topic:: Examples + + * :ref:`tut-phantom-KIT` + + 4D Neuroimaging / BTi dataset ============================= :func:`mne.datasets.phantom_4dbti.data_path`. diff --git a/doc/references.bib b/doc/references.bib index 7d22e7ecbdc..9263379209a 100644 --- a/doc/references.bib +++ b/doc/references.bib @@ -2449,3 +2449,16 @@ @article{TierneyEtAl2022 doi = {10.1016/j.neuroimage.2022.119338}, author = {Tierney, Tim M. and Mellor, Stephanie nd O'Neill, George C. and Timms, Ryan C. and Barnes, Gareth R.}, } + + +@article{OyamaEtAl2015, + title = {Dry phantom for magnetoencephalography —{Configuration}, calibration, and contribution}, + volume = {251}, + issn = {0165-0270}, + doi = {10.1016/j.jneumeth.2015.05.004}, + journal = {Journal of Neuroscience Methods}, + author = {Oyama, Daisuke and Adachi, Yoshiaki and Yumoto, Masato and Hashimoto, Isao and Uehara, Gen}, + month = aug, + year = {2015}, + pages = {24--36}, +} diff --git a/mne/datasets/__init__.pyi b/mne/datasets/__init__.pyi index 8d23c4c9c97..22cb6acce7b 100644 --- a/mne/datasets/__init__.pyi +++ b/mne/datasets/__init__.pyi @@ -24,6 +24,7 @@ __all__ = [ "opm", "phantom_4dbti", "phantom_kernel", + "phantom_kit", "refmeg_noise", "sample", "sleep_physionet", @@ -52,6 +53,7 @@ from . import ( opm, phantom_4dbti, phantom_kernel, + phantom_kit, refmeg_noise, sample, sleep_physionet, diff --git a/mne/datasets/config.py b/mne/datasets/config.py index a61f96af06f..35ff714b8ee 100644 --- a/mne/datasets/config.py +++ b/mne/datasets/config.py @@ -174,6 +174,14 @@ config_key="MNE_DATASETS_OPM_PATH", ) +MNE_DATASETS["phantom_kit"] = dict( + archive_name="MNE-phantom-KIT-24bit.zip", + hash="md5:CAF82EE978DD473C7DE6C1034D9CCD45", + url="https://osf.io/download/svnt3/", + folder_name="MNE-phantom-KIT-data", + config_key="MNE_DATASETS_PHANTOM_KIT_PATH", +) + MNE_DATASETS["phantom_4dbti"] = dict( archive_name="MNE-phantom-4DBTi.zip", hash="md5:938a601440f3ffa780d20a17bae039ff", diff --git a/mne/datasets/phantom_kit/__init__.py b/mne/datasets/phantom_kit/__init__.py new file mode 100644 index 00000000000..6efd7be9e15 --- /dev/null +++ b/mne/datasets/phantom_kit/__init__.py @@ -0,0 +1,3 @@ +"""KIT phantom dataset.""" + +from .phantom_kit import data_path, get_version diff --git a/mne/datasets/phantom_kit/phantom_kit.py b/mne/datasets/phantom_kit/phantom_kit.py new file mode 100644 index 00000000000..e6d3275ccef --- /dev/null +++ b/mne/datasets/phantom_kit/phantom_kit.py @@ -0,0 +1,28 @@ +from ...utils import verbose +from ..utils import _data_path_doc, _download_mne_dataset, _get_version, _version_doc + + +@verbose +def data_path( + path=None, force_update=False, update_path=True, download=True, *, verbose=None +): # noqa: D103 + return _download_mne_dataset( + name="phantom_kit", + processor="unzip", + path=path, + force_update=force_update, + update_path=update_path, + download=download, + ) + + +data_path.__doc__ = _data_path_doc.format( + name="phantom_kit", conf="MNE_DATASETS_PHANTOM_KIT_PATH" +) + + +def get_version(): # noqa: D103 + return _get_version("phantom_kit") + + +get_version.__doc__ = _version_doc.format(name="phantom_kit") diff --git a/mne/datasets/utils.py b/mne/datasets/utils.py index 5ee5539ce1b..568436a2898 100644 --- a/mne/datasets/utils.py +++ b/mne/datasets/utils.py @@ -324,7 +324,7 @@ def _download_all_example_data(verbose=True): paths = dict() for kind in ( "sample testing misc spm_face somato hf_sef multimodal " - "fnirs_motor opm mtrf fieldtrip_cmc kiloword phantom_4dbti " + "fnirs_motor opm mtrf fieldtrip_cmc kiloword phantom_kit phantom_4dbti " "refmeg_noise ssvep epilepsy_ecog ucl_opm_auditory eyelink " "erp_core brainstorm.bst_raw brainstorm.bst_auditory " "brainstorm.bst_resting brainstorm.bst_phantom_ctf " diff --git a/mne/dipole.py b/mne/dipole.py index 810955011a2..31663d31e94 100644 --- a/mne/dipole.py +++ b/mne/dipole.py @@ -1760,6 +1760,36 @@ def fit_dipole( return dipoles, residual +# Every other row of Table 3 from OyamaEtAl2015 +_OYAMA = """ +0.00 56.29 -27.50 +32.50 56.29 5.00 +0.00 65.00 5.00 +-32.50 56.29 5.00 +0.00 56.29 37.50 +0.00 32.50 61.29 +-56.29 0.00 -27.50 +-56.29 32.50 5.00 +-65.00 0.00 5.00 +-56.29 -32.50 5.00 +-56.29 0.00 37.50 +-32.50 0.00 61.29 +0.00 -56.29 -27.50 +-32.50 -56.29 5.00 +0.00 -65.00 5.00 +32.50 -56.29 5.00 +0.00 -56.29 37.50 +0.00 -32.50 61.29 +56.29 0.00 -27.50 +56.29 -32.50 5.00 +65.00 0.00 5.00 +56.29 32.50 5.00 +56.29 0.00 37.50 +32.50 0.00 61.29 +0.00 0.00 70.00 +""" + + def get_phantom_dipoles(kind="vectorview"): """Get standard phantom dipole locations and orientations. @@ -1772,6 +1802,11 @@ def get_phantom_dipoles(kind="vectorview"): The Neuromag VectorView phantom. ``otaniemi`` The older Neuromag phantom used at Otaniemi. + ``oyama`` + The phantom from :footcite:`OyamaEtAl2015`. + + .. versionchanged:: 1.6 + Support added for ``'oyama'``. Returns ------- @@ -1788,8 +1823,13 @@ def get_phantom_dipoles(kind="vectorview"): ----- The Elekta phantoms have a radius of 79.5mm, and HPI coil locations in the XY-plane at the axis extrema (e.g., (79.5, 0), (0, -79.5), ...). + + References + ---------- + .. footbibliography:: """ - _check_option("kind", kind, ["vectorview", "otaniemi"]) + _validate_type(kind, str, "kind") + _check_option("kind", kind, ["vectorview", "otaniemi", "oyama"]) if kind == "vectorview": # these values were pulled from a scanned image provided by # Elekta folks @@ -1811,18 +1851,43 @@ def get_phantom_dipoles(kind="vectorview"): y = np.concatenate((c, c, -a, -b, c, c, b, a)) z = np.concatenate((b, a, b, a, b, a, a, b)) signs = [-1] * 8 + [1] * 16 + [-1] * 8 + else: + assert kind == "oyama" + xyz = np.fromstring(_OYAMA.strip().replace("\n", " "), sep=" ").reshape(25, 3) + xyz = np.repeat(xyz, 2, axis=0) + x, y, z = xyz.T + signs = [1] * 50 pos = np.vstack((x, y, z)).T / 1000.0 + # For Neuromag-style phantoms, # Locs are always in XZ or YZ, and so are the oris. The oris are # also in the same plane and tangential, so it's easy to determine # the orientation. + # For Oyama, vectors are orthogonal to the position vector and oriented with one + # pointed toward the north pole (except for the topmost points, which are just xy). ori = list() for pi, this_pos in enumerate(pos): this_ori = np.zeros(3) idx = np.where(this_pos == 0)[0] # assert len(idx) == 1 + if len(idx) == 0: # oyama + idx = [np.argmin(this_pos)] idx = np.setdiff1d(np.arange(3), idx[0]) this_ori[idx] = (this_pos[idx][::-1] / np.linalg.norm(this_pos[idx])) * [1, -1] - this_ori *= signs[pi] + if kind == "oyama": + # Ensure it's orthogonal to the position vector + pos_unit = this_pos / np.linalg.norm(this_pos) + this_ori -= pos_unit * np.dot(this_ori, pos_unit) + this_ori /= np.linalg.norm(this_ori) + # This was empirically determined by looking at the dipole fits + if np.abs(this_ori[2]) >= 1e-6: # if it's not in the XY plane + this_ori *= -1 * np.sign(this_ori[2]) # point downward + elif np.abs(this_ori[0]) < 1e-6: # in the XY plane (at the north pole) + this_ori *= -1 * np.sign(this_ori[1]) # point backward + # Odd ones create a RH coordinate system with their ori + if pi % 2: + this_ori = np.cross(pos_unit, this_ori) + else: + this_ori *= signs[pi] # Now we have this quality, which we could uncomment to # double-check: # np.testing.assert_allclose(np.dot(this_ori, this_pos) / diff --git a/mne/event.py b/mne/event.py index 17e45707844..9b4e3a3f725 100644 --- a/mne/event.py +++ b/mne/event.py @@ -25,6 +25,7 @@ _check_option, _get_stim_channel, _on_missing, + _pl, _validate_type, check_fname, fill_doc, @@ -482,6 +483,7 @@ def find_stim_steps(raw, pad_start=None, pad_stop=None, merge=0, stim_channel=No def _find_events( data, first_samp, + *, verbose=None, output="onset", consecutive="increasing", @@ -490,6 +492,7 @@ def _find_events( uint_cast=False, mask_type="and", initial_event=False, + ch_name=None, ): """Help find events.""" assert data.shape[0] == 1 # data should be only a row vector @@ -520,9 +523,9 @@ def _find_events( events = np.insert(events, 0, [first_samp, 0, initial_value], axis=0) else: logger.info( - "Trigger channel has a non-zero initial value of {} " - "(consider using initial_event=True to detect this " - "event)".format(initial_value) + f"Trigger channel {ch_name} has a non-zero initial value of " + "{initial_value} (consider using initial_event=True to detect this " + "event)" ) events = _mask_trigs(events, mask, mask_type) @@ -555,22 +558,22 @@ def _find_events( logger.info("Removing orphaned onset at the end of the file.") onset_idx = np.delete(onset_idx, -1) + _check_option("output", output, ("onset", "step", "offset")) if output == "onset": events = events[onset_idx] elif output == "step": idx = np.union1d(onset_idx, offset_idx) events = events[idx] - elif output == "offset": + else: + assert output == "offset" event_id = events[onset_idx, 2] events = events[offset_idx] events[:, 1] = events[:, 2] events[:, 2] = event_id events[:, 0] -= 1 - else: - raise ValueError("Invalid output parameter %r" % output) - logger.info("%s events found" % len(events)) - logger.info("Event IDs: %s" % np.unique(events[:, 2])) + logger.info(f"{len(events)} event{_pl(events)} found on stim channel {ch_name}") + logger.info(f"Event IDs: {np.unique(events[:, 2])}") return events @@ -772,7 +775,7 @@ def find_events( data, _ = raw[picks, :] events_list = [] - for d in data: + for d, ch_name in zip(data, stim_channel): events = _find_events( d[np.newaxis, :], raw.first_samp, @@ -784,6 +787,7 @@ def find_events( uint_cast=uint_cast, mask_type=mask_type, initial_event=initial_event, + ch_name=ch_name, ) # add safety check for spurious events (for ex. from neuromag syst.) by # checking the number of low sample events diff --git a/mne/tests/test_dipole.py b/mne/tests/test_dipole.py index b26cfec936f..f4a1215a128 100644 --- a/mne/tests/test_dipole.py +++ b/mne/tests/test_dipole.py @@ -471,14 +471,25 @@ def _check_roundtrip_fixed(dip, tmp_path): assert_allclose(ch_1[key], ch_2[key], err_msg=key) -def test_get_phantom_dipoles(): +@pytest.mark.parametrize( + "kind, count", + [ + ("vectorview", 32), + ("otaniemi", 32), + ("oyama", 50), + ], +) +def test_get_phantom_dipoles(kind, count): """Test getting phantom dipole locations.""" - pytest.raises(ValueError, get_phantom_dipoles, 0) - pytest.raises(ValueError, get_phantom_dipoles, "foo") - for kind in ("vectorview", "otaniemi"): - pos, ori = get_phantom_dipoles(kind) - assert pos.shape == (32, 3) - assert ori.shape == (32, 3) + with pytest.raises(TypeError, match="must be an instance of"): + get_phantom_dipoles(0) + with pytest.raises(ValueError, match="Invalid value for"): + get_phantom_dipoles("foo") + pos, ori = get_phantom_dipoles(kind) + assert pos.shape == (count, 3) + assert ori.shape == (count, 3) + # pos should be orthogonal to ori for all dipoles + assert_allclose(np.sum(pos * ori, axis=1), 0.0, atol=1e-7) @testing.requires_testing_data diff --git a/mne/transforms.py b/mne/transforms.py index c25ca958cf9..88b10a3e11f 100644 --- a/mne/transforms.py +++ b/mne/transforms.py @@ -291,31 +291,8 @@ def rotation(x=0, y=0, z=0): r : array, shape = (4, 4) The rotation matrix. """ - cos_x = np.cos(x) - cos_y = np.cos(y) - cos_z = np.cos(z) - sin_x = np.sin(x) - sin_y = np.sin(y) - sin_z = np.sin(z) - r = np.array( - [ - [ - cos_y * cos_z, - -cos_x * sin_z + sin_x * sin_y * cos_z, - sin_x * sin_z + cos_x * sin_y * cos_z, - 0, - ], - [ - cos_y * sin_z, - cos_x * cos_z + sin_x * sin_y * sin_z, - -sin_x * cos_z + cos_x * sin_y * sin_z, - 0, - ], - [-sin_y, sin_x * cos_y, cos_x * cos_y, 0], - [0, 0, 0, 1], - ], - dtype=float, - ) + r = np.eye(4) + r[:3, :3] = rotation3d(x=x, y=y, z=z) return r diff --git a/mne/utils/config.py b/mne/utils/config.py index 453cdb5c084..0ac71d7d4a5 100644 --- a/mne/utils/config.py +++ b/mne/utils/config.py @@ -163,6 +163,7 @@ def set_memmap_min_size(memmap_min_size): "MNE_DATASETS_VISUAL_92_CATEGORIES_PATH": "str, path for visual_92_categories data", "MNE_DATASETS_KILOWORD_PATH": "str, path for kiloword data", "MNE_DATASETS_FIELDTRIP_CMC_PATH": "str, path for fieldtrip_cmc data", + "MNE_DATASETS_PHANTOM_KIT_PATH": "str, path for phantom_kit data", "MNE_DATASETS_PHANTOM_4DBTI_PATH": "str, path for phantom_4dbti data", "MNE_DATASETS_PHANTOM_KERNEL_PATH": "str, path for phantom_kernel data", "MNE_DATASETS_LIMO_PATH": "str, path for limo data", @@ -496,7 +497,7 @@ def _get_stim_channel(stim_channel, info, raise_error=True): Returns ------- - stim_channel : str | list of str + stim_channel : list of str The name of the stim channel(s) to use """ from .._fiff.pick import pick_types @@ -525,13 +526,12 @@ def _get_stim_channel(stim_channel, info, raise_error=True): return ["STI 014"] stim_channel = pick_types(info, meg=False, ref_meg=False, stim=True) - if len(stim_channel) > 0: - stim_channel = [info["ch_names"][ch_] for ch_ in stim_channel] - elif raise_error: + if len(stim_channel) == 0 and raise_error: raise ValueError( "No stim channels found. Consider specifying them " "manually using the 'stim_channel' parameter." ) + stim_channel = [info["ch_names"][ch_] for ch_ in stim_channel] return stim_channel diff --git a/tools/circleci_download.sh b/tools/circleci_download.sh index d2dff2eb499..2088587b1ad 100755 --- a/tools/circleci_download.sh +++ b/tools/circleci_download.sh @@ -113,6 +113,9 @@ else if [[ $(cat $FNAME | grep -x ".*datasets.*ucl_opm_auditory.*" | wc -l) -gt 0 ]]; then python -c "import mne; print(mne.datasets.ucl_opm_auditory.data_path(update_path=True))"; fi; + if [[ $(cat $FNAME | grep -x ".*datasets.*phantom_kit.*" | wc -l) -gt 0 ]]; then + python -c "import mne; print(mne.datasets.phantom_kit.data_path(update_path=True))"; + fi; fi; done; echo PATTERN="$PATTERN"; diff --git a/tutorials/inverse/95_phantom_KIT.py b/tutorials/inverse/95_phantom_KIT.py new file mode 100644 index 00000000000..af259b9ad79 --- /dev/null +++ b/tutorials/inverse/95_phantom_KIT.py @@ -0,0 +1,186 @@ +""" +.. _tut-phantom-kit: + +============================ +KIT phantom dataset tutorial +============================ + +Here we read KIT data obtained from a phantom with 49 dipoles sequentially activated +with 2-cycle 11 Hz sinusoidal bursts to verify source localization accuracy. +""" + +# Authors: Eric Larson +# +# License: BSD-3-Clause + +# %% +import matplotlib.pyplot as plt +import numpy as np +from scipy.signal import find_peaks + +import mne + +data_path = mne.datasets.phantom_kit.data_path() +actual_pos, actual_ori = mne.dipole.get_phantom_dipoles("oyama") +actual_pos, actual_ori = actual_pos[:49], actual_ori[:49] # only 49 of 50 dipoles + +raw = mne.io.read_raw_kit(data_path / "002_phantom_11Hz_100uA.con") +# cut from ~800 to ~300s for speed, and also at convenient dip stim boundaries +# chosen by examining MISC 017 by eye. +raw.crop(11.5, 302.9).load_data() +raw.filter(None, 40) # 11 Hz stimulation, no need to keep higher freqs +plot_scalings = dict(mag=5e-12) # large-amplitude sinusoids +raw_plot_kwargs = dict(duration=15, n_channels=50, scalings=plot_scalings) +raw.plot(**raw_plot_kwargs) + +# %% +# We can also look at the power spectral density to see the phantom oscillations at +# 11 Hz plus the expected frequency-domain sinc-like oscillations due to the time-domain +# boxcar windowing of the 11 Hz sinusoid. + +spectrum = raw.copy().crop(0, 60).compute_psd(n_fft=10000) +fig = spectrum.plot() +fig.axes[0].set_xlim(0, 50) +dip_freq = 11.0 +fig.axes[0].axvline(dip_freq, color="r", ls="--", lw=2, zorder=4) + +# %% +# To find the events, we can look at the MISC channel that recorded the activations. +# Here we use a very simple thresholding approach to find the events. +# The MISC 017 channel holds the dipole activations, which are 2-cycle 11 Hz sinusoidal +# bursts with the initial sinusoidal deflection downward, so we do a little bit of +# signal manipulation to help :func:`~scipy.signal.find_peaks`. + +# Figure out events +dip_act, dip_t = raw["MISC 017"] +dip_act = dip_act[0] # 2D to 1D array +dip_act -= dip_act.mean() # remove DC offset +dip_act *= -1 # invert so first deflection is positive +thresh = np.percentile(dip_act, 90) +min_dist = raw.info["sfreq"] / dip_freq * 0.9 # 90% of period, to be safe +peaks = find_peaks(dip_act, height=thresh, distance=min_dist)[0] +assert len(peaks) % 2 == 0 # 2-cycle modulations +peaks = peaks[::2] # take only first peaks of each 2-cycle burst + +fig, ax = plt.subplots(layout="constrained", figsize=(12, 4)) +stop = int(15 * raw.info["sfreq"]) # 15 sec +ax.plot(dip_t[:stop], dip_act[:stop], color="k", lw=1) +ax.axhline(thresh, color="r", ls="--", lw=1) +peak_idx = peaks[peaks < stop] +ax.plot(dip_t[peak_idx], dip_act[peak_idx], "ro", zorder=5, ms=5) +ax.set(xlabel="Time (s)", ylabel="Dipole activation (AU)\n(MISC 017 adjusted)") +ax.set(xlim=dip_t[[0, stop - 1]]) + +# We know that there are 32 dipoles, so mark the first ones as well +n_dip = 49 +assert len(peaks) % n_dip == 0 # we found them all (hopefully) +ax.plot(dip_t[peak_idx[::n_dip]], dip_act[peak_idx[::n_dip]], "bo", zorder=4, ms=10) + +# Knowing we've caught the top of the first cycle of a 11 Hz sinusoid, plot onsets +# with red X's. +onsets = peaks - np.round(raw.info["sfreq"] / dip_freq / 4.0).astype( + int +) # shift to start +onset_idx = onsets[onsets < stop] +ax.plot(dip_t[onset_idx], dip_act[onset_idx], "rx", zorder=5, ms=5) + +# %% +# Given the onsets are now stored in ``peaks``, we can create our events array and plot +# on our raw data. + +n_rep = len(peaks) // n_dip +events = np.zeros((len(peaks), 3), int) +events[:, 0] = onsets + raw.first_samp +events[:, 2] = np.tile(np.arange(1, n_dip + 1), n_rep) +raw.plot(events=events, **raw_plot_kwargs) + +# %% +# Now we can figure out our epoching parameters and epoch the data, sanity checking +# some values along the way knowing how the stimulation was done. + +# Sanity check and determine epoching params +deltas = np.diff(events[:, 0], axis=0) +group_deltas = deltas[n_dip - 1 :: n_dip] / raw.info["sfreq"] # gap between 49 and 1 +assert (group_deltas > 0.8).all() +assert (group_deltas < 0.9).all() +others = np.delete(deltas, np.arange(n_dip - 1, len(deltas), n_dip)) # remove 49->1 +others = others / raw.info["sfreq"] +assert (others > 0.25).all() +assert (others < 0.3).all() +tmax = 1 / dip_freq * 2.0 # 2 cycles +tmin = tmax - others.min() +assert tmin < 0 +epochs = mne.Epochs( + raw, + events, + tmin=tmin, + tmax=tmax, + baseline=(None, 0), + decim=10, + picks="data", + preload=True, +) +del raw +epochs.plot(scalings=plot_scalings) + +# %% +# Now we can average the epochs for each dipole, get the activation at the peak time, +# and create an :class:`mne.EvokedArray` from the result. + +t_peak = 1.0 / dip_freq / 4.0 +data = np.zeros((len(epochs.ch_names), n_dip)) +for di in range(n_dip): + data[:, [di]] = epochs[str(di + 1)].average().crop(t_peak, t_peak).data +evoked = mne.EvokedArray(data, epochs.info, tmin=0, comment="KIT phantom activations") +evoked.plot_joint() + +# %% +# Let's fit dipoles at each dipole's peak activation time. + +trans = mne.transforms.Transform("head", "mri", np.eye(4)) +sphere = mne.make_sphere_model(r0=(0.0, 0.0, 0.0), head_radius=0.08) +cov = mne.compute_covariance(epochs, tmax=0, method="empirical") +# We need to correct the ``dev_head_t`` because it's incorrect for these data! +# relative to the helmet: hleft, forward, up +translation = mne.transforms.translation(x=0.01, y=-0.015, z=-0.088) +# pitch down (rot about x/R), roll left (rot about y/A), yaw left (rot about z/S) +rotation = mne.transforms.rotation( + x=np.deg2rad(5), + y=np.deg2rad(-1), + z=np.deg2rad(-3), +) +evoked.info["dev_head_t"]["trans"][:] = translation @ rotation +dip, residual = mne.fit_dipole(evoked, cov, sphere, n_jobs=None) + +# %% +# Finally let's look at the results. + +# sphinx_gallery_thumbnail_number = 7 + +print(f"Average amplitude: {np.mean(dip.amplitude) * 1e9:0.1f} nAm") +print(f"Average GOF: {np.mean(dip.gof):0.1f}%") +diffs = 1000 * np.sqrt(np.sum((dip.pos - actual_pos) ** 2, axis=-1)) +print(f"Average loc error: {np.mean(diffs):0.1f} mm") +angles = np.rad2deg(np.arccos(np.abs(np.sum(dip.ori * actual_ori, axis=1)))) +print(f"Average ori error: {np.mean(angles):0.1f}°") + +fig = mne.viz.plot_alignment( + evoked.info, + trans, + bem=sphere, + coord_frame="head", + meg="helmet", + show_axes=True, +) +fig = mne.viz.plot_dipole_locations( + dipoles=dip, mode="arrow", color=(0.2, 1.0, 0.5), fig=fig +) + +actual_amp = np.ones(len(dip)) # misc amp to create Dipole instance +actual_gof = np.ones(len(dip)) # misc GOF to create Dipole instance +dip_true = mne.Dipole(dip.times, actual_pos, actual_amp, actual_ori, actual_gof) +fig = mne.viz.plot_dipole_locations( + dipoles=dip_true, mode="arrow", color=(0.0, 0.0, 0.0), fig=fig +) + +mne.viz.set_3d_view(figure=fig, azimuth=90, elevation=90, distance=0.5) From 22a6dc6b22a6397b75b6caa2271079ed14931986 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20H=C3=B6chenberger?= Date: Tue, 14 Nov 2023 16:31:08 +0100 Subject: [PATCH 063/405] Move requirements to `pyproject.toml` (#12178) Co-authored-by: Eric Larson --- .circleci/config.yml | 4 +- .github/workflows/tests.yml | 12 +- MANIFEST.in | 6 - Makefile | 6 +- azure-pipelines.yml | 13 +- doc/changes/v0.24.rst | 2 +- doc/development/contributing.rst | 14 ++- doc/links.inc | 1 - mne/epochs.py | 2 + mne/viz/_brain/tests/test_brain.py | 20 +-- pyproject.toml | 174 +++++++++++++++++++++++++-- requirements.txt | 47 -------- requirements_base.txt | 12 -- requirements_doc.txt | 22 ---- requirements_hdf5.txt | 3 - requirements_testing.txt | 15 --- requirements_testing_extra.txt | 10 -- setup.py | 57 --------- tools/azure_dependencies.sh | 9 +- tools/circleci_dependencies.sh | 4 +- tools/generate_codemeta.py | 27 ++--- tools/github_actions_dependencies.sh | 25 ++-- tools/github_actions_env_vars.sh | 6 +- tutorials/io/20_reading_eeg_data.py | 2 +- 24 files changed, 231 insertions(+), 262 deletions(-) delete mode 100644 requirements.txt delete mode 100644 requirements_base.txt delete mode 100644 requirements_doc.txt delete mode 100644 requirements_hdf5.txt delete mode 100644 requirements_testing.txt delete mode 100644 requirements_testing_extra.txt delete mode 100644 setup.py diff --git a/.circleci/config.yml b/.circleci/config.yml index cead5bbaaab..bd94268c1ad 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -43,8 +43,8 @@ jobs: brew install python@3.11 which python which pip - pip install --upgrade pip setuptools wheel - pip install --upgrade --only-binary "numpy,scipy,dipy,statsmodels" -ve . -r requirements.txt -r requirements_testing.txt -r requirements_testing_extra.txt PyQt6 + pip install --upgrade pip + pip install --upgrade --only-binary "numpy,scipy,dipy,statsmodels" -ve .[full,test_extra] # 3D too slow on Apple's software renderer, and numba causes us problems pip uninstall -y vtk pyvista pyvistaqt numba mkdir -p test-results diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 6d45a299fdc..09555ac5eb9 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -59,9 +59,6 @@ jobs: - os: ubuntu-latest python: '3.10' kind: conda - - os: ubuntu-latest - python: '3.10' - kind: notebook - os: ubuntu-latest python: '3.11' kind: pip-pre @@ -87,7 +84,6 @@ jobs: with: qt: true pyvista: false - if: matrix.kind != 'notebook' # Python (if pip) - uses: actions/setup-python@v4 with: @@ -103,19 +99,13 @@ jobs: miniforge-variant: Mambaforge use-mamba: ${{ matrix.kind != 'conda' }} if: ${{ !startswith(matrix.kind, 'pip') }} - - name: 'Install OSMesa VTK variant' - run: | - # TODO: As of 2023/02/28, notebook tests need a pinned mesalib - mamba install -c conda-forge "vtk>=9.2=*osmesa*" "vtk-base>=9.2=*osmesa*" "mesalib=23.1.4" "numpy=1.24.4" "numba=0.57.1" - mamba list - if: matrix.kind == 'notebook' - run: ./tools/github_actions_dependencies.sh # Minimal commands on Linux (macOS stalls) - run: ./tools/get_minimal_commands.sh if: ${{ startswith(matrix.os, 'ubuntu') }} - run: ./tools/github_actions_install.sh - run: ./tools/github_actions_infos.sh - # Check Qt on non-notebook + # Check Qt - run: ./tools/check_qt_import.sh $MNE_QT_BACKEND if: ${{ env.MNE_QT_BACKEND != '' }} - name: Run tests with no testing data diff --git a/MANIFEST.in b/MANIFEST.in index 9e12649b5fe..5a06c9c814b 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,12 +1,6 @@ include *.rst include LICENSE.txt include SECURITY.md -include requirements.txt -include requirements_base.txt -include requirements_hdf5.txt -include requirements_testing.txt -include requirements_testing_extra.txt -include requirements_doc.txt include mne/__init__.py recursive-include examples *.py diff --git a/Makefile b/Makefile index b0b61e8370c..2843e0193b5 100644 --- a/Makefile +++ b/Makefile @@ -24,8 +24,8 @@ clean-cache: clean: clean-build clean-pyc clean-so clean-ctags clean-cache -wheel_quiet: - $(PYTHON) setup.py -q sdist bdist_wheel +wheel: + $(PYTHON) -m build sample_data: @python -c "import mne; mne.datasets.sample.data_path(verbose=True);" @@ -57,7 +57,7 @@ codespell: # running manually check-manifest: check-manifest -q --ignore .circleci/config.yml,doc,logo,mne/io/*/tests/data*,mne/io/tests/data,mne/preprocessing/tests/data,.DS_Store,mne/_version.py -check-readme: clean wheel_quiet +check-readme: clean wheel twine check dist/* nesting: diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 1b8ddc505a4..5cee5568623 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -51,8 +51,8 @@ stages: displayName: 'Get Python' - bash: | set -eo pipefail - python -m pip install --progress-bar off --upgrade pip setuptools wheel - python -m pip install --progress-bar off -r requirements_base.txt -r requirements_hdf5.txt -r requirements_testing.txt + python -m pip install --progress-bar off --upgrade pip build + python -m pip install --progress-bar off -ve .[hdf5,test] python -m pip uninstall -yq pytest-qt # don't want to set up display, etc. for this pre-commit install --install-hooks displayName: Install dependencies @@ -107,8 +107,8 @@ stages: displayName: 'Get Python' - bash: | set -e - python -m pip install --progress-bar off --upgrade pip setuptools wheel - python -m pip install --progress-bar off "mne-qt-browser[opengl] @ git+https://github.com/mne-tools/mne-qt-browser.git@main" pyvista scikit-learn pytest-error-for-skips python-picard "PyQt6!=6.5.1" qtpy nibabel + python -m pip install --progress-bar off --upgrade pip + python -m pip install --progress-bar off "mne-qt-browser[opengl] @ git+https://github.com/mne-tools/mne-qt-browser.git@main" pyvista scikit-learn pytest-error-for-skips python-picard "PyQt6!=6.5.1" qtpy nibabel sphinx-gallery python -m pip uninstall -yq mne python -m pip install --progress-bar off --upgrade -e .[test] displayName: 'Install dependencies with pip' @@ -166,11 +166,10 @@ stages: displayName: 'Get Python' - bash: | set -e - python -m pip install --progress-bar off --upgrade pip setuptools wheel + python -m pip install --progress-bar off --upgrade pip python -m pip install --progress-bar off --upgrade --pre --only-binary=\"numpy,scipy,matplotlib,vtk\" numpy scipy matplotlib vtk python -c "import vtk" - python -m pip install --progress-bar off --upgrade -r requirements.txt -r requirements_testing.txt -r requirements_testing_extra.txt - python -m pip install -e . + python -m pip install --progress-bar off --upgrade -ve .[full,test_extra] displayName: 'Install dependencies with pip' - bash: | set -e diff --git a/doc/changes/v0.24.rst b/doc/changes/v0.24.rst index ba3c76ee7f4..425fd5d5759 100644 --- a/doc/changes/v0.24.rst +++ b/doc/changes/v0.24.rst @@ -191,7 +191,7 @@ Enhancements - All functions for reading and writing files should now automatically handle ``~`` (the tilde character) and expand it to the user's home directory. Should you come across any function that doesn't do it, please do let us know! (:gh:`9613`, :gh:`9845` by `Richard Höchenberger`_) -- All functions accepting a FreeSurfer subjects directory via a ``subjects_dir`` parameter can now consume :py:class:`pathlib.Path` objects too (used to be only strings) (:gh:`9613` by `Richard Höchenberger`_) +- All functions accepting a FreeSurfer subjects directory via a ``subjects_dir`` parameter can now consume :class:`pathlib.Path` objects too (used to be only strings) (:gh:`9613` by `Richard Höchenberger`_) - Add support for colormap normalization in :meth:`mne.time_frequency.AverageTFR.plot` (:gh:`9851` by `Clemens Brunner`_) diff --git a/doc/development/contributing.rst b/doc/development/contributing.rst index 2dbf90d306b..4a9e7f52d0e 100644 --- a/doc/development/contributing.rst +++ b/doc/development/contributing.rst @@ -304,11 +304,11 @@ be reflected the next time you open a Python interpreter and ``import mne`` Finally, we'll add a few dependencies that are not needed for running MNE-Python, but are needed for locally running our test suite:: - $ pip install -r requirements_testing.txt + $ pip install -e .[test] And for building our documentation:: - $ pip install -r requirements_doc.txt + $ pip install -e .[doc] $ conda install graphviz .. note:: @@ -329,9 +329,15 @@ To build documentation, you will also require `optipng`_: - On Windows, unzip :file:`optipng.exe` from the `optipng for Windows`_ archive into the :file:`doc/` folder. This step is optional for Windows users. -You can also choose to install some optional linters for reStructuredText:: +There are additional optional dependencies needed to run various tests, such as +scikit-learn for decoding tests, or nibabel for MRI tests. If you want to run all the +tests, consider using our MNE installers (which provide these dependencies) or pay +attention to the skips that ``pytest`` reports and install the relevant libraries. +For example, this traceback:: - $ conda install -c conda-forge sphinx-autobuild doc8 + SKIPPED [2] mne/io/eyelink/tests/test_eyelink.py:14: could not import 'pandas': No module named 'pandas' + +indicates that ``pandas`` needs to be installed in order to run the Eyelink tests. .. _basic-git: diff --git a/doc/links.inc b/doc/links.inc index 170d91b553a..9623c165fe2 100644 --- a/doc/links.inc +++ b/doc/links.inc @@ -114,7 +114,6 @@ .. installation links -.. _requirements file: https://raw.githubusercontent.com/mne-tools/mne-python/main/requirements.txt .. _NVIDIA CUDA GPU processing: https://developer.nvidia.com/cuda-zone .. _NVIDIA proprietary drivers: https://www.geforce.com/drivers diff --git a/mne/epochs.py b/mne/epochs.py index b7afada3d1a..59edcd139ab 100644 --- a/mne/epochs.py +++ b/mne/epochs.py @@ -1902,6 +1902,8 @@ def get_data( .. versionchanged:: 1.7 The default changed from ``False`` to ``True``. + + .. versionadded:: 1.6 %(verbose)s Returns diff --git a/mne/viz/_brain/tests/test_brain.py b/mne/viz/_brain/tests/test_brain.py index 9f7f7c1cac5..1940138d13e 100644 --- a/mne/viz/_brain/tests/test_brain.py +++ b/mne/viz/_brain/tests/test_brain.py @@ -8,7 +8,7 @@ # License: Simplified BSD import os -import sys +import platform from pathlib import Path from shutil import copyfile @@ -662,15 +662,20 @@ def test_single_hemi(hemi, renderer_interactive_pyvistaqt, brain_gc): @testing.requires_testing_data @pytest.mark.slowtest -def test_brain_save_movie(tmp_path, renderer, brain_gc): +@pytest.mark.parametrize("interactive_state", (False, True)) +def test_brain_save_movie(tmp_path, renderer, brain_gc, interactive_state): """Test saving a movie of a Brain instance.""" - from imageio_ffmpeg import count_frames_and_secs + imageio_ffmpeg = pytest.importorskip("imageio_ffmpeg") + # TODO: Figure out why this fails -- some imageio_ffmpeg error + if os.getenv("MNE_CI_KIND", "") == "conda" and platform.system() == "Linux": + pytest.skip("Test broken for unknown reason on conda linux") brain = _create_testing_brain( hemi="lh", time_viewer=False, cortex=["r", "b"] ) # custom binarized filename = tmp_path / "brain_test.mov" - for interactive_state in (False, True): + + try: # for coverage, we set interactivity if interactive_state: brain._renderer.plotter.enable() @@ -688,11 +693,12 @@ def test_brain_save_movie(tmp_path, renderer, brain_gc): filename, time_dilation=1.0, tmin=tmin, tmax=tmax, interpolation="nearest" ) assert filename.is_file() - _, nsecs = count_frames_and_secs(filename) + _, nsecs = imageio_ffmpeg.count_frames_and_secs(filename) assert_allclose(duration, nsecs, atol=0.2) os.remove(filename) - brain.close() + finally: + brain.close() _TINY_SIZE = (350, 300) @@ -1093,7 +1099,7 @@ def test_brain_traces(renderer_interactive_pyvistaqt, hemi, src, tmp_path, brain # TODO: don't skip on Windows, see # https://github.com/mne-tools/mne-python/pull/10935 # for some reason there is a dependency issue with ipympl even using pyvista -@pytest.mark.skipif(sys.platform == "win32", reason="ipympl issue on Windows") +@pytest.mark.skipif(platform.system() == "Windows", reason="ipympl issue on Windows") @testing.requires_testing_data def test_brain_scraper(renderer_interactive_pyvistaqt, brain_gc, tmp_path): """Test a simple scraping example.""" diff --git a/pyproject.toml b/pyproject.toml index a57b18928d4..af5b7e6b5ab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,10 +1,11 @@ [project] name = "mne" description = "MNE-Python project for MEG and EEG data analysis." -maintainers = [ +dynamic = ["version"] +authors = [ { name = "Alexandre Gramfort", email = "alexandre.gramfort@inria.fr" }, ] -dynamic = ["version"] +maintainers = [{ name = "Dan McCloy", email = "dan@mccloy.info" }] license = { text = "BSD-3-Clause" } readme = { file = "README.rst", content-type = "text/x-rst" } requires-python = ">=3.8" @@ -30,9 +31,129 @@ classifiers = [ "Operating System :: MacOS", "Programming Language :: Python :: 3", ] +scripts = { mne = "mne.commands.utils:main" } +dependencies = [ + "numpy>=1.21.2", + "scipy>=1.7.1", + "matplotlib>=3.5.0", + "tqdm", + "pooch>=1.5", + "decorator", + "packaging", + "jinja2", + "importlib_resources>=5.10.2; python_version<'3.9'", + "lazy_loader>=0.3", + "defusedxml", +] + +[project.optional-dependencies] +# Variants with dependencies that will get installed on top of those listed unter +# project.dependencies + +# Leave this one here for backward-compat +data = [] + +# Dependencies for MNE-Python functions that use HDF5 I/O +hdf5 = ["h5io", "pymatreader"] + +# Dependencies for full MNE-Python functionality (other than raw/epochs export) +full = [ + "mne[hdf5]", + "qtpy", + "PyQt6", + "pyobjc-framework-Cocoa>=5.2.0; platform_system=='Darwin'", + "sip", + "scikit-learn", + "nibabel", + "openmeeg>=2.5.5", + "numba", + "h5py", + "pandas", + "numexpr", + "jupyter", + "python-picard", + "statsmodels", + "joblib", + "psutil", + "dipy", + "vtk", + "nilearn", + "xlrd", + "imageio>=2.6.1", + "imageio-ffmpeg>=0.4.1", + "traitlets", + "pyvista>=0.32,!=0.35.2,!=0.38.0,!=0.38.1,!=0.38.2,!=0.38.3,!=0.38.4,!=0.38.5,!=0.38.6,!=0.42.0", + "pyvistaqt>=0.4", + "mffpy>=0.5.7", + "ipywidgets", + "ipympl", + "ipyevents", + "trame", + "trame-vtk", + "trame-vuetify", + "mne-qt-browser", + "darkdetect", + "qdarkstyle", + "threadpoolctl", +] + +# Dependencies for running the test infrastructure +test = [ + "pytest", + "pytest-cov", + "pytest-timeout", + "pytest-harvest", + "pytest-qt", + "ruff", + "numpydoc", + "codespell", + "check-manifest", + "tomli; python_version<'3.11'", + "twine", + "wheel", + "pre-commit", + "black", +] -[project.scripts] -mne = "mne.commands.utils:main" +# Dependencies for being able to run additional tests (rare/CIs/advanced devs) +test_extra = [ + "mne[test]", + "nitime", + "nbclient", + "sphinx-gallery", + "eeglabio", + "EDFlib-Python", + "pybv", + "imageio>=2.6.1", + "imageio-ffmpeg>=0.4.1", + "snirf", +] + +# Dependencies for building the docuemntation +doc = [ + "sphinx>=6", + "numpydoc", + "pydata_sphinx_theme==0.13.3", + "sphinx-gallery @ git+https://github.com/sphinx-gallery/sphinx-gallery@master", + "sphinxcontrib-bibtex>=2.5", + "memory_profiler", + "neo", + "seaborn!=0.11.2", + "sphinx_copybutton", + "sphinx-design", + "sphinxcontrib-youtube", + "mne-bids @ git+https://github.com/mne-tools/mne-bids@main", + "pyxdf", + "mne-connectivity @ git+https://github.com/mne-tools/mne-connectivity@main", + "mne-gui-addons @ git+https://github.com/mne-tools/mne-gui-addons@main", + "pygments>=2.13", + "pytest", + "graphviz", + "pyzmq!=24.0.0", + "ipython!=8.7.0", + "selenium", +] +dev = ["mne[test,doc]"] [project.urls] Homepage = "https://mne.tools/" @@ -46,6 +167,11 @@ Forum = "https://mne.discourse.group/" requires = ["setuptools>=45", "setuptools_scm[toml]>=6.2", "wheel"] build-backend = "setuptools.build_meta" +[tool.setuptools.packages.find] +where = ["."] +include = ["mne*"] +namespaces = false + [tool.setuptools_scm] write_to = "mne/_version.py" version_scheme = "release-branch-semver" @@ -144,13 +270,43 @@ skips = ["*/test_*.py"] # assert statements are good practice with pytest [tool.rstcheck] report_level = "WARNING" ignore_roles = [ - "func", "class", "term", "ref", "doc", "gh", "file", "samp", "meth", "mod", "kbd", - "newcontrib", "footcite", "footcite:t", "eq", "py:mod", "attr", "py:class", "exc", + "attr", + "class", + "doc", + "eq", + "exc", + "file", + "footcite", + "footcite:t", + "func", + "gh", + "kbd", + "meth", + "mod", + "newcontrib", + "py:mod", + "ref", + "samp", + "term", ] ignore_directives = [ - "rst-class", "tab-set", "grid", "toctree", "footbibliography", "autosummary", - "currentmodule", "automodule", "cssclass", "tabularcolumns", "minigallery", - "autoclass", "highlight", "dropdown", "graphviz", "glossary", "autofunction", + "autoclass", + "autofunction", + "automodule", + "autosummary", "bibliography", + "cssclass", + "currentmodule", + "dropdown", + "footbibliography", + "glossary", + "graphviz", + "grid", + "highlight", + "minigallery", + "tabularcolumns", + "toctree", + "rst-class", + "tab-set", ] ignore_messages = "^.*(Unknown target name|Undefined substitution referenced)[^`]*$" diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 90944200247..00000000000 --- a/requirements.txt +++ /dev/null @@ -1,47 +0,0 @@ -# requirements for full MNE-Python functionality (other than raw/epochs export) -numpy>=1.15.4 -scipy>=1.7.1 -matplotlib>=3.5.0 -tqdm -pooch>=1.5 -decorator -h5io -packaging -pymatreader -qtpy -PyQt6 -pyobjc-framework-Cocoa>=5.2.0; platform_system=="Darwin" -sip -scikit-learn -nibabel -openmeeg>=2.5.5 -numba -h5py -jinja2 -pandas -numexpr -jupyter -python-picard -statsmodels -joblib -psutil -dipy -vtk -nilearn -xlrd -imageio>=2.6.1 -imageio-ffmpeg>=0.4.1 -traitlets -pyvista>=0.32,!=0.35.2,!=0.38.0,!=0.38.1,!=0.38.2,!=0.38.3,!=0.38.4,!=0.38.5,!=0.38.6,!=0.42.0 -pyvistaqt>=0.4 -mffpy>=0.5.7 -ipywidgets -ipympl -ipyevents -trame -trame-vtk -trame-vuetify -mne-qt-browser -darkdetect -qdarkstyle -threadpoolctl diff --git a/requirements_base.txt b/requirements_base.txt deleted file mode 100644 index 2e6ba6e6c80..00000000000 --- a/requirements_base.txt +++ /dev/null @@ -1,12 +0,0 @@ -# requirements for basic MNE-Python functionality -numpy>=1.21.2 -scipy>=1.7.1 -matplotlib>=3.5.0 -tqdm -pooch>=1.5 -decorator -packaging -jinja2 -importlib_resources>=5.10.2; python_version<'3.9' -lazy_loader>=0.3 -defusedxml diff --git a/requirements_doc.txt b/requirements_doc.txt deleted file mode 100644 index c0b2bdce0a6..00000000000 --- a/requirements_doc.txt +++ /dev/null @@ -1,22 +0,0 @@ -# requirements for building docs -sphinx>=6 -numpydoc -pydata_sphinx_theme==0.13.3 -git+https://github.com/sphinx-gallery/sphinx-gallery@master -sphinxcontrib-bibtex>=2.5 -memory_profiler -neo -seaborn!=0.11.2 -sphinx_copybutton -sphinx-design -sphinxcontrib-youtube -git+https://github.com/mne-tools/mne-bids@main -pyxdf -git+https://github.com/mne-tools/mne-connectivity.git@main -git+https://github.com/mne-tools/mne-gui-addons.git@main -pygments>=2.13 -pytest -graphviz -pyzmq!=24.0.0 -ipython!=8.7.0 -selenium diff --git a/requirements_hdf5.txt b/requirements_hdf5.txt deleted file mode 100644 index 6d60bf93dab..00000000000 --- a/requirements_hdf5.txt +++ /dev/null @@ -1,3 +0,0 @@ -# requirements for MNE-Python functions that use HDF5 I/O -h5io -pymatreader diff --git a/requirements_testing.txt b/requirements_testing.txt deleted file mode 100644 index a45a91c056b..00000000000 --- a/requirements_testing.txt +++ /dev/null @@ -1,15 +0,0 @@ -# requirements for running tests (on top of environment.yml/requirements.txt) -pytest -pytest-cov -pytest-timeout -pytest-harvest -pytest-qt -ruff -numpydoc -codespell -check-manifest -tomli; python_version<'3.11' -twine -wheel -pre-commit -black diff --git a/requirements_testing_extra.txt b/requirements_testing_extra.txt deleted file mode 100644 index d0318d9c707..00000000000 --- a/requirements_testing_extra.txt +++ /dev/null @@ -1,10 +0,0 @@ -# requirements for full testing (on top of environment.yml/requirements.txt) -nitime -nbclient -sphinx-gallery -eeglabio -EDFlib-Python -pybv -imageio>=2.6.1 -imageio-ffmpeg>=0.4.1 -snirf diff --git a/setup.py b/setup.py deleted file mode 100644 index d3422b58a6a..00000000000 --- a/setup.py +++ /dev/null @@ -1,57 +0,0 @@ -#!/usr/bin/env python - -# Copyright (C) 2011-2020 Alexandre Gramfort -# - -import os -import os.path as op - -from setuptools import setup - - -def parse_requirements_file(fname): - requirements = list() - with open(fname, "r") as fid: - for line in fid: - req = line.strip() - if req.startswith("#"): - continue - # strip end-of-line comments - req = req.split("#", maxsplit=1)[0].strip() - requirements.append(req) - return requirements - - -def package_tree(pkgroot): - """Get the submodule list.""" - # Adapted from VisPy - path = op.dirname(__file__) - subdirs = [ - op.relpath(i[0], path).replace(op.sep, ".") - for i in os.walk(op.join(path, pkgroot)) - if "__init__.py" in i[2] - ] - return sorted(subdirs) - - -if __name__ == "__main__": - if op.exists("MANIFEST"): - os.remove("MANIFEST") - - # data_dependencies is empty, but let's leave them so that we don't break - # people's workflows who did `pip install mne[data]` - install_requires = parse_requirements_file("requirements_base.txt") - data_requires = [] - hdf5_requires = parse_requirements_file("requirements_hdf5.txt") - test_requires = parse_requirements_file( - "requirements_testing.txt" - ) + parse_requirements_file("requirements_testing_extra.txt") - setup( - install_requires=install_requires, - extras_require={ - "data": data_requires, - "hdf5": hdf5_requires, - "test": test_requires, - }, - packages=package_tree("mne"), - ) diff --git a/tools/azure_dependencies.sh b/tools/azure_dependencies.sh index d3ce1a98119..371a61b60e3 100755 --- a/tools/azure_dependencies.sh +++ b/tools/azure_dependencies.sh @@ -1,12 +1,11 @@ #!/bin/bash -ef STD_ARGS="--progress-bar off --upgrade" +python -m pip install $STD_ARGS pip setuptools wheel packaging setuptools_scm if [ "${TEST_MODE}" == "pip" ]; then - python -m pip install --upgrade pip setuptools wheel - python -m pip install --upgrade --only-binary="numba,llvmlite,numpy,scipy,vtk" -r requirements.txt + python -m pip install --only-binary="numba,llvmlite,numpy,scipy,vtk" -e .[test,full] elif [ "${TEST_MODE}" == "pip-pre" ]; then STD_ARGS="$STD_ARGS --pre" - python -m pip install $STD_ARGS pip setuptools wheel packaging setuptools_scm python -m pip install $STD_ARGS --only-binary ":all:" --extra-index-url "https://www.riverbankcomputing.com/pypi/simple" PyQt6 PyQt6-sip PyQt6-Qt6 echo "Numpy etc." # As of 2023/10/25 no pandas (or statsmodels) because they pin to NumPy < 2 @@ -23,7 +22,7 @@ elif [ "${TEST_MODE}" == "pip-pre" ]; then python -m pip install --progress-bar off git+https://github.com/pyvista/pyvista python -m pip install --progress-bar off git+https://github.com/pyvista/pyvistaqt echo "misc" - python -m pip install $STD_ARGS imageio-ffmpeg xlrd mffpy python-picard pillow + python -m pip install $STD_ARGS imageio-ffmpeg xlrd mffpy python-picard pillow traitlets pybv eeglabio echo "nibabel with workaround" python -m pip install --progress-bar off git+https://github.com/nipy/nibabel.git echo "joblib" @@ -31,8 +30,8 @@ elif [ "${TEST_MODE}" == "pip-pre" ]; then echo "EDFlib-Python" python -m pip install $STD_ARGS git+https://gitlab.com/Teuniz/EDFlib-Python@master ./tools/check_qt_import.sh PyQt6 + python -m pip install $STD_ARGS -e .[hdf5,test] else echo "Unknown run type ${TEST_MODE}" exit 1 fi -python -m pip install $EXTRA_ARGS .[test,hdf5] diff --git a/tools/circleci_dependencies.sh b/tools/circleci_dependencies.sh index 677bd5ced22..7e244530a34 100755 --- a/tools/circleci_dependencies.sh +++ b/tools/circleci_dependencies.sh @@ -1,4 +1,4 @@ #!/bin/bash -ef -python -m pip install --upgrade "pip!=20.3.0" setuptools wheel -python -m pip install --upgrade --progress-bar off --only-binary "numpy,scipy,matplotlib,pandas,statsmodels" -r requirements.txt -r requirements_testing.txt -r requirements_doc.txt PyQt6 git+https://github.com/mne-tools/mne-qt-browser -e . +python -m pip install --upgrade "pip!=20.3.0" build +python -m pip install --upgrade --progress-bar off --only-binary "numpy,scipy,matplotlib,pandas,statsmodels" PyQt6 git+https://github.com/mne-tools/mne-qt-browser -ve .[full,test,doc] diff --git a/tools/generate_codemeta.py b/tools/generate_codemeta.py index f53d2ae856a..2525b7aa9d9 100644 --- a/tools/generate_codemeta.py +++ b/tools/generate_codemeta.py @@ -1,7 +1,8 @@ +import subprocess +import tomllib from argparse import ArgumentParser from datetime import date from pathlib import Path -import subprocess parser = ArgumentParser(description="Generate codemeta.json and CITATION.cff") parser.add_argument("release_version", type=str) @@ -13,7 +14,6 @@ # updated. Run this script only at release time. package_name = "MNE-Python" -hard_dependencies = ("numpy", "scipy") release_date = str(date.today()) commit = subprocess.run( ["git", "log", "-1", "--pretty=%H"], capture_output=True, text=True @@ -84,7 +84,10 @@ def parse_name(name): split_version = list(map(int, release_version.split("."))) except ValueError: raise -msg = f"First argument must be the release version X.Y.Z (all integers), got {release_version}" +msg = ( + "First argument must be the release version X.Y.Z (all integers), " + f"got {release_version}" +) assert len(split_version) == 3, msg @@ -108,19 +111,11 @@ def parse_name(name): # GET OUR DEPENDENCY VERSIONS -with open(out_dir / "setup.py", "r") as fid: - for line in fid: - if line.strip().startswith("python_requires="): - version = line.strip().split("=", maxsplit=1)[1].strip("'\",") - dependencies = [f"python{version}"] - break -with open(out_dir / "requirements.txt", "r") as fid: - for line in fid: - req = line.strip() - for hard_dep in hard_dependencies: - if req.startswith(hard_dep): - dependencies.append(req) - +pyproject = tomllib.loads( + (Path(__file__).parents[1] / "pyproject.toml").read_text("utf-8") +) +dependencies = [f"python{pyproject['project']['requires-python']}"] +dependencies.extend(pyproject["project"]["dependencies"]) # these must be done outside the boilerplate (no \n allowed in f-strings): json_authors = ",\n ".join(json_authors) diff --git a/tools/github_actions_dependencies.sh b/tools/github_actions_dependencies.sh index c5f4dd0ea7e..768165635a4 100755 --- a/tools/github_actions_dependencies.sh +++ b/tools/github_actions_dependencies.sh @@ -3,6 +3,7 @@ set -o pipefail STD_ARGS="--progress-bar off --upgrade" +INSTALL_KIND="test_extra,hdf5" if [ ! -z "$CONDA_ENV" ]; then echo "Uninstalling MNE for CONDA_ENV=${CONDA_ENV}" conda remove -c conda-forge --force -yq mne @@ -10,11 +11,14 @@ if [ ! -z "$CONDA_ENV" ]; then elif [ ! -z "$CONDA_DEPENDENCIES" ]; then echo "Using Mamba to install CONDA_DEPENDENCIES=${CONDA_DEPENDENCIES}" mamba install -y $CONDA_DEPENDENCIES + # for compat_minimal and compat_old, we don't want to --upgrade + STD_ARGS="--progress-bar off" + INSTALL_KIND="test" else echo "Install pip-pre dependencies" test "${MNE_CI_KIND}" == "pip-pre" STD_ARGS="$STD_ARGS --pre" - python -m pip install $STD_ARGS pip setuptools wheel packaging + python -m pip install $STD_ARGS pip echo "Numpy" pip uninstall -yq numpy echo "PyQt6" @@ -40,7 +44,7 @@ else echo "pyvistaqt" pip install $STD_ARGS git+https://github.com/pyvista/pyvistaqt echo "imageio-ffmpeg, xlrd, mffpy, python-picard" - pip install $STD_ARGS imageio-ffmpeg xlrd mffpy python-picard patsy + pip install $STD_ARGS imageio-ffmpeg xlrd mffpy python-picard patsy traitlets pybv eeglabio echo "mne-qt-browser" pip install $STD_ARGS git+https://github.com/mne-tools/mne-qt-browser echo "nibabel with workaround" @@ -54,18 +58,5 @@ else fi echo "" - -# for compat_minimal and compat_old, we don't want to --upgrade -if [ ! -z "$CONDA_DEPENDENCIES" ]; then - echo "Installing dependencies for conda" - python -m pip install -r requirements_base.txt -r requirements_testing.txt -else - echo "Installing dependencies using pip" - python -m pip install $STD_ARGS -r requirements_base.txt -r requirements_testing.txt -r requirements_hdf5.txt -fi -echo "" - -if [ "${DEPS}" != "minimal" ]; then - echo "Installing non-minimal dependencies" - python -m pip install $STD_ARGS -r requirements_testing_extra.txt -fi +echo "Installing test dependencies using pip" +python -m pip install $STD_ARGS -e .[$INSTALL_KIND] diff --git a/tools/github_actions_env_vars.sh b/tools/github_actions_env_vars.sh index ba1cac712a5..b291468a58a 100755 --- a/tools/github_actions_env_vars.sh +++ b/tools/github_actions_env_vars.sh @@ -7,13 +7,11 @@ if [[ "$MNE_CI_KIND" == "old" ]]; then 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 "MNE_IGNORE_WARNINGS_IN_TESTS=true" >> $GITHUB_ENV echo "MNE_SKIP_NETWORK_TESTS=1" >> $GITHUB_ENV + echo "MNE_QT_BACKEND=PyQt5" >> $GITHUB_ENV elif [[ "$MNE_CI_KIND" == "minimal" ]]; then echo "Setting conda env vars for minimal" echo "CONDA_DEPENDENCIES=numpy scipy matplotlib" >> $GITHUB_ENV -elif [[ "$MNE_CI_KIND" == "notebook" ]]; then - echo "CONDA_ENV=environment.yml" >> $GITHUB_ENV - # TODO: This should work but breaks stuff... - # echo "MNE_3D_BACKEND=notebook" >> $GITHUB_ENV + echo "MNE_QT_BACKEND=PyQt5" >> $GITHUB_ENV elif [[ "$MNE_CI_KIND" != "pip"* ]]; then # conda, mamba (use warning level for completeness) echo "Setting conda env vars for $MNE_CI_KIND" echo "CONDA_ENV=environment.yml" >> $GITHUB_ENV diff --git a/tutorials/io/20_reading_eeg_data.py b/tutorials/io/20_reading_eeg_data.py index dd373424667..e95794fb35c 100644 --- a/tutorials/io/20_reading_eeg_data.py +++ b/tutorials/io/20_reading_eeg_data.py @@ -39,7 +39,7 @@ `example `_ for instructions. -.. note:: For *writing* BrainVision files, take a look at the :py:mod:`mne.export` +.. note:: For *writing* BrainVision files, take a look at the :mod:`mne.export` module, which used the `pybv `_ Python package. From e2b3c03bf1b55863d5653ff091f3784e9ab34121 Mon Sep 17 00:00:00 2001 From: Kristijan Armeni Date: Tue, 14 Nov 2023 15:31:38 -0500 Subject: [PATCH 064/405] ENH: Add support for reading Neuralynx data (#11969) Co-authored-by: Eric Larson Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- doc/changes/devel.rst | 1 + doc/changes/names.inc | 4 + mne/datasets/config.py | 4 +- mne/io/__init__.pyi | 2 + mne/io/neuralynx/__init__.py | 1 + mne/io/neuralynx/neuralynx.py | 199 +++++++++++++++++++++++ mne/io/neuralynx/tests/__init__.py | 0 mne/io/neuralynx/tests/test_neuralynx.py | 138 ++++++++++++++++ 8 files changed, 347 insertions(+), 2 deletions(-) create mode 100644 mne/io/neuralynx/__init__.py create mode 100644 mne/io/neuralynx/neuralynx.py create mode 100644 mne/io/neuralynx/tests/__init__.py create mode 100644 mne/io/neuralynx/tests/test_neuralynx.py diff --git a/doc/changes/devel.rst b/doc/changes/devel.rst index 61a9b3c3cbc..c586d1e44df 100644 --- a/doc/changes/devel.rst +++ b/doc/changes/devel.rst @@ -23,6 +23,7 @@ Version 1.6.dev0 (development) Enhancements ~~~~~~~~~~~~ +- Add support for Neuralynx data files with ``mne.io.read_raw_neuralynx`` (:gh:`11969` by :newcontrib:`Kristijan Armeni` and :newcontrib:`Ivan Skelin`) - Improve tests for saving splits with :class:`mne.Epochs` (:gh:`11884` by `Dmitrii Altukhov`_) - Added functionality for linking interactive figures together, such that changing one figure will affect another, see :ref:`tut-ui-events` and :mod:`mne.viz.ui_events`. Current figures implementing UI events are :func:`mne.viz.plot_topomap` and :func:`mne.viz.plot_source_estimates` (:gh:`11685` :gh:`11891` by `Marijn van Vliet`_) - HTML anchors for :class:`mne.Report` now reflect the ``section-title`` of the report items rather than using a global incrementor ``global-N`` (:gh:`11890` by `Eric Larson`_) diff --git a/doc/changes/names.inc b/doc/changes/names.inc index 3d441e2289f..da884792c4f 100644 --- a/doc/changes/names.inc +++ b/doc/changes/names.inc @@ -212,6 +212,8 @@ .. _Ilias Machairas: https://github.com/JungleHippo +.. _Ivan Skelin: https://github.com/ivan-skelin + .. _Ivan Zubarev: https://github.com/zubara .. _Ivana Kojcic: https://github.com/ikojcic @@ -294,6 +296,8 @@ .. _Kostiantyn Maksymenko: https://github.com/makkostya +.. _Kristijan Armeni: https://github.com/kristijanarmeni + .. _Kyle Mathewson: https://github.com/kylemath .. _Larry Eisenman: https://github.com/lneisenman diff --git a/mne/datasets/config.py b/mne/datasets/config.py index 35ff714b8ee..88444be352a 100644 --- a/mne/datasets/config.py +++ b/mne/datasets/config.py @@ -87,7 +87,7 @@ # respective repos, and make a new release of the dataset on GitHub. Then # update the checksum in the MNE_DATASETS dict below, and change version # here: ↓↓↓↓↓ ↓↓↓ -RELEASES = dict(testing="0.149", misc="0.26") +RELEASES = dict(testing="0.150", misc="0.26") TESTING_VERSIONED = f'mne-testing-data-{RELEASES["testing"]}' MISC_VERSIONED = f'mne-misc-data-{RELEASES["misc"]}' @@ -111,7 +111,7 @@ # Testing and misc are at the top as they're updated most often MNE_DATASETS["testing"] = dict( archive_name=f"{TESTING_VERSIONED}.tar.gz", - hash="md5:86c47eb83426f48ff17338cb0e379754", + hash="md5:0b7452daef4d19132505b5639d695628", url=( "https://codeload.github.com/mne-tools/mne-testing-data/" f'tar.gz/{RELEASES["testing"]}' diff --git a/mne/io/__init__.pyi b/mne/io/__init__.pyi index 1a0cccf8c54..3843ca3a384 100644 --- a/mne/io/__init__.pyi +++ b/mne/io/__init__.pyi @@ -37,6 +37,7 @@ __all__ = [ "read_raw_hitachi", "read_raw_kit", "read_raw_nedf", + "read_raw_neuralynx", "read_raw_nicolet", "read_raw_nihon", "read_raw_nirx", @@ -79,6 +80,7 @@ from .fil import read_raw_fil from .hitachi import read_raw_hitachi from .kit import read_epochs_kit, read_raw_kit from .nedf import read_raw_nedf +from .neuralynx import read_raw_neuralynx from .nicolet import read_raw_nicolet from .nihon import read_raw_nihon from .nirx import read_raw_nirx diff --git a/mne/io/neuralynx/__init__.py b/mne/io/neuralynx/__init__.py new file mode 100644 index 00000000000..bd9f226064c --- /dev/null +++ b/mne/io/neuralynx/__init__.py @@ -0,0 +1 @@ +from .neuralynx import read_raw_neuralynx diff --git a/mne/io/neuralynx/neuralynx.py b/mne/io/neuralynx/neuralynx.py new file mode 100644 index 00000000000..cab4abdc0ab --- /dev/null +++ b/mne/io/neuralynx/neuralynx.py @@ -0,0 +1,199 @@ +import glob +import os + +import numpy as np + +from ..._fiff.meas_info import create_info +from ..._fiff.utils import _mult_cal_one +from ...utils import _check_fname, _soft_import, fill_doc, logger, verbose +from ..base import BaseRaw + + +@fill_doc +def read_raw_neuralynx( + fname, *, preload=False, exclude_fname_patterns=None, verbose=None +): + """Reader for Neuralynx files. + + Parameters + ---------- + fname : path-like + Path to a folder with Neuralynx .ncs files. + %(preload)s + exclude_fname_patterns : list of str + List of glob-like string patterns to exclude from channel list. + Useful when not all channels have the same number of samples + so you can read separate instances. + %(verbose)s + + Returns + ------- + raw : instance of RawNeuralynx + A Raw object containing Neuralynx data. + See :class:`mne.io.Raw` for documentation of attributes and methods. + + See Also + -------- + mne.io.Raw : Documentation of attributes and methods of RawNeuralynx. + """ + return RawNeuralynx(fname, preload, verbose, exclude_fname_patterns) + + +@fill_doc +class RawNeuralynx(BaseRaw): + """RawNeuralynx class.""" + + @verbose + def __init__( + self, fname, preload=False, verbose=None, exclude_fname_patterns: list = None + ): + _soft_import("neo", "Reading NeuralynxIO files", strict=True) + from neo.io import NeuralynxIO + + fname = _check_fname(fname, "read", True, "fname", need_dir=True) + + logger.info(f"Checking files in {fname}") + + # construct a list of filenames to ignore + exclude_fnames = None + if exclude_fname_patterns: + exclude_fnames = [] + for pattern in exclude_fname_patterns: + fnames = glob.glob(os.path.join(fname, pattern)) + fnames = [os.path.basename(fname) for fname in fnames] + exclude_fnames.extend(fnames) + + logger.info("Ignoring .ncs files:\n" + "\n".join(exclude_fnames)) + + # get basic file info from header, throw Error if NeuralynxIO can't parse + try: + nlx_reader = NeuralynxIO(dirname=fname, exclude_filename=exclude_fnames) + except ValueError as e: + raise ValueError( + "It seems some .ncs channels might have different number of samples. " + + "This is likely due to different sampling rates. " + + "Try excluding them with `exclude_fname_patterns` input arg." + + f"\nOriginal neo.NeuralynxIO.parse_header() ValueError:\n{e}" + ) + + info = create_info( + ch_types="seeg", + ch_names=nlx_reader.header["signal_channels"]["name"].tolist(), + sfreq=nlx_reader.get_signal_sampling_rate(), + ) + + # find total number of samples per .ncs file (`channel`) by summing + # the sample sizes of all segments + n_segments = nlx_reader.header["nb_segment"][0] + block_id = 0 # assumes there's only one block of recording + n_total_samples = sum( + nlx_reader.get_signal_size(block_id, segment) + for segment in range(n_segments) + ) + + # construct an array of shape (n_total_samples,) indicating + # segment membership for each sample + sample2segment = np.concatenate( + [ + np.full(shape=(nlx_reader.get_signal_size(block_id, i),), fill_value=i) + for i in range(n_segments) + ] + ) + + super(RawNeuralynx, self).__init__( + info=info, + last_samps=[n_total_samples - 1], + filenames=[fname], + preload=preload, + raw_extras=[dict(smp2seg=sample2segment, exclude_fnames=exclude_fnames)], + ) + + def _read_segment_file(self, data, idx, fi, start, stop, cals, mult): + """Read a chunk of raw data.""" + from neo.io import NeuralynxIO + + nlx_reader = NeuralynxIO( + dirname=self._filenames[fi], + exclude_filename=self._raw_extras[0]["exclude_fnames"], + ) + neo_block = nlx_reader.read(lazy=True) + + # check that every segment has 1 associated neo.AnalogSignal() object + # (not sure what multiple analogsignals per neo.Segment would mean) + assert sum( + [len(segment.analogsignals) for segment in neo_block[0].segments] + ) == len(neo_block[0].segments) + + # collect sizes of each segment + segment_sizes = np.array( + [ + nlx_reader.get_signal_size(0, segment_id) + for segment_id in range(len(neo_block[0].segments)) + ] + ) + + # construct a (n_segments, 2) array of the first and last + # sample index for each segment relative to the start of the recording + seg_starts = [0] # first chunk starts at sample 0 + seg_stops = [segment_sizes[0] - 1] + for i in range(1, len(segment_sizes)): + ons_new = ( + seg_stops[i - 1] + 1 + ) # current chunk starts one sample after the previous one + seg_starts.append(ons_new) + off_new = ( + seg_stops[i - 1] + segment_sizes[i] + ) # the last sample is len(chunk) samples after the previous ended + seg_stops.append(off_new) + + start_stop_samples = np.stack([np.array(seg_starts), np.array(seg_stops)]).T + + first_seg = self._raw_extras[0]["smp2seg"][ + start + ] # segment containing start sample + last_seg = self._raw_extras[0]["smp2seg"][ + stop - 1 + ] # segment containing stop sample + + # select all segments between the one that contains the start sample + # and the one that contains the stop sample + sel_samples_global = start_stop_samples[first_seg : last_seg + 1, :] + + # express end samples relative to segment onsets + # to be used for slicing the arrays below + sel_samples_local = sel_samples_global.copy() + sel_samples_local[0:-1, 1] = ( + sel_samples_global[0:-1, 1] - sel_samples_global[0:-1, 0] + ) + sel_samples_local[ + 1::, 0 + ] = 0 # now set the start sample for all segments after the first to 0 + + sel_samples_local[0, 0] = ( + start - sel_samples_global[0, 0] + ) # express start sample relative to segment onset + sel_samples_local[-1, -1] = (stop - 1) - sel_samples_global[ + -1, 0 + ] # express stop sample relative to segment onset + + # now load data from selected segments/channels via + # neo.Segment.AnalogSignal.load() + all_data = np.concatenate( + [ + signal.load(channel_indexes=idx).magnitude[ + samples[0] : samples[-1] + 1, : + ] + for seg, samples in zip( + neo_block[0].segments[first_seg : last_seg + 1], sel_samples_local + ) + for signal in seg.analogsignals + ] + ).T + + all_data *= 1e-6 # Convert uV to V + n_channels = len(nlx_reader.header["signal_channels"]["name"]) + block = np.zeros((n_channels, stop - start), dtype=data.dtype) + block[idx] = all_data # shape = (n_channels, n_samples) + + # Then store the result where it needs to go + _mult_cal_one(data, block, idx, cals, mult) diff --git a/mne/io/neuralynx/tests/__init__.py b/mne/io/neuralynx/tests/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/mne/io/neuralynx/tests/test_neuralynx.py b/mne/io/neuralynx/tests/test_neuralynx.py new file mode 100644 index 00000000000..ce5a6f8cb9f --- /dev/null +++ b/mne/io/neuralynx/tests/test_neuralynx.py @@ -0,0 +1,138 @@ +import os +from ast import literal_eval +from typing import Dict + +import numpy as np +import pytest +from numpy.testing import assert_allclose +from scipy.io import loadmat + +from mne.datasets.testing import data_path, requires_testing_data +from mne.io import read_raw_neuralynx +from mne.io.tests.test_raw import _test_raw_reader + +testing_path = data_path(download=False) / "neuralynx" + + +def _nlxheader_to_dict(matdict: Dict) -> Dict: + """Convert the read-in "Header" field into a dict. + + All the key-value pairs of Header entries are formatted as strings + (e.g. np.array("-AdbitVolts 0.000323513")) so we reformat that + into dict by splitting at blank spaces. + """ + entries = matdict["Header"][ + 1::, : + ] # skip the first row which is just the "Header" string + + return { + arr.item().item().split(" ")[0].strip("-"): arr.item().item().split(" ")[-1] + for arr in entries + if arr[0].size > 0 + } + + +def _read_nlx_mat_chan(matfile: str) -> np.ndarray: + """Read a single channel from a Neuralynx .mat file.""" + mat = loadmat(matfile) + + hdr_dict = _nlxheader_to_dict(mat) + + # Nlx2MatCSC.m reads the data in N equal-sized (512-item) chunks + # this array (1, n_chunks) stores the number of valid samples + # per chunk (the last chunk is usually shorter) + n_valid_samples = mat["NumberOfValidSamples"].ravel() + + # concatenate chunks, respecting the number of valid samples + x = np.concatenate( + [mat["Samples"][0:n, i] for i, n in enumerate(n_valid_samples)] + ) # in ADBits + + # this value is the same for all channels and + # converts data from ADBits to Volts + conversionf = literal_eval(hdr_dict["ADBitVolts"]) + x = x * conversionf + + # if header says input was inverted at acquisition + # (possibly for spike detection or so?), flip it back + # NeuralynxIO does this under the hood in NeuralynxIO.parse_header() + # see this discussion: https://github.com/NeuralEnsemble/python-neo/issues/819 + if hdr_dict["InputInverted"] == "True": + x *= -1 + + return x + + +mne_testing_ncs = [ + "LAHC1.ncs", + "LAHC2.ncs", + "LAHC3.ncs", + "LAHCu1.ncs", # the 'u' files are going to be filtered out + "xAIR1.ncs", + "xEKG1.ncs", +] + +expected_chan_names = ["LAHC1", "LAHC2", "LAHC3", "xAIR1", "xEKG1"] + + +@requires_testing_data +def test_neuralynx(): + """Test basic reading.""" + pytest.importorskip("neo") + + from neo.io import NeuralynxIO + + excluded_ncs_files = ["LAHCu1.ncs", "LAHCu2.ncs", "LAHCu3.ncs"] + + # ==== MNE-Python ==== # + raw = read_raw_neuralynx( + fname=testing_path, preload=True, exclude_fname_patterns=["*u*.ncs"] + ) + + # test that channel selection worked + assert ( + raw.ch_names == expected_chan_names + ), "labels in raw.ch_names don't match expected channel names" + + mne_y, mne_t = raw.get_data(return_times=True) # in V + + # ==== NeuralynxIO ==== # + nlx_reader = NeuralynxIO(dirname=testing_path, exclude_filename=excluded_ncs_files) + bl = nlx_reader.read( + lazy=False + ) # read a single block which contains the data split in segments + + # concatenate all signals and times from all segments (== total recording) + nlx_y = np.concatenate( + [sig.magnitude for seg in bl[0].segments for sig in seg.analogsignals] + ).T + nlx_y *= 1e-6 # convert from uV to V + + nlx_t = np.concatenate( + [sig.times.magnitude for seg in bl[0].segments for sig in seg.analogsignals] + ).T + nlx_t = np.round(nlx_t, 3) # round to millisecond precision + + nlx_ch_names = [ch[0] for ch in nlx_reader.header["signal_channels"]] + + # ===== Nlx2MatCSC.m ===== # + matchans = ["LAHC1.mat", "LAHC2.mat", "LAHC3.mat", "xAIR1.mat", "xEKG1.mat"] + + # (n_chan, n_samples) array, in V + mat_y = np.stack( + [_read_nlx_mat_chan(os.path.join(testing_path, ch)) for ch in matchans] + ) + + # ===== Check sample values across MNE-Python, NeuralynxIO and MATLAB ===== # + assert nlx_ch_names == raw.ch_names # check channel names + + assert_allclose( + mne_y, nlx_y, rtol=1e-6, err_msg="MNE and NeuralynxIO not all close" + ) # data + assert_allclose( + mne_y, mat_y, rtol=1e-6, err_msg="MNE and Nlx2MatCSC.m not all close" + ) # data + + _test_raw_reader( + read_raw_neuralynx, fname=testing_path, exclude_fname_patterns=["*u*.ncs"] + ) From 3fd2c912f25d7c6344edb44e77bd19f036db1c5c Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Tue, 14 Nov 2023 15:35:38 -0500 Subject: [PATCH 065/405] MAINT: Dont force git installs (#12209) --- pyproject.toml | 8 ++++---- tools/circleci_dependencies.sh | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index af5b7e6b5ab..8c172fcac70 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -134,7 +134,7 @@ doc = [ "sphinx>=6", "numpydoc", "pydata_sphinx_theme==0.13.3", - "sphinx-gallery @ git+https://github.com/sphinx-gallery/sphinx-gallery@master", + "sphinx-gallery", "sphinxcontrib-bibtex>=2.5", "memory_profiler", "neo", @@ -142,10 +142,10 @@ doc = [ "sphinx_copybutton", "sphinx-design", "sphinxcontrib-youtube", - "mne-bids @ git+https://github.com/mne-tools/mne-bids@main", + "mne-bids", "pyxdf", - "mne-connectivity @ git+https://github.com/mne-tools/mne-connectivity@main", - "mne-gui-addons @ git+https://github.com/mne-tools/mne-gui-addons@main", + "mne-connectivity", + "mne-gui-addons", "pygments>=2.13", "pytest", "graphviz", diff --git a/tools/circleci_dependencies.sh b/tools/circleci_dependencies.sh index 7e244530a34..83021758186 100755 --- a/tools/circleci_dependencies.sh +++ b/tools/circleci_dependencies.sh @@ -1,4 +1,4 @@ #!/bin/bash -ef python -m pip install --upgrade "pip!=20.3.0" build -python -m pip install --upgrade --progress-bar off --only-binary "numpy,scipy,matplotlib,pandas,statsmodels" PyQt6 git+https://github.com/mne-tools/mne-qt-browser -ve .[full,test,doc] +python -m pip install --upgrade --progress-bar off --only-binary "numpy,scipy,matplotlib,pandas,statsmodels" git+https://github.com/sphinx-gallery/sphinx-gallery.git -ve .[full,test,doc] From 9b57c51686ca5536edc2a5e74444428a9a138ef6 Mon Sep 17 00:00:00 2001 From: Jacob Woessner Date: Tue, 14 Nov 2023 15:05:36 -0600 Subject: [PATCH 066/405] [WIP] [BUG] Fix reading bad channels in cnt files (#12189) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Eric Larson --- doc/changes/devel.rst | 2 ++ mne/io/cnt/cnt.py | 39 ++++++++++++++++++++++++++++++------ mne/io/cnt/tests/test_cnt.py | 35 +++++++++++++++++++++++++++++++- 3 files changed, 69 insertions(+), 7 deletions(-) diff --git a/doc/changes/devel.rst b/doc/changes/devel.rst index c586d1e44df..f0179a0a705 100644 --- a/doc/changes/devel.rst +++ b/doc/changes/devel.rst @@ -95,6 +95,8 @@ Bugs - Fix bug with :func:`mne.io.read_raw_eeglab` and :func:`mne.read_epochs_eeglab` where automatic fiducial detection would fail for certain files (:gh:`12165` by `Clemens Brunner`_) - Fix concatenation of ``raws`` with ``np.nan`` in the device to head transformation (:gh:`12198` by `Mathieu Scheltienne`_) - Fix bug with :func:`mne.viz.plot_compare_evokeds` where the title was not displayed when ``axes='topo'`` (:gh:`12192` by `Jacob Woessner`_) +- Fix bug with :func:`mne.io.read_raw_cnt` where the bad channels were not properly read (:gh:`12189` by `Jacob Woessner`_) + API changes ~~~~~~~~~~~ diff --git a/mne/io/cnt/cnt.py b/mne/io/cnt/cnt.py index 249497fdf73..32f2611173e 100644 --- a/mne/io/cnt/cnt.py +++ b/mne/io/cnt/cnt.py @@ -14,7 +14,7 @@ from ..._fiff.utils import _create_chs, _find_channels, _mult_cal_one, read_str from ...annotations import Annotations from ...channels.layout import _topo_to_sphere -from ...utils import _check_option, fill_doc, warn +from ...utils import _check_option, _validate_type, fill_doc, warn from ..base import BaseRaw from ._utils import ( CNTEventType3, @@ -169,6 +169,8 @@ def read_raw_cnt( emg=(), data_format="auto", date_format="mm/dd/yy", + *, + header="auto", preload=False, verbose=None, ): @@ -219,6 +221,13 @@ def read_raw_cnt( Defaults to ``'auto'``. date_format : ``'mm/dd/yy'`` | ``'dd/mm/yy'`` Format of date in the header. Defaults to ``'mm/dd/yy'``. + header : ``'auto'`` | ``'new'`` | ``'old'`` + Defines the header format. Used to describe how bad channels + are formatted. If auto, reads using old and new header and + if either contain a bad channel make channel bad. + Defaults to ``'auto'``. + + .. versionadded:: 1.6 %(preload)s %(verbose)s @@ -244,12 +253,13 @@ def read_raw_cnt( emg=emg, data_format=data_format, date_format=date_format, + header=header, preload=preload, verbose=verbose, ) -def _get_cnt_info(input_fname, eog, ecg, emg, misc, data_format, date_format): +def _get_cnt_info(input_fname, eog, ecg, emg, misc, data_format, date_format, header): """Read the cnt header.""" data_offset = 900 # Size of the 'SETUP' header. cnt_info = dict() @@ -340,13 +350,23 @@ def _get_cnt_info(input_fname, eog, ecg, emg, misc, data_format, date_format): ch_names, cals, baselines, chs, pos = (list(), list(), list(), list(), list()) bads = list() + _validate_type(header, str, "header") + _check_option("header", header, ("auto", "new", "old")) for ch_idx in range(n_channels): # ELECTLOC fields fid.seek(data_offset + 75 * ch_idx) ch_name = read_str(fid, 10) ch_names.append(ch_name) - fid.seek(data_offset + 75 * ch_idx + 4) - if np.fromfile(fid, dtype="u1", count=1).item(): - bads.append(ch_name) + + # Some files have bad channels marked differently in the header. + if header in ("new", "auto"): + fid.seek(data_offset + 75 * ch_idx + 14) + if np.fromfile(fid, dtype="u1", count=1).item(): + bads.append(ch_name) + if header in ("old", "auto"): + fid.seek(data_offset + 75 * ch_idx + 4) + if np.fromfile(fid, dtype="u1", count=1).item(): + bads.append(ch_name) + fid.seek(data_offset + 75 * ch_idx + 19) xy = np.fromfile(fid, dtype="f4", count=2) xy[1] *= -1 # invert y-axis @@ -451,6 +471,11 @@ class RawCNT(BaseRaw): Defaults to ``'auto'``. date_format : ``'mm/dd/yy'`` | ``'dd/mm/yy'`` Format of date in the header. Defaults to ``'mm/dd/yy'``. + header : ``'auto'`` | ``'new'`` | ``'old'`` + Defines the header format. Used to describe how bad channels + are formatted. If auto, reads using old and new header and + if either contain a bad channel make channel bad. + Defaults to ``'auto'``. %(preload)s stim_channel : bool | None Add a stim channel from the events. Defaults to None to trigger a @@ -478,6 +503,8 @@ def __init__( emg=(), data_format="auto", date_format="mm/dd/yy", + *, + header="auto", preload=False, verbose=None, ): # noqa: D102 @@ -489,7 +516,7 @@ def __init__( input_fname = path.abspath(input_fname) info, cnt_info = _get_cnt_info( - input_fname, eog, ecg, emg, misc, data_format, _date_format + input_fname, eog, ecg, emg, misc, data_format, _date_format, header ) last_samps = [cnt_info["n_samples"] - 1] super(RawCNT, self).__init__( diff --git a/mne/io/cnt/tests/test_cnt.py b/mne/io/cnt/tests/test_cnt.py index a5e5788eff3..c7dd956e9f9 100644 --- a/mne/io/cnt/tests/test_cnt.py +++ b/mne/io/cnt/tests/test_cnt.py @@ -19,7 +19,7 @@ @testing.requires_testing_data -def test_data(): +def test_old_data(): """Test reading raw cnt files.""" with pytest.warns(RuntimeWarning, match="number of bytes"): raw = _test_raw_reader( @@ -37,6 +37,39 @@ def test_data(): assert raw.info["meas_date"] is None +@testing.requires_testing_data +def test_new_data(): + """Test reading raw cnt files with different header.""" + with pytest.warns(RuntimeWarning): + raw = read_raw_cnt(input_fname=fname_bad_spans, header="new") + + assert raw.info["bads"] == ["F8"] # test bads + + +@testing.requires_testing_data +def test_auto_data(): + """Test reading raw cnt files with automatic header.""" + with pytest.warns(RuntimeWarning): + raw = read_raw_cnt(input_fname=fname_bad_spans) + + assert raw.info["bads"] == ["F8"] + + with pytest.warns(RuntimeWarning, match="number of bytes"): + raw = _test_raw_reader( + read_raw_cnt, input_fname=fname, eog="auto", misc=["NA1", "LEFT_EAR"] + ) + + # make sure we use annotations event if we synthesized stim + assert len(raw.annotations) == 6 + + eog_chs = pick_types(raw.info, eog=True, exclude=[]) + assert len(eog_chs) == 2 # test eog='auto' + assert raw.info["bads"] == ["LEFT_EAR", "VEOGR"] # test bads + + # the data has "05/10/200 17:35:31" so it is set to None + assert raw.info["meas_date"] is None + + @testing.requires_testing_data def test_compare_events_and_annotations(): """Test comparing annotations and events.""" From 137664c00475d6714ae655eee4fd09ff87bd74c4 Mon Sep 17 00:00:00 2001 From: Marijn van Vliet Date: Wed, 15 Nov 2023 18:14:34 +0200 Subject: [PATCH 067/405] Add tomli to rstcheck dependencies (#12211) --- .pre-commit-config.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3755c67222b..436fbbb80a7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -47,4 +47,6 @@ repos: rev: v6.2.0 hooks: - id: rstcheck + additional_dependencies: + - tomli files: ^doc/.*\.(rst|inc)$ From f58776c4d1a0101c1442ae0432e2395ce4a38809 Mon Sep 17 00:00:00 2001 From: Clemens Brunner Date: Wed, 15 Nov 2023 17:20:36 +0100 Subject: [PATCH 068/405] Raise IndexError when picks exceeds nchan (#12205) --- doc/changes/devel.rst | 1 + mne/_fiff/pick.py | 4 ++-- mne/_fiff/tests/test_pick.py | 4 ++-- mne/channels/tests/test_channels.py | 2 +- mne/io/fiff/tests/test_raw_fiff.py | 11 ++++++++++- 5 files changed, 16 insertions(+), 6 deletions(-) diff --git a/doc/changes/devel.rst b/doc/changes/devel.rst index f0179a0a705..9558f8fe0ea 100644 --- a/doc/changes/devel.rst +++ b/doc/changes/devel.rst @@ -96,6 +96,7 @@ Bugs - Fix concatenation of ``raws`` with ``np.nan`` in the device to head transformation (:gh:`12198` by `Mathieu Scheltienne`_) - Fix bug with :func:`mne.viz.plot_compare_evokeds` where the title was not displayed when ``axes='topo'`` (:gh:`12192` by `Jacob Woessner`_) - Fix bug with :func:`mne.io.read_raw_cnt` where the bad channels were not properly read (:gh:`12189` by `Jacob Woessner`_) +- Fix bug where iterating over :class:`~mne.io.Raw` would result in an error (:gh:`12205` by `Clemens Brunner`_) API changes diff --git a/mne/_fiff/pick.py b/mne/_fiff/pick.py index 3060e306bc3..5328c7fbf37 100644 --- a/mne/_fiff/pick.py +++ b/mne/_fiff/pick.py @@ -1299,9 +1299,9 @@ def _picks_to_idx( "(%r)" % (picks_on, orig_picks) ) if (picks < -n_chan).any(): - raise ValueError("All picks must be >= %d, got %r" % (-n_chan, orig_picks)) + raise IndexError("All picks must be >= %d, got %r" % (-n_chan, orig_picks)) if (picks >= n_chan).any(): - raise ValueError( + raise IndexError( "All picks must be < n_%s (%d), got %r" % (picks_on, n_chan, orig_picks) ) picks %= n_chan # ensure positive diff --git a/mne/_fiff/tests/test_pick.py b/mne/_fiff/tests/test_pick.py index c5bfc9f94ce..fd2658f1da8 100644 --- a/mne/_fiff/tests/test_pick.py +++ b/mne/_fiff/tests/test_pick.py @@ -596,9 +596,9 @@ def test_picks_to_idx(): # Name indexing assert_array_equal([2], _picks_to_idx(info, info["ch_names"][2])) assert_array_equal(np.arange(5, 9), _picks_to_idx(info, info["ch_names"][5:9])) - with pytest.raises(ValueError, match="must be >= "): + with pytest.raises(IndexError, match="must be >= "): _picks_to_idx(info, -len(picks) - 1) - with pytest.raises(ValueError, match="must be < "): + with pytest.raises(IndexError, match="must be < "): _picks_to_idx(info, len(picks)) with pytest.raises(ValueError, match="could not be interpreted"): _picks_to_idx(info, ["a", "b"]) diff --git a/mne/channels/tests/test_channels.py b/mne/channels/tests/test_channels.py index 8e3e482659c..2924aa2dfd9 100644 --- a/mne/channels/tests/test_channels.py +++ b/mne/channels/tests/test_channels.py @@ -288,7 +288,7 @@ def test_read_ch_adjacency(tmp_path): assert_equal(x[0, 1], False) assert_equal(x[0, 2], True) assert np.all(x.diagonal()) - pytest.raises(ValueError, read_ch_adjacency, mat_fname, [0, 3]) + pytest.raises(IndexError, read_ch_adjacency, mat_fname, [0, 3]) ch_adjacency, ch_names = read_ch_adjacency(mat_fname, picks=[0, 2]) assert_equal(ch_adjacency.shape[0], 2) assert_equal(len(ch_names), 2) diff --git a/mne/io/fiff/tests/test_raw_fiff.py b/mne/io/fiff/tests/test_raw_fiff.py index ed94c4c527a..9e4b6e5960a 100644 --- a/mne/io/fiff/tests/test_raw_fiff.py +++ b/mne/io/fiff/tests/test_raw_fiff.py @@ -915,10 +915,19 @@ def test_getitem(): ) with pytest.raises(ValueError, match="No appropriate channels"): raw[slice(-len(raw.ch_names) - 1), slice(None)] - with pytest.raises(ValueError, match="must be"): + with pytest.raises(IndexError, match="must be"): raw[-1000] +@testing.requires_testing_data +def test_iter(): + """Test iterating over Raw via __getitem__().""" + raw = read_raw_fif(fif_fname).pick("eeg") # 60 EEG channels + for i, _ in enumerate(raw): # iterate over channels + pass + assert i == 59 # 60 channels means iterating from 0 to 59 + + @testing.requires_testing_data def test_proj(tmp_path): """Test SSP proj operations.""" From 3e73a0ea8ac886622d8f167bcb1b44d496be8a28 Mon Sep 17 00:00:00 2001 From: Dominik Welke Date: Wed, 15 Nov 2023 18:05:56 +0100 Subject: [PATCH 069/405] DOC: permutation test docstrings should link to adjacency reader (#12212) --- mne/utils/docs.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/mne/utils/docs.py b/mne/utils/docs.py index 573066ca3c2..e68b839055d 100644 --- a/mne/utils/docs.py +++ b/mne/utils/docs.py @@ -101,8 +101,11 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): ] = """ adjacency : scipy.sparse.spmatrix | None | False Defines adjacency between locations in the data, where "locations" can be - spatial vertices, frequency bins, time points, etc. For spatial vertices, - see: :func:`mne.channels.find_ch_adjacency`. If ``False``, assumes + spatial vertices, frequency bins, time points, etc. For spatial vertices + (i.e. sensor space data), see :func:`mne.channels.find_ch_adjacency` or + :func:`mne.spatial_inter_hemi_adjacency`. For source space data, see + :func:`mne.spatial_src_adjacency` or + :func:`mne.spatio_temporal_src_adjacency`. If ``False``, assumes no adjacency (each location is treated as independent and unconnected). If ``None``, a regular lattice adjacency is assumed, connecting each {sp} location to its neighbor(s) along the last dimension From 239e574ee2f16f1c87dc3062df1876b6f5a5b1b5 Mon Sep 17 00:00:00 2001 From: Daniel McCloy Date: Wed, 15 Nov 2023 15:12:50 -0600 Subject: [PATCH 070/405] band-aid for circleCI [skip azp][skip actions] (#12215) --- tools/circleci_dependencies.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/circleci_dependencies.sh b/tools/circleci_dependencies.sh index 83021758186..5ae66219c24 100755 --- a/tools/circleci_dependencies.sh +++ b/tools/circleci_dependencies.sh @@ -2,3 +2,4 @@ python -m pip install --upgrade "pip!=20.3.0" build python -m pip install --upgrade --progress-bar off --only-binary "numpy,scipy,matplotlib,pandas,statsmodels" git+https://github.com/sphinx-gallery/sphinx-gallery.git -ve .[full,test,doc] +python -m pip install git+https://github.com/mne-tools/mne-bids.git From 61777eb2679fbbc08f9752ae93ebcae547977fed Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Thu, 16 Nov 2023 11:34:51 -0500 Subject: [PATCH 071/405] MAINT: Update for mne-bids stable [circle deploy] (#12220) --- tools/circleci_dependencies.sh | 1 - tutorials/clinical/30_ecog.py | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/circleci_dependencies.sh b/tools/circleci_dependencies.sh index 5ae66219c24..83021758186 100755 --- a/tools/circleci_dependencies.sh +++ b/tools/circleci_dependencies.sh @@ -2,4 +2,3 @@ python -m pip install --upgrade "pip!=20.3.0" build python -m pip install --upgrade --progress-bar off --only-binary "numpy,scipy,matplotlib,pandas,statsmodels" git+https://github.com/sphinx-gallery/sphinx-gallery.git -ve .[full,test,doc] -python -m pip install git+https://github.com/mne-tools/mne-bids.git diff --git a/tutorials/clinical/30_ecog.py b/tutorials/clinical/30_ecog.py index d3ee7c9268c..f59ce6b213f 100644 --- a/tutorials/clinical/30_ecog.py +++ b/tutorials/clinical/30_ecog.py @@ -23,6 +23,7 @@ :ref:`manual-install`) as well as ``mne-bids`` which can be installed using ``pip``. """ + # Authors: Eric Larson # Chris Holdgraf # Adam Li From 2efb77d7cbe085be548fdda9dfb3e1ad047ab812 Mon Sep 17 00:00:00 2001 From: Mathieu Scheltienne Date: Fri, 17 Nov 2023 12:09:17 +0100 Subject: [PATCH 072/405] Fix link to mne-lsl (#12222) --- doc/links.inc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/links.inc b/doc/links.inc index 9623c165fe2..52dfec9b068 100644 --- a/doc/links.inc +++ b/doc/links.inc @@ -19,7 +19,7 @@ .. _`MNE-BIDS-Pipeline`: https://mne.tools/mne-bids-pipeline .. _`MNE-HCP`: http://mne.tools/mne-hcp .. _`MNE-Realtime`: https://mne.tools/mne-realtime -.. _`MNE-LSL`: https://mne.tools/mne-realtime +.. _`MNE-LSL`: https://mne.tools/mne-lsl .. _`MNE-gui-addons`: https://mne.tools/mne-gui-addons .. _`MNE-MATLAB`: https://github.com/mne-tools/mne-matlab .. _`MNE-Docker`: https://github.com/mne-tools/mne-docker From 5897b818e90308d25713fd69552fd29862757dce Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Thu, 16 Nov 2023 14:02:45 -0500 Subject: [PATCH 073/405] MAINT: Add copyright to the top of each file (#12221) --- doc/conf.py | 2 + doc/conftest.py | 2 + doc/sphinxext/contrib_avatars.py | 2 + doc/sphinxext/flow_diagram.py | 2 + doc/sphinxext/gen_commands.py | 2 + doc/sphinxext/gen_names.py | 2 + doc/sphinxext/gh_substitutions.py | 2 + doc/sphinxext/mne_substitutions.py | 2 + doc/sphinxext/newcontrib_substitutions.py | 2 + doc/sphinxext/unit_role.py | 2 + examples/datasets/brainstorm_data.py | 1 + examples/datasets/hf_sef_data.py | 1 + examples/datasets/kernel_phantom.py | 2 + examples/datasets/limo_data.py | 1 + examples/datasets/opm_data.py | 2 + examples/datasets/spm_faces_dataset_sgskip.py | 1 + examples/decoding/decoding_csp_eeg.py | 1 + examples/decoding/decoding_csp_timefreq.py | 1 + examples/decoding/decoding_rsa_sgskip.py | 1 + .../decoding_spatio_temporal_source.py | 1 + examples/decoding/decoding_spoc_CMC.py | 1 + ...decoding_time_generalization_conditions.py | 1 + .../decoding_unsupervised_spatial_filter.py | 1 + examples/decoding/decoding_xdawn_eeg.py | 1 + examples/decoding/ems_filtering.py | 1 + examples/decoding/linear_model_patterns.py | 1 + examples/decoding/receptive_field_mtrf.py | 1 + examples/decoding/ssd_spatial_filters.py | 1 + examples/forward/forward_sensitivity_maps.py | 1 + .../forward/left_cerebellum_volume_source.py | 1 + examples/forward/source_space_morphing.py | 1 + .../compute_mne_inverse_epochs_in_label.py | 1 + .../compute_mne_inverse_raw_in_label.py | 1 + .../inverse/compute_mne_inverse_volume.py | 1 + examples/inverse/custom_inverse_solver.py | 2 + examples/inverse/dics_epochs.py | 1 + examples/inverse/dics_source_power.py | 1 + examples/inverse/evoked_ers_source_power.py | 1 + examples/inverse/gamma_map_inverse.py | 1 + examples/inverse/label_activation_from_stc.py | 1 + examples/inverse/label_from_stc.py | 1 + examples/inverse/label_source_activations.py | 1 + examples/inverse/mixed_norm_inverse.py | 1 + .../inverse/mixed_source_space_inverse.py | 1 + examples/inverse/mne_cov_power.py | 1 + examples/inverse/morph_surface_stc.py | 1 + examples/inverse/morph_volume_stc.py | 1 + examples/inverse/multi_dipole_model.py | 1 + .../inverse/multidict_reweighted_tfmxne.py | 1 + examples/inverse/psf_ctf_label_leakage.py | 1 + examples/inverse/psf_ctf_vertices.py | 1 + examples/inverse/psf_ctf_vertices_lcmv.py | 1 + examples/inverse/psf_volume.py | 1 + examples/inverse/rap_music.py | 1 + examples/inverse/read_inverse.py | 1 + examples/inverse/read_stc.py | 1 + examples/inverse/resolution_metrics.py | 1 + examples/inverse/resolution_metrics_eegmeg.py | 1 + examples/inverse/snr_estimate.py | 1 + examples/inverse/source_space_snr.py | 1 + .../time_frequency_mixed_norm_inverse.py | 1 + examples/inverse/trap_music.py | 1 + examples/inverse/vector_mne_solution.py | 1 + examples/io/elekta_epochs.py | 1 + examples/io/read_neo_format.py | 2 + examples/io/read_noise_covariance_matrix.py | 1 + examples/io/read_xdf.py | 1 + .../contralateral_referencing.py | 2 + examples/preprocessing/css.py | 2 + .../preprocessing/define_target_events.py | 1 + examples/preprocessing/eeg_bridging.py | 1 + examples/preprocessing/eeg_csd.py | 1 + .../preprocessing/eog_artifact_histogram.py | 1 + examples/preprocessing/eog_regression.py | 3 +- examples/preprocessing/find_ref_artifacts.py | 1 + .../preprocessing/fnirs_artifact_removal.py | 1 + examples/preprocessing/ica_comparison.py | 1 + .../preprocessing/interpolate_bad_channels.py | 1 + .../preprocessing/movement_compensation.py | 1 + examples/preprocessing/movement_detection.py | 1 + examples/preprocessing/muscle_detection.py | 1 + examples/preprocessing/muscle_ica.py | 1 + examples/preprocessing/otp.py | 1 + examples/preprocessing/shift_evoked.py | 1 + examples/preprocessing/virtual_evoked.py | 1 + examples/preprocessing/xdawn_denoising.py | 1 + examples/simulation/plot_stc_metrics.py | 3 +- examples/simulation/simulate_evoked_data.py | 1 + examples/simulation/simulate_raw_data.py | 1 + ...imulated_raw_data_using_subject_anatomy.py | 1 + examples/simulation/source_simulator.py | 1 + examples/stats/cluster_stats_evoked.py | 1 + examples/stats/fdr_stats_evoked.py | 1 + examples/stats/linear_regression_raw.py | 1 + examples/stats/sensor_permutation_test.py | 1 + examples/stats/sensor_regression.py | 1 + examples/time_frequency/compute_csd.py | 1 + .../compute_source_psd_epochs.py | 1 + .../source_label_time_frequency.py | 1 + .../time_frequency/source_power_spectrum.py | 1 + .../source_power_spectrum_opm.py | 1 + .../source_space_time_frequency.py | 1 + examples/time_frequency/temporal_whitening.py | 1 + .../time_frequency/time_frequency_erds.py | 1 + .../time_frequency_global_field_power.py | 1 + .../time_frequency_simulated.py | 1 + examples/visualization/3d_to_2d.py | 1 + examples/visualization/brain.py | 1 + .../visualization/channel_epochs_image.py | 1 + examples/visualization/eeg_on_scalp.py | 1 + examples/visualization/evoked_arrowmap.py | 1 + examples/visualization/evoked_topomap.py | 1 + examples/visualization/evoked_whitening.py | 1 + .../visualization/eyetracking_plot_heatmap.py | 3 +- examples/visualization/meg_sensors.py | 1 + examples/visualization/mne_helmet.py | 2 + examples/visualization/montage_sgskip.py | 1 + examples/visualization/parcellation.py | 1 + examples/visualization/roi_erpimage_by_rt.py | 1 + .../ssp_projs_sensitivity_map.py | 1 + .../visualization/topo_compare_conditions.py | 1 + examples/visualization/topo_customized.py | 1 + examples/visualization/xhemi.py | 1 + logo/generate_mne_logos.py | 1 + mne/__init__.py | 2 + mne/__main__.py | 3 +- mne/_fiff/__init__.py | 1 + mne/_fiff/_digitization.py | 1 + mne/_fiff/compensator.py | 2 + mne/_fiff/constants.py | 1 + mne/_fiff/ctf_comp.py | 1 + mne/_fiff/matrix.py | 1 + mne/_fiff/meas_info.py | 1 + mne/_fiff/open.py | 1 + mne/_fiff/pick.py | 1 + mne/_fiff/proc_history.py | 3 +- mne/_fiff/proj.py | 1 + mne/_fiff/reference.py | 1 + mne/_fiff/tag.py | 1 + mne/_fiff/tests/__init__.py | 2 + mne/_fiff/tests/test_compensator.py | 1 + mne/_fiff/tests/test_constants.py | 1 + mne/_fiff/tests/test_meas_info.py | 1 + mne/_fiff/tests/test_pick.py | 2 + mne/_fiff/tests/test_proc_history.py | 3 +- mne/_fiff/tests/test_reference.py | 1 + mne/_fiff/tests/test_show_fiff.py | 1 + mne/_fiff/tests/test_utils.py | 1 + mne/_fiff/tests/test_what.py | 3 +- mne/_fiff/tests/test_write.py | 1 + mne/_fiff/tree.py | 1 + mne/_fiff/utils.py | 1 + mne/_fiff/what.py | 1 + mne/_fiff/write.py | 1 + mne/_freesurfer.py | 1 + mne/_ola.py | 1 + mne/annotations.py | 1 + mne/baseline.py | 1 + mne/beamformer/__init__.py | 2 + mne/beamformer/_compute_beamformer.py | 1 + mne/beamformer/_dics.py | 1 + mne/beamformer/_lcmv.py | 1 + mne/beamformer/_rap_music.py | 1 + mne/beamformer/resolution_matrix.py | 1 + mne/beamformer/tests/__init__.py | 2 + mne/beamformer/tests/test_dics.py | 1 + mne/beamformer/tests/test_external.py | 1 + mne/beamformer/tests/test_lcmv.py | 2 + mne/beamformer/tests/test_rap_music.py | 1 + .../tests/test_resolution_matrix.py | 1 + mne/bem.py | 1 + mne/channels/__init__.py | 2 + mne/channels/_dig_montage_utils.py | 3 +- mne/channels/_standard_montage_utils.py | 1 + mne/channels/channels.py | 1 + mne/channels/data/neighbors/__init__.py | 2 + mne/channels/interpolation.py | 1 + mne/channels/layout.py | 3 +- mne/channels/montage.py | 3 +- mne/channels/tests/__init__.py | 2 + mne/channels/tests/test_channels.py | 1 + mne/channels/tests/test_interpolation.py | 2 + mne/channels/tests/test_layout.py | 3 +- mne/channels/tests/test_montage.py | 1 + mne/channels/tests/test_standard_montage.py | 1 + mne/channels/tests/test_unify_bads.py | 2 + mne/chpi.py | 1 + mne/commands/__init__.py | 2 + mne/commands/mne_anonymize.py | 2 + mne/commands/mne_browse_raw.py | 2 + mne/commands/mne_bti2fiff.py | 2 + mne/commands/mne_clean_eog_ecg.py | 2 + mne/commands/mne_compare_fiff.py | 2 + mne/commands/mne_compute_proj_ecg.py | 2 + mne/commands/mne_compute_proj_eog.py | 2 + mne/commands/mne_coreg.py | 2 + mne/commands/mne_flash_bem.py | 2 + mne/commands/mne_freeview_bem_surfaces.py | 2 + mne/commands/mne_kit2fiff.py | 2 + mne/commands/mne_make_scalp_surfaces.py | 2 + mne/commands/mne_maxfilter.py | 2 + mne/commands/mne_prepare_bem_model.py | 2 + mne/commands/mne_report.py | 2 + mne/commands/mne_setup_forward_model.py | 2 + mne/commands/mne_setup_source_space.py | 2 + mne/commands/mne_show_fiff.py | 2 + mne/commands/mne_show_info.py | 2 + mne/commands/mne_surf2bem.py | 1 + mne/commands/mne_sys_info.py | 2 + mne/commands/mne_watershed_bem.py | 2 + mne/commands/mne_what.py | 2 + mne/commands/tests/__init__.py | 2 + mne/commands/tests/test_commands.py | 2 + mne/commands/utils.py | 1 + mne/conftest.py | 1 + mne/coreg.py | 1 + mne/cov.py | 1 + mne/cuda.py | 1 + mne/data/__init__.py | 2 + mne/datasets/__init__.py | 2 + mne/datasets/_fake/__init__.py | 2 + mne/datasets/_fake/_fake.py | 3 +- mne/datasets/_fetch.py | 3 +- mne/datasets/_fsaverage/__init__.py | 2 + mne/datasets/_fsaverage/base.py | 3 +- mne/datasets/_infant/base.py | 3 +- mne/datasets/_phantom/__init__.py | 2 + mne/datasets/_phantom/base.py | 3 +- mne/datasets/brainstorm/__init__.py | 2 + mne/datasets/brainstorm/bst_auditory.py | 1 + mne/datasets/brainstorm/bst_phantom_ctf.py | 1 + mne/datasets/brainstorm/bst_phantom_elekta.py | 1 + mne/datasets/brainstorm/bst_raw.py | 1 + mne/datasets/brainstorm/bst_resting.py | 1 + mne/datasets/config.py | 3 +- mne/datasets/eegbci/__init__.py | 2 + mne/datasets/eegbci/eegbci.py | 3 +- mne/datasets/eegbci/tests/test_eegbci.py | 3 +- mne/datasets/epilepsy_ecog/__init__.py | 2 + mne/datasets/epilepsy_ecog/_data.py | 3 +- mne/datasets/erp_core/__init__.py | 2 + mne/datasets/erp_core/erp_core.py | 2 + mne/datasets/eyelink/__init__.py | 2 + mne/datasets/eyelink/eyelink.py | 3 +- mne/datasets/fieldtrip_cmc/__init__.py | 2 + mne/datasets/fieldtrip_cmc/fieldtrip_cmc.py | 3 +- mne/datasets/fnirs_motor/__init__.py | 2 + mne/datasets/fnirs_motor/fnirs_motor.py | 3 +- mne/datasets/hf_sef/__init__.py | 2 + mne/datasets/hf_sef/hf_sef.py | 3 +- mne/datasets/kiloword/__init__.py | 2 + mne/datasets/kiloword/kiloword.py | 3 +- mne/datasets/limo/__init__.py | 2 + mne/datasets/limo/limo.py | 1 + mne/datasets/misc/__init__.py | 2 + mne/datasets/misc/_misc.py | 3 +- mne/datasets/mtrf/__init__.py | 2 + mne/datasets/mtrf/mtrf.py | 3 +- mne/datasets/multimodal/__init__.py | 2 + mne/datasets/multimodal/multimodal.py | 3 +- mne/datasets/opm/__init__.py | 2 + mne/datasets/opm/opm.py | 3 +- mne/datasets/phantom_4dbti/__init__.py | 2 + mne/datasets/phantom_4dbti/phantom_4dbti.py | 3 +- mne/datasets/phantom_kernel/__init__.py | 2 + mne/datasets/phantom_kernel/phantom_kernel.py | 3 +- mne/datasets/phantom_kit/__init__.py | 2 + mne/datasets/phantom_kit/phantom_kit.py | 2 + mne/datasets/refmeg_noise/__init__.py | 2 + mne/datasets/refmeg_noise/refmeg_noise.py | 3 +- mne/datasets/sample/__init__.py | 2 + mne/datasets/sample/sample.py | 3 +- mne/datasets/sleep_physionet/__init__.py | 2 + mne/datasets/sleep_physionet/_utils.py | 3 +- mne/datasets/sleep_physionet/age.py | 3 +- mne/datasets/sleep_physionet/temazepam.py | 3 +- .../sleep_physionet/tests/test_physionet.py | 3 +- mne/datasets/somato/__init__.py | 2 + mne/datasets/somato/somato.py | 3 +- mne/datasets/spm_face/__init__.py | 2 + mne/datasets/spm_face/spm_data.py | 3 +- mne/datasets/ssvep/__init__.py | 2 + mne/datasets/ssvep/ssvep.py | 3 +- mne/datasets/testing/__init__.py | 2 + mne/datasets/testing/_testing.py | 3 +- mne/datasets/tests/__init__.py | 2 + mne/datasets/tests/test_datasets.py | 2 + mne/datasets/ucl_opm_auditory/__init__.py | 2 + .../ucl_opm_auditory/ucl_opm_auditory.py | 3 +- mne/datasets/utils.py | 3 +- mne/datasets/visual_92_categories/__init__.py | 2 + .../visual_92_categories.py | 3 +- mne/decoding/__init__.py | 2 + mne/decoding/base.py | 1 + mne/decoding/csp.py | 1 + mne/decoding/ems.py | 1 + mne/decoding/mixin.py | 2 + mne/decoding/receptive_field.py | 1 + mne/decoding/search_light.py | 1 + mne/decoding/ssd.py | 1 + mne/decoding/tests/__init__.py | 2 + mne/decoding/tests/test_base.py | 1 + mne/decoding/tests/test_csp.py | 1 + mne/decoding/tests/test_ems.py | 1 + mne/decoding/tests/test_receptive_field.py | 1 + mne/decoding/tests/test_search_light.py | 1 + mne/decoding/tests/test_ssd.py | 1 + mne/decoding/tests/test_time_frequency.py | 1 + mne/decoding/tests/test_transformer.py | 1 + mne/decoding/time_delaying_ridge.py | 1 + mne/decoding/time_frequency.py | 1 + mne/decoding/transformer.py | 1 + mne/defaults.py | 1 + mne/dipole.py | 3 +- mne/epochs.py | 1 + mne/event.py | 1 + mne/evoked.py | 1 + mne/export/__init__.py | 2 + mne/export/_brainvision.py | 1 + mne/export/_edf.py | 1 + mne/export/_eeglab.py | 1 + mne/export/_egimff.py | 1 + mne/export/_export.py | 1 + mne/export/tests/test_export.py | 1 + mne/filter.py | 2 + mne/fixes.py | 3 +- mne/forward/__init__.py | 2 + mne/forward/_compute_forward.py | 1 + mne/forward/_field_interpolation.py | 2 + mne/forward/_lead_dots.py | 1 + mne/forward/_make_forward.py | 1 + mne/forward/forward.py | 1 + mne/forward/tests/__init__.py | 2 + mne/forward/tests/test_field_interpolation.py | 2 + mne/forward/tests/test_forward.py | 2 + mne/forward/tests/test_make_forward.py | 2 + mne/gui/__init__.py | 2 + mne/gui/_coreg.py | 2 + mne/gui/_gui.py | 1 + mne/gui/tests/__init__.py | 2 + mne/gui/tests/test_coreg.py | 1 + mne/gui/tests/test_gui_api.py | 3 +- mne/html_templates/__init__.py | 2 + mne/html_templates/_templates.py | 2 + mne/inverse_sparse/__init__.py | 3 +- mne/inverse_sparse/_gamma_map.py | 3 +- mne/inverse_sparse/mxne_debiasing.py | 1 + mne/inverse_sparse/mxne_inverse.py | 3 +- mne/inverse_sparse/mxne_optim.py | 3 +- mne/inverse_sparse/tests/__init__.py | 2 + mne/inverse_sparse/tests/test_gamma_map.py | 3 +- .../tests/test_mxne_debiasing.py | 1 + mne/inverse_sparse/tests/test_mxne_inverse.py | 3 +- mne/inverse_sparse/tests/test_mxne_optim.py | 3 +- mne/io/__init__.py | 1 + mne/io/_fiff_wrap.py | 2 + mne/io/_read_raw.py | 1 + mne/io/array/__init__.py | 2 + mne/io/array/array.py | 1 + mne/io/array/tests/__init__.py | 2 + mne/io/array/tests/test_array.py | 1 + mne/io/artemis123/__init__.py | 1 + mne/io/artemis123/artemis123.py | 1 + mne/io/artemis123/tests/__init__.py | 2 + mne/io/artemis123/tests/test_artemis123.py | 1 + mne/io/artemis123/utils.py | 2 + mne/io/base.py | 1 + mne/io/besa/__init__.py | 1 + mne/io/besa/besa.py | 2 + mne/io/besa/tests/test_besa.py | 2 + mne/io/boxy/__init__.py | 1 + mne/io/boxy/boxy.py | 1 + mne/io/boxy/tests/__init__.py | 2 + mne/io/boxy/tests/test_boxy.py | 1 + mne/io/brainvision/__init__.py | 1 + mne/io/brainvision/brainvision.py | 1 + mne/io/brainvision/tests/__init__.py | 3 +- mne/io/brainvision/tests/test_brainvision.py | 1 + mne/io/bti/__init__.py | 2 + mne/io/bti/bti.py | 2 + mne/io/bti/constants.py | 1 + mne/io/bti/read.py | 2 + mne/io/bti/tests/__init__.py | 2 + mne/io/bti/tests/test_bti.py | 1 + mne/io/cnt/__init__.py | 2 + mne/io/cnt/_utils.py | 1 + mne/io/cnt/cnt.py | 1 + mne/io/cnt/tests/__init__.py | 2 + mne/io/cnt/tests/test_cnt.py | 1 + mne/io/constants.py | 1 + mne/io/ctf/__init__.py | 1 + mne/io/ctf/constants.py | 1 + mne/io/ctf/ctf.py | 1 + mne/io/ctf/eeg.py | 1 + mne/io/ctf/hc.py | 1 + mne/io/ctf/info.py | 1 + mne/io/ctf/markers.py | 1 + mne/io/ctf/res4.py | 1 + mne/io/ctf/tests/__init__.py | 2 + mne/io/ctf/tests/test_ctf.py | 1 + mne/io/ctf/trans.py | 1 + mne/io/curry/__init__.py | 1 + mne/io/curry/curry.py | 1 + mne/io/curry/tests/__init__.py | 2 + mne/io/curry/tests/test_curry.py | 1 + mne/io/edf/__init__.py | 1 + mne/io/edf/edf.py | 1 + mne/io/edf/tests/__init__.py | 2 + mne/io/edf/tests/test_edf.py | 1 + mne/io/edf/tests/test_gdf.py | 1 + mne/io/eeglab/__init__.py | 2 + mne/io/eeglab/_eeglab.py | 2 + mne/io/eeglab/eeglab.py | 1 + mne/io/eeglab/tests/__init__.py | 2 + mne/io/eeglab/tests/test_eeglab.py | 1 + mne/io/egi/__init__.py | 2 + mne/io/egi/egi.py | 2 + mne/io/egi/egimff.py | 2 + mne/io/egi/events.py | 1 + mne/io/egi/general.py | 1 + mne/io/egi/tests/__init__.py | 2 + mne/io/egi/tests/test_egi.py | 2 + mne/io/eximia/__init__.py | 1 + mne/io/eximia/eximia.py | 1 + mne/io/eximia/tests/__init__.py | 2 + mne/io/eximia/tests/test_eximia.py | 2 + mne/io/eyelink/__init__.py | 1 + mne/io/eyelink/_utils.py | 1 + mne/io/eyelink/eyelink.py | 1 + mne/io/eyelink/tests/__init__.py | 2 + mne/io/eyelink/tests/test_eyelink.py | 2 + mne/io/fieldtrip/__init__.py | 1 + mne/io/fieldtrip/fieldtrip.py | 1 + mne/io/fieldtrip/tests/__init__.py | 1 + mne/io/fieldtrip/tests/helpers.py | 1 + mne/io/fieldtrip/tests/test_fieldtrip.py | 1 + mne/io/fieldtrip/utils.py | 1 + mne/io/fiff/__init__.py | 2 + mne/io/fiff/raw.py | 1 + mne/io/fiff/tests/__init__.py | 2 + mne/io/fiff/tests/test_raw_fiff.py | 1 + mne/io/fil/__init__.py | 1 + mne/io/fil/fil.py | 1 + mne/io/fil/sensors.py | 1 + mne/io/fil/tests/test_fil.py | 1 + mne/io/hitachi/__init__.py | 1 + mne/io/hitachi/hitachi.py | 1 + mne/io/hitachi/tests/test_hitachi.py | 1 + mne/io/kit/__init__.py | 1 + mne/io/kit/constants.py | 1 + mne/io/kit/coreg.py | 1 + mne/io/kit/kit.py | 1 + mne/io/kit/tests/__init__.py | 2 + mne/io/kit/tests/test_coreg.py | 1 + mne/io/kit/tests/test_kit.py | 1 + mne/io/meas_info.py | 1 + mne/io/nedf/__init__.py | 1 + mne/io/nedf/nedf.py | 2 + mne/io/nedf/tests/__init__.py | 3 +- mne/io/nedf/tests/test_nedf.py | 1 + mne/io/neuralynx/__init__.py | 2 + mne/io/neuralynx/neuralynx.py | 2 + mne/io/neuralynx/tests/__init__.py | 2 + mne/io/neuralynx/tests/test_neuralynx.py | 2 + mne/io/nicolet/__init__.py | 1 + mne/io/nicolet/nicolet.py | 1 + mne/io/nicolet/tests/__init__.py | 2 + mne/io/nicolet/tests/test_nicolet.py | 1 + mne/io/nihon/__init__.py | 1 + mne/io/nihon/nihon.py | 1 + mne/io/nihon/tests/test_nihon.py | 2 + mne/io/nirx/__init__.py | 1 + mne/io/nirx/_localized_abbr.py | 1 + mne/io/nirx/nirx.py | 1 + mne/io/nirx/tests/__init__.py | 2 + mne/io/nirx/tests/test_nirx.py | 2 + mne/io/nsx/__init__.py | 1 + mne/io/nsx/nsx.py | 1 + mne/io/nsx/tests/test_nsx.py | 1 + mne/io/persyst/__init__.py | 1 + mne/io/persyst/persyst.py | 1 + mne/io/persyst/tests/__init__.py | 2 + mne/io/persyst/tests/test_persyst.py | 1 + mne/io/pick.py | 1 + mne/io/proj.py | 1 + mne/io/reference.py | 1 + mne/io/snirf/__init__.py | 1 + mne/io/snirf/_snirf.py | 1 + mne/io/snirf/tests/__init__.py | 2 + mne/io/snirf/tests/test_snirf.py | 2 + mne/io/tag.py | 1 + mne/io/tests/__init__.py | 2 + mne/io/tests/data/__init__.py | 2 + mne/io/tests/test_apply_function.py | 1 + mne/io/tests/test_deprecation.py | 1 + mne/io/tests/test_raw.py | 1 + mne/io/tests/test_read_raw.py | 1 + mne/io/utils.py | 1 + mne/io/write.py | 1 + mne/label.py | 1 + mne/minimum_norm/__init__.py | 2 + mne/minimum_norm/_eloreta.py | 1 + mne/minimum_norm/inverse.py | 1 + mne/minimum_norm/resolution_matrix.py | 1 + mne/minimum_norm/spatial_resolution.py | 1 + mne/minimum_norm/tests/__init__.py | 2 + mne/minimum_norm/tests/test_inverse.py | 2 + .../tests/test_resolution_matrix.py | 1 + .../tests/test_resolution_metrics.py | 1 + mne/minimum_norm/tests/test_snr.py | 1 + mne/minimum_norm/tests/test_time_frequency.py | 2 + mne/minimum_norm/time_frequency.py | 1 + mne/misc.py | 1 + mne/morph.py | 1 + mne/morph_map.py | 1 + mne/parallel.py | 3 +- mne/preprocessing/__init__.py | 1 + mne/preprocessing/_annotate_amplitude.py | 1 + mne/preprocessing/_annotate_nan.py | 1 + mne/preprocessing/_csd.py | 18 +-- mne/preprocessing/_css.py | 2 + mne/preprocessing/_fine_cal.py | 1 + mne/preprocessing/_peak_finder.py | 2 + mne/preprocessing/_regress.py | 1 + mne/preprocessing/artifact_detection.py | 1 + mne/preprocessing/bads.py | 1 + mne/preprocessing/ctps_.py | 3 +- mne/preprocessing/ecg.py | 1 + mne/preprocessing/eog.py | 1 + mne/preprocessing/eyetracking/__init__.py | 1 + .../eyetracking/_pupillometry.py | 1 + mne/preprocessing/eyetracking/calibration.py | 1 + mne/preprocessing/eyetracking/eyetracking.py | 1 + .../eyetracking/tests/__init__.py | 2 + .../eyetracking/tests/test_calibration.py | 2 + .../eyetracking/tests/test_pupillometry.py | 3 +- mne/preprocessing/hfc.py | 1 + mne/preprocessing/ica.py | 1 + mne/preprocessing/ieeg/__init__.py | 1 + mne/preprocessing/ieeg/_projection.py | 1 + mne/preprocessing/ieeg/_volume.py | 1 + .../ieeg/tests/test_projection.py | 1 + mne/preprocessing/ieeg/tests/test_volume.py | 1 + mne/preprocessing/infomax_.py | 1 + mne/preprocessing/interpolate.py | 2 + mne/preprocessing/maxfilter.py | 1 + mne/preprocessing/maxwell.py | 1 + mne/preprocessing/nirs/__init__.py | 1 + mne/preprocessing/nirs/_beer_lambert_law.py | 1 + mne/preprocessing/nirs/_optical_density.py | 1 + .../nirs/_scalp_coupling_index.py | 1 + mne/preprocessing/nirs/_tddr.py | 1 + mne/preprocessing/nirs/nirs.py | 1 + .../nirs/tests/test_beer_lambert_law.py | 1 + mne/preprocessing/nirs/tests/test_nirs.py | 1 + .../nirs/tests/test_optical_density.py | 1 + .../nirs/tests/test_scalp_coupling_index.py | 1 + ...temporal_derivative_distribution_repair.py | 1 + mne/preprocessing/otp.py | 1 + mne/preprocessing/realign.py | 1 + mne/preprocessing/ssp.py | 1 + mne/preprocessing/stim.py | 1 + mne/preprocessing/tests/__init__.py | 2 + .../tests/test_annotate_amplitude.py | 1 + mne/preprocessing/tests/test_annotate_nan.py | 1 + .../tests/test_artifact_detection.py | 1 + mne/preprocessing/tests/test_csd.py | 1 + mne/preprocessing/tests/test_css.py | 2 + mne/preprocessing/tests/test_ctps.py | 1 + mne/preprocessing/tests/test_ecg.py | 2 + .../tests/test_eeglab_infomax.py | 2 + mne/preprocessing/tests/test_eog.py | 2 + mne/preprocessing/tests/test_fine_cal.py | 1 + mne/preprocessing/tests/test_hfc.py | 1 + mne/preprocessing/tests/test_ica.py | 1 + mne/preprocessing/tests/test_infomax.py | 1 + mne/preprocessing/tests/test_interpolate.py | 2 + mne/preprocessing/tests/test_maxwell.py | 1 + mne/preprocessing/tests/test_otp.py | 1 + mne/preprocessing/tests/test_peak_finder.py | 2 + mne/preprocessing/tests/test_realign.py | 1 + mne/preprocessing/tests/test_regress.py | 1 + mne/preprocessing/tests/test_ssp.py | 2 + mne/preprocessing/tests/test_stim.py | 1 + mne/preprocessing/tests/test_xdawn.py | 1 + mne/preprocessing/xdawn.py | 1 + mne/proj.py | 1 + mne/rank.py | 1 + mne/report/__init__.py | 2 + .../bootstrap-icons/gen_css_for_mne.py | 1 + mne/report/report.py | 1 + mne/report/tests/test_report.py | 1 + mne/simulation/__init__.py | 2 + mne/simulation/_metrics.py | 1 + mne/simulation/evoked.py | 1 + mne/simulation/metrics/__init__.py | 2 + mne/simulation/metrics/metrics.py | 3 +- mne/simulation/metrics/tests/__init__.py | 2 + mne/simulation/metrics/tests/test_metrics.py | 3 +- mne/simulation/raw.py | 1 + mne/simulation/source.py | 1 + mne/simulation/tests/__init__.py | 2 + mne/simulation/tests/test_evoked.py | 1 + mne/simulation/tests/test_metrics.py | 1 + mne/simulation/tests/test_raw.py | 1 + mne/simulation/tests/test_source.py | 1 + mne/source_estimate.py | 1 + mne/source_space/__init__.py | 2 + mne/source_space/_source_space.py | 1 + mne/source_space/tests/__init__.py | 2 + mne/source_space/tests/test_source_space.py | 1 + mne/stats/__init__.py | 2 + mne/stats/_adjacency.py | 1 + mne/stats/cluster_level.py | 3 +- mne/stats/multi_comp.py | 1 + mne/stats/parametric.py | 3 +- mne/stats/permutations.py | 3 +- mne/stats/regression.py | 1 + mne/stats/tests/__init__.py | 2 + mne/stats/tests/test_adjacency.py | 3 +- mne/stats/tests/test_cluster_level.py | 1 + mne/stats/tests/test_multi_comp.py | 2 + mne/stats/tests/test_parametric.py | 2 + mne/stats/tests/test_permutations.py | 1 + mne/stats/tests/test_regression.py | 1 + mne/surface.py | 1 + mne/tests/__init__.py | 2 + mne/tests/test_annotations.py | 1 + mne/tests/test_bem.py | 1 + mne/tests/test_chpi.py | 1 + mne/tests/test_coreg.py | 2 + mne/tests/test_cov.py | 1 + mne/tests/test_defaults.py | 2 + mne/tests/test_dipole.py | 1 + mne/tests/test_docstring_parameters.py | 1 + mne/tests/test_epochs.py | 1 + mne/tests/test_event.py | 1 + mne/tests/test_evoked.py | 1 + mne/tests/test_filter.py | 2 + mne/tests/test_freesurfer.py | 2 + mne/tests/test_import_nesting.py | 1 + mne/tests/test_label.py | 1 + mne/tests/test_line_endings.py | 1 + mne/tests/test_misc.py | 1 + mne/tests/test_morph.py | 1 + mne/tests/test_morph_map.py | 1 + mne/tests/test_ola.py | 2 + mne/tests/test_parallel.py | 1 + mne/tests/test_proj.py | 2 + mne/tests/test_rank.py | 2 + mne/tests/test_read_vectorview_selection.py | 2 + mne/tests/test_source_estimate.py | 1 + mne/tests/test_surface.py | 1 + mne/tests/test_transforms.py | 1 + mne/time_frequency/__init__.py | 2 + mne/time_frequency/_stft.py | 2 + mne/time_frequency/_stockwell.py | 2 + mne/time_frequency/ar.py | 1 + mne/time_frequency/csd.py | 1 + mne/time_frequency/multitaper.py | 2 + mne/time_frequency/psd.py | 2 + mne/time_frequency/spectrum.py | 1 + mne/time_frequency/tests/__init__.py | 2 + mne/time_frequency/tests/test_ar.py | 2 + mne/time_frequency/tests/test_csd.py | 2 + mne/time_frequency/tests/test_multitaper.py | 2 + mne/time_frequency/tests/test_psd.py | 2 + mne/time_frequency/tests/test_spectrum.py | 2 + mne/time_frequency/tests/test_stft.py | 2 + mne/time_frequency/tests/test_stockwell.py | 2 + mne/time_frequency/tests/test_tfr.py | 2 + mne/time_frequency/tfr.py | 2 + mne/transforms.py | 1 + mne/utils/__init__.py | 2 + mne/utils/_bunch.py | 1 + mne/utils/_logging.py | 1 + mne/utils/_testing.py | 1 + mne/utils/check.py | 1 + mne/utils/config.py | 1 + mne/utils/dataframe.py | 1 + mne/utils/docs.py | 1 + mne/utils/fetching.py | 1 + mne/utils/linalg.py | 1 + mne/utils/misc.py | 1 + mne/utils/mixin.py | 1 + mne/utils/numerics.py | 1 + mne/utils/progressbar.py | 1 + mne/utils/spectrum.py | 2 + mne/utils/tests/test_bunch.py | 1 + mne/utils/tests/test_check.py | 1 + mne/utils/tests/test_config.py | 2 + mne/utils/tests/test_docs.py | 2 + mne/utils/tests/test_linalg.py | 1 + mne/utils/tests/test_logging.py | 2 + mne/utils/tests/test_misc.py | 2 + mne/utils/tests/test_mixin.py | 1 + mne/utils/tests/test_numerics.py | 2 + mne/utils/tests/test_progressbar.py | 1 + mne/utils/tests/test_testing.py | 1 + mne/viz/_3d.py | 3 +- mne/viz/_3d_overlay.py | 3 +- mne/viz/__init__.py | 2 + mne/viz/_brain/__init__.py | 3 +- mne/viz/_brain/_brain.py | 3 +- mne/viz/_brain/_linkviewer.py | 3 +- mne/viz/_brain/_scraper.py | 2 + mne/viz/_brain/colormap.py | 3 +- mne/viz/_brain/surface.py | 3 +- mne/viz/_brain/tests/test_brain.py | 3 +- mne/viz/_brain/tests/test_notebook.py | 2 + mne/viz/_brain/view.py | 3 +- mne/viz/_dipole.py | 3 +- mne/viz/_figure.py | 3 +- mne/viz/_mpl_figure.py | 3 +- mne/viz/_proj.py | 3 +- mne/viz/_scraper.py | 3 +- mne/viz/backends/__init__.py | 2 + mne/viz/backends/_abstract.py | 3 +- mne/viz/backends/_notebook.py | 3 +- mne/viz/backends/_pyvista.py | 3 +- mne/viz/backends/_qt.py | 3 +- mne/viz/backends/_utils.py | 3 +- mne/viz/backends/renderer.py | 3 +- mne/viz/backends/tests/_utils.py | 3 +- mne/viz/backends/tests/test_abstract.py | 3 +- mne/viz/backends/tests/test_renderer.py | 3 +- mne/viz/backends/tests/test_utils.py | 3 +- mne/viz/circle.py | 3 +- mne/viz/conftest.py | 1 + mne/viz/epochs.py | 3 +- mne/viz/evoked.py | 3 +- mne/viz/evoked_field.py | 2 + mne/viz/eyetracking/__init__.py | 1 + mne/viz/eyetracking/heatmap.py | 1 + mne/viz/eyetracking/tests/__init__.py | 2 + mne/viz/eyetracking/tests/test_heatmap.py | 3 +- mne/viz/ica.py | 3 +- mne/viz/misc.py | 3 +- mne/viz/montage.py | 2 + mne/viz/raw.py | 3 +- mne/viz/tests/__init__.py | 2 + mne/viz/tests/test_3d.py | 3 +- mne/viz/tests/test_3d_mpl.py | 3 +- mne/viz/tests/test_circle.py | 3 +- mne/viz/tests/test_epochs.py | 3 +- mne/viz/tests/test_evoked.py | 3 +- mne/viz/tests/test_figure.py | 3 +- mne/viz/tests/test_ica.py | 3 +- mne/viz/tests/test_misc.py | 3 +- mne/viz/tests/test_montage.py | 3 +- mne/viz/tests/test_proj.py | 3 +- mne/viz/tests/test_raw.py | 3 +- mne/viz/tests/test_scraper.py | 3 +- mne/viz/tests/test_topo.py | 3 +- mne/viz/tests/test_topomap.py | 3 +- mne/viz/tests/test_ui_events.py | 3 +- mne/viz/tests/test_utils.py | 3 +- mne/viz/topo.py | 3 +- mne/viz/topomap.py | 3 +- mne/viz/ui_events.py | 2 + mne/viz/utils.py | 3 +- tools/check_mne_location.py | 2 + tools/dev/check_steering_committee.py | 2 + tools/dev/ensure_headers.py | 122 ++++++++++++++++++ tools/dev/generate_pyi_files.py | 2 + tools/generate_codemeta.py | 2 + tutorials/clinical/20_seeg.py | 1 + tutorials/clinical/30_ecog.py | 1 + tutorials/clinical/60_sleep.py | 1 + tutorials/epochs/10_epochs_overview.py | 2 + tutorials/epochs/15_baseline_regression.py | 1 + tutorials/epochs/20_visualize_epochs.py | 2 + tutorials/epochs/30_epochs_metadata.py | 2 + tutorials/epochs/40_autogenerate_metadata.py | 2 + tutorials/epochs/50_epochs_to_data_frame.py | 2 + .../epochs/60_make_fixed_length_epochs.py | 2 + tutorials/evoked/10_evoked_overview.py | 2 + tutorials/evoked/20_visualize_evoked.py | 2 + tutorials/evoked/30_eeg_erp.py | 2 + tutorials/evoked/40_whitened.py | 2 + tutorials/forward/10_background_freesurfer.py | 2 + tutorials/forward/20_source_alignment.py | 2 + tutorials/forward/25_automated_coreg.py | 1 + tutorials/forward/30_forward.py | 2 + tutorials/forward/35_eeg_no_mri.py | 1 + .../forward/50_background_freesurfer_mne.py | 2 + tutorials/forward/80_fix_bem_in_blender.py | 1 + tutorials/forward/90_compute_covariance.py | 2 + tutorials/intro/10_overview.py | 2 + tutorials/intro/15_inplace.py | 2 + tutorials/intro/20_events_from_raw.py | 2 + tutorials/intro/30_info.py | 2 + tutorials/intro/40_sensor_locations.py | 2 + tutorials/intro/50_configure_mne.py | 2 + tutorials/intro/70_report.py | 2 + tutorials/inverse/10_stc_class.py | 2 + tutorials/inverse/20_dipole_fit.py | 2 + tutorials/inverse/30_mne_dspm_loreta.py | 2 + tutorials/inverse/35_dipole_orientations.py | 2 + tutorials/inverse/40_mne_fixed_free.py | 1 + tutorials/inverse/50_beamformer_lcmv.py | 1 + tutorials/inverse/60_visualize_stc.py | 2 + tutorials/inverse/70_eeg_mri_coords.py | 1 + .../inverse/80_brainstorm_phantom_elekta.py | 1 + .../inverse/85_brainstorm_phantom_ctf.py | 1 + tutorials/inverse/90_phantom_4DBTi.py | 1 + tutorials/inverse/95_phantom_KIT.py | 1 + tutorials/io/10_reading_meg_data.py | 2 + tutorials/io/20_reading_eeg_data.py | 2 + tutorials/io/30_reading_fnirs_data.py | 2 + tutorials/io/60_ctf_bst_auditory.py | 1 + tutorials/io/70_reading_eyetracking_data.py | 3 +- tutorials/machine-learning/30_strf.py | 1 + tutorials/machine-learning/50_decoding.py | 2 + .../10_preprocessing_overview.py | 2 + .../preprocessing/15_handling_bad_channels.py | 2 + .../preprocessing/20_rejecting_bad_data.py | 2 + .../preprocessing/25_background_filtering.py | 2 + .../preprocessing/30_filtering_resampling.py | 2 + .../35_artifact_correction_regression.py | 2 + .../40_artifact_correction_ica.py | 2 + .../preprocessing/45_projectors_background.py | 2 + .../50_artifact_correction_ssp.py | 2 + .../preprocessing/55_setting_eeg_reference.py | 2 + tutorials/preprocessing/59_head_positions.py | 1 + .../preprocessing/60_maxwell_filtering_sss.py | 2 + .../preprocessing/70_fnirs_processing.py | 2 + tutorials/preprocessing/80_opm_processing.py | 2 + .../preprocessing/90_eyetracking_data.py | 2 +- tutorials/raw/10_raw_overview.py | 2 + tutorials/raw/20_event_arrays.py | 2 + tutorials/raw/30_annotate_raw.py | 2 + tutorials/raw/40_visualize_raw.py | 2 + tutorials/simulation/10_array_objs.py | 2 + tutorials/simulation/70_point_spread.py | 2 + tutorials/simulation/80_dics.py | 1 + .../stats-sensor-space/10_background_stats.py | 1 + tutorials/stats-sensor-space/20_erp_stats.py | 2 + .../40_cluster_1samp_time_freq.py | 1 + .../50_cluster_between_time_freq.py | 1 + .../70_cluster_rmANOVA_time_freq.py | 1 + .../75_cluster_ftest_spatiotemporal.py | 1 + .../20_cluster_1samp_spatiotemporal.py | 1 + .../30_cluster_ftest_spatiotemporal.py | 1 + .../60_cluster_rmANOVA_spatiotemporal.py | 1 + tutorials/time-freq/10_spectrum_class.py | 2 + .../time-freq/20_sensors_time_frequency.py | 1 + tutorials/time-freq/50_ssvep.py | 1 + .../visualization/10_publication_figure.py | 1 + tutorials/visualization/20_ui_events.py | 1 + 850 files changed, 1371 insertions(+), 126 deletions(-) create mode 100644 tools/dev/ensure_headers.py diff --git a/doc/conf.py b/doc/conf.py index c1afdedb4f2..2267fcb1026 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -3,6 +3,8 @@ # This file only contains a selection of the most common options. For a full # list see the documentation: # https://www.sphinx-doc.org/en/master/usage/configuration.html +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. from datetime import datetime, timezone import faulthandler diff --git a/doc/conftest.py b/doc/conftest.py index 102c338598e..2782b6956ac 100644 --- a/doc/conftest.py +++ b/doc/conftest.py @@ -1 +1,3 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. from mne.conftest import * # noqa diff --git a/doc/sphinxext/contrib_avatars.py b/doc/sphinxext/contrib_avatars.py index e3bedc5e649..bbfd17de7d3 100644 --- a/doc/sphinxext/contrib_avatars.py +++ b/doc/sphinxext/contrib_avatars.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. from pathlib import Path from selenium import webdriver diff --git a/doc/sphinxext/flow_diagram.py b/doc/sphinxext/flow_diagram.py index d6a941d7869..ba374c60f88 100644 --- a/doc/sphinxext/flow_diagram.py +++ b/doc/sphinxext/flow_diagram.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import os from os import path as op diff --git a/doc/sphinxext/gen_commands.py b/doc/sphinxext/gen_commands.py index 21c31d2829b..5fa9cd7418a 100644 --- a/doc/sphinxext/gen_commands.py +++ b/doc/sphinxext/gen_commands.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import glob from importlib import import_module import os diff --git a/doc/sphinxext/gen_names.py b/doc/sphinxext/gen_names.py index c5cc7f9f9ea..1871ae0068c 100644 --- a/doc/sphinxext/gen_names.py +++ b/doc/sphinxext/gen_names.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import os from os import path as op diff --git a/doc/sphinxext/gh_substitutions.py b/doc/sphinxext/gh_substitutions.py index 4463425867d..bccc16d13d0 100644 --- a/doc/sphinxext/gh_substitutions.py +++ b/doc/sphinxext/gh_substitutions.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. from docutils.nodes import reference from docutils.parsers.rst.roles import set_classes diff --git a/doc/sphinxext/mne_substitutions.py b/doc/sphinxext/mne_substitutions.py index debfffc50da..6a5cdbb6797 100644 --- a/doc/sphinxext/mne_substitutions.py +++ b/doc/sphinxext/mne_substitutions.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. from docutils import nodes from docutils.parsers.rst import Directive from docutils.statemachine import StringList diff --git a/doc/sphinxext/newcontrib_substitutions.py b/doc/sphinxext/newcontrib_substitutions.py index 8c31e8ca0e2..41cf348c7c4 100644 --- a/doc/sphinxext/newcontrib_substitutions.py +++ b/doc/sphinxext/newcontrib_substitutions.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. from docutils.nodes import reference, strong, target diff --git a/doc/sphinxext/unit_role.py b/doc/sphinxext/unit_role.py index 83b82c223e4..b882aedc6b1 100644 --- a/doc/sphinxext/unit_role.py +++ b/doc/sphinxext/unit_role.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. from docutils import nodes diff --git a/examples/datasets/brainstorm_data.py b/examples/datasets/brainstorm_data.py index 03bc28f8132..0f32c704284 100644 --- a/examples/datasets/brainstorm_data.py +++ b/examples/datasets/brainstorm_data.py @@ -14,6 +14,7 @@ # Authors: Mainak Jas # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/examples/datasets/hf_sef_data.py b/examples/datasets/hf_sef_data.py index f9240698231..ec6ef61bcb2 100644 --- a/examples/datasets/hf_sef_data.py +++ b/examples/datasets/hf_sef_data.py @@ -10,6 +10,7 @@ # Author: Jussi Nurminen (jnu@iki.fi) # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/examples/datasets/kernel_phantom.py b/examples/datasets/kernel_phantom.py index f71a07ce7d5..d29e9196d66 100644 --- a/examples/datasets/kernel_phantom.py +++ b/examples/datasets/kernel_phantom.py @@ -8,6 +8,8 @@ stimulated with 7 modules active (121 channels). Here we show some example traces. """ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np import mne diff --git a/examples/datasets/limo_data.py b/examples/datasets/limo_data.py index 4f5291358c3..4a0f96ed8ff 100644 --- a/examples/datasets/limo_data.py +++ b/examples/datasets/limo_data.py @@ -35,6 +35,7 @@ # Authors: Jose C. Garcia Alanis # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% import matplotlib.pyplot as plt diff --git a/examples/datasets/opm_data.py b/examples/datasets/opm_data.py index 60c4a08f8fe..3f1903b3010 100644 --- a/examples/datasets/opm_data.py +++ b/examples/datasets/opm_data.py @@ -10,6 +10,8 @@ we demonstrate how to localize these custom OPM data in MNE. """ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. # sphinx_gallery_thumbnail_number = 4 import numpy as np diff --git a/examples/datasets/spm_faces_dataset_sgskip.py b/examples/datasets/spm_faces_dataset_sgskip.py index cdc7dc9aab0..1357fc513b6 100644 --- a/examples/datasets/spm_faces_dataset_sgskip.py +++ b/examples/datasets/spm_faces_dataset_sgskip.py @@ -19,6 +19,7 @@ # Denis Engemann # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/examples/decoding/decoding_csp_eeg.py b/examples/decoding/decoding_csp_eeg.py index 9be079d928f..85a468cb590 100644 --- a/examples/decoding/decoding_csp_eeg.py +++ b/examples/decoding/decoding_csp_eeg.py @@ -16,6 +16,7 @@ # Authors: Martin Billinger # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/examples/decoding/decoding_csp_timefreq.py b/examples/decoding/decoding_csp_timefreq.py index cfeaf326ce6..f81e4fc0fea 100644 --- a/examples/decoding/decoding_csp_timefreq.py +++ b/examples/decoding/decoding_csp_timefreq.py @@ -17,6 +17,7 @@ # Alexandre Gramfort # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/examples/decoding/decoding_rsa_sgskip.py b/examples/decoding/decoding_rsa_sgskip.py index 0b6b8e7a340..d25844dc1a5 100644 --- a/examples/decoding/decoding_rsa_sgskip.py +++ b/examples/decoding/decoding_rsa_sgskip.py @@ -26,6 +26,7 @@ # Alexandre Gramfort # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/examples/decoding/decoding_spatio_temporal_source.py b/examples/decoding/decoding_spatio_temporal_source.py index 3efbde4c047..696eab955af 100644 --- a/examples/decoding/decoding_spatio_temporal_source.py +++ b/examples/decoding/decoding_spatio_temporal_source.py @@ -18,6 +18,7 @@ # Eric Larson # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/examples/decoding/decoding_spoc_CMC.py b/examples/decoding/decoding_spoc_CMC.py index ba3c23a08dc..4d49fb1e350 100644 --- a/examples/decoding/decoding_spoc_CMC.py +++ b/examples/decoding/decoding_spoc_CMC.py @@ -21,6 +21,7 @@ # Jean-Remi King # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% import matplotlib.pyplot as plt diff --git a/examples/decoding/decoding_time_generalization_conditions.py b/examples/decoding/decoding_time_generalization_conditions.py index a81123c73d7..71ce7b1f076 100644 --- a/examples/decoding/decoding_time_generalization_conditions.py +++ b/examples/decoding/decoding_time_generalization_conditions.py @@ -16,6 +16,7 @@ # Denis Engemann # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/examples/decoding/decoding_unsupervised_spatial_filter.py b/examples/decoding/decoding_unsupervised_spatial_filter.py index b27c16e003a..33b286be21b 100644 --- a/examples/decoding/decoding_unsupervised_spatial_filter.py +++ b/examples/decoding/decoding_unsupervised_spatial_filter.py @@ -14,6 +14,7 @@ # Asish Panda # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/examples/decoding/decoding_xdawn_eeg.py b/examples/decoding/decoding_xdawn_eeg.py index 221a16c380c..76817eb2850 100644 --- a/examples/decoding/decoding_xdawn_eeg.py +++ b/examples/decoding/decoding_xdawn_eeg.py @@ -13,6 +13,7 @@ # Authors: Alexandre Barachant # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/examples/decoding/ems_filtering.py b/examples/decoding/ems_filtering.py index 0273643c61a..1f96bd349a6 100644 --- a/examples/decoding/ems_filtering.py +++ b/examples/decoding/ems_filtering.py @@ -22,6 +22,7 @@ # Jean-Remi King # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/examples/decoding/linear_model_patterns.py b/examples/decoding/linear_model_patterns.py index caf53603f97..d4758b97dae 100644 --- a/examples/decoding/linear_model_patterns.py +++ b/examples/decoding/linear_model_patterns.py @@ -19,6 +19,7 @@ # Jean-Remi King # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/examples/decoding/receptive_field_mtrf.py b/examples/decoding/receptive_field_mtrf.py index 799920611bf..24b459f192f 100644 --- a/examples/decoding/receptive_field_mtrf.py +++ b/examples/decoding/receptive_field_mtrf.py @@ -24,6 +24,7 @@ # Nicolas Barascud # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% # sphinx_gallery_thumbnail_number = 3 diff --git a/examples/decoding/ssd_spatial_filters.py b/examples/decoding/ssd_spatial_filters.py index c3165a7110f..5f4ea3fbcf7 100644 --- a/examples/decoding/ssd_spatial_filters.py +++ b/examples/decoding/ssd_spatial_filters.py @@ -16,6 +16,7 @@ # Author: Denis A. Engemann # Victoria Peterson # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/examples/forward/forward_sensitivity_maps.py b/examples/forward/forward_sensitivity_maps.py index db501375b86..abda7be4b16 100644 --- a/examples/forward/forward_sensitivity_maps.py +++ b/examples/forward/forward_sensitivity_maps.py @@ -15,6 +15,7 @@ # Author: Eric Larson # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/examples/forward/left_cerebellum_volume_source.py b/examples/forward/left_cerebellum_volume_source.py index e74b71c6c4f..22e46073d88 100644 --- a/examples/forward/left_cerebellum_volume_source.py +++ b/examples/forward/left_cerebellum_volume_source.py @@ -13,6 +13,7 @@ # Author: Alan Leggitt # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/examples/forward/source_space_morphing.py b/examples/forward/source_space_morphing.py index 5085e629615..fd5b992696e 100644 --- a/examples/forward/source_space_morphing.py +++ b/examples/forward/source_space_morphing.py @@ -18,6 +18,7 @@ # Eric larson # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/examples/inverse/compute_mne_inverse_epochs_in_label.py b/examples/inverse/compute_mne_inverse_epochs_in_label.py index b7938868058..ca15c80efcc 100644 --- a/examples/inverse/compute_mne_inverse_epochs_in_label.py +++ b/examples/inverse/compute_mne_inverse_epochs_in_label.py @@ -11,6 +11,7 @@ # Author: Alexandre Gramfort # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/examples/inverse/compute_mne_inverse_raw_in_label.py b/examples/inverse/compute_mne_inverse_raw_in_label.py index 5c15563f76a..d2d7b8be3d2 100644 --- a/examples/inverse/compute_mne_inverse_raw_in_label.py +++ b/examples/inverse/compute_mne_inverse_raw_in_label.py @@ -13,6 +13,7 @@ # Author: Alexandre Gramfort # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/examples/inverse/compute_mne_inverse_volume.py b/examples/inverse/compute_mne_inverse_volume.py index 501bc30199a..8283dfdeeca 100644 --- a/examples/inverse/compute_mne_inverse_volume.py +++ b/examples/inverse/compute_mne_inverse_volume.py @@ -11,6 +11,7 @@ # Author: Alexandre Gramfort # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/examples/inverse/custom_inverse_solver.py b/examples/inverse/custom_inverse_solver.py index 6798d0c80a3..64add6b8d25 100644 --- a/examples/inverse/custom_inverse_solver.py +++ b/examples/inverse/custom_inverse_solver.py @@ -19,6 +19,8 @@ in order to try out another inverse algorithm. """ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% import numpy as np diff --git a/examples/inverse/dics_epochs.py b/examples/inverse/dics_epochs.py index 6370275e3b2..d480b13f8a4 100644 --- a/examples/inverse/dics_epochs.py +++ b/examples/inverse/dics_epochs.py @@ -15,6 +15,7 @@ # Alex Rockhill # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np diff --git a/examples/inverse/dics_source_power.py b/examples/inverse/dics_source_power.py index a140b32e7e4..586044cdd9f 100644 --- a/examples/inverse/dics_source_power.py +++ b/examples/inverse/dics_source_power.py @@ -17,6 +17,7 @@ # Stefan Appelhoff # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/examples/inverse/evoked_ers_source_power.py b/examples/inverse/evoked_ers_source_power.py index 0ffcf90816d..7ae7fa86424 100644 --- a/examples/inverse/evoked_ers_source_power.py +++ b/examples/inverse/evoked_ers_source_power.py @@ -14,6 +14,7 @@ # Eric Larson # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/examples/inverse/gamma_map_inverse.py b/examples/inverse/gamma_map_inverse.py index 2a11f32fd41..3d10cbe0aac 100644 --- a/examples/inverse/gamma_map_inverse.py +++ b/examples/inverse/gamma_map_inverse.py @@ -11,6 +11,7 @@ # Daniel Strohmeier # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/examples/inverse/label_activation_from_stc.py b/examples/inverse/label_activation_from_stc.py index a154b5b1aec..ae0e528924a 100644 --- a/examples/inverse/label_activation_from_stc.py +++ b/examples/inverse/label_activation_from_stc.py @@ -14,6 +14,7 @@ # Author: Christian Brodbeck # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/examples/inverse/label_from_stc.py b/examples/inverse/label_from_stc.py index 31d057b92e6..d73d7a20b45 100644 --- a/examples/inverse/label_from_stc.py +++ b/examples/inverse/label_from_stc.py @@ -15,6 +15,7 @@ # Author: Luke Bloy # Alex Gramfort # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/examples/inverse/label_source_activations.py b/examples/inverse/label_source_activations.py index 060e2506cf9..4a92ea27962 100644 --- a/examples/inverse/label_source_activations.py +++ b/examples/inverse/label_source_activations.py @@ -15,6 +15,7 @@ # Eric Larson # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/examples/inverse/mixed_norm_inverse.py b/examples/inverse/mixed_norm_inverse.py index 031483ae137..038bbad0d8b 100644 --- a/examples/inverse/mixed_norm_inverse.py +++ b/examples/inverse/mixed_norm_inverse.py @@ -14,6 +14,7 @@ # Daniel Strohmeier # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/examples/inverse/mixed_source_space_inverse.py b/examples/inverse/mixed_source_space_inverse.py index ddf3e35db45..bec6fc6177d 100644 --- a/examples/inverse/mixed_source_space_inverse.py +++ b/examples/inverse/mixed_source_space_inverse.py @@ -11,6 +11,7 @@ # Author: Annalisa Pascarella # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/examples/inverse/mne_cov_power.py b/examples/inverse/mne_cov_power.py index 79f3dd08a4e..a6cf0df181f 100644 --- a/examples/inverse/mne_cov_power.py +++ b/examples/inverse/mne_cov_power.py @@ -20,6 +20,7 @@ # Luke Bloy # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/examples/inverse/morph_surface_stc.py b/examples/inverse/morph_surface_stc.py index 0417a8d807a..abf84345e14 100644 --- a/examples/inverse/morph_surface_stc.py +++ b/examples/inverse/morph_surface_stc.py @@ -26,6 +26,7 @@ # Author: Tommy Clausner # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% import mne diff --git a/examples/inverse/morph_volume_stc.py b/examples/inverse/morph_volume_stc.py index c5fc2b5130c..144bfd631e3 100644 --- a/examples/inverse/morph_volume_stc.py +++ b/examples/inverse/morph_volume_stc.py @@ -23,6 +23,7 @@ # Author: Tommy Clausner # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% import os diff --git a/examples/inverse/multi_dipole_model.py b/examples/inverse/multi_dipole_model.py index b05ead57498..b6985bcc182 100644 --- a/examples/inverse/multi_dipole_model.py +++ b/examples/inverse/multi_dipole_model.py @@ -26,6 +26,7 @@ # Author: Marijn van Vliet # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. ############################################################################### # Importing everything and setting up the data paths for the MNE-Sample diff --git a/examples/inverse/multidict_reweighted_tfmxne.py b/examples/inverse/multidict_reweighted_tfmxne.py index ce45efc8707..aed27ea880b 100644 --- a/examples/inverse/multidict_reweighted_tfmxne.py +++ b/examples/inverse/multidict_reweighted_tfmxne.py @@ -23,6 +23,7 @@ # Alexandre Gramfort # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/examples/inverse/psf_ctf_label_leakage.py b/examples/inverse/psf_ctf_label_leakage.py index 1ca6d5de8a1..4991ae6eaf3 100644 --- a/examples/inverse/psf_ctf_label_leakage.py +++ b/examples/inverse/psf_ctf_label_leakage.py @@ -17,6 +17,7 @@ # Nicolas P. Rougier (graph code borrowed from his matplotlib gallery) # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/examples/inverse/psf_ctf_vertices.py b/examples/inverse/psf_ctf_vertices.py index efbfe6ff470..e6ab88a5fef 100644 --- a/examples/inverse/psf_ctf_vertices.py +++ b/examples/inverse/psf_ctf_vertices.py @@ -11,6 +11,7 @@ # Alexandre Gramfort # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/examples/inverse/psf_ctf_vertices_lcmv.py b/examples/inverse/psf_ctf_vertices_lcmv.py index cd5527c88c9..569f77ab237 100644 --- a/examples/inverse/psf_ctf_vertices_lcmv.py +++ b/examples/inverse/psf_ctf_vertices_lcmv.py @@ -12,6 +12,7 @@ # Author: Olaf Hauk # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/examples/inverse/psf_volume.py b/examples/inverse/psf_volume.py index 1074527d6af..91af78b9dfc 100644 --- a/examples/inverse/psf_volume.py +++ b/examples/inverse/psf_volume.py @@ -12,6 +12,7 @@ # Eric Larson # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/examples/inverse/rap_music.py b/examples/inverse/rap_music.py index 4c294d06d7b..cc386605dc1 100644 --- a/examples/inverse/rap_music.py +++ b/examples/inverse/rap_music.py @@ -12,6 +12,7 @@ # Author: Yousra Bekhti # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/examples/inverse/read_inverse.py b/examples/inverse/read_inverse.py index a0fe1774252..95db394012e 100644 --- a/examples/inverse/read_inverse.py +++ b/examples/inverse/read_inverse.py @@ -10,6 +10,7 @@ # Author: Alexandre Gramfort # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/examples/inverse/read_stc.py b/examples/inverse/read_stc.py index d98ba170400..9b2823bd7a7 100644 --- a/examples/inverse/read_stc.py +++ b/examples/inverse/read_stc.py @@ -11,6 +11,7 @@ # Author: Alexandre Gramfort # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/examples/inverse/resolution_metrics.py b/examples/inverse/resolution_metrics.py index 594a37a5161..5ab39d3c645 100644 --- a/examples/inverse/resolution_metrics.py +++ b/examples/inverse/resolution_metrics.py @@ -14,6 +14,7 @@ # Author: Olaf Hauk # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/examples/inverse/resolution_metrics_eegmeg.py b/examples/inverse/resolution_metrics_eegmeg.py index d570cb42baa..51acd51fd94 100644 --- a/examples/inverse/resolution_metrics_eegmeg.py +++ b/examples/inverse/resolution_metrics_eegmeg.py @@ -16,6 +16,7 @@ # Author: Olaf Hauk # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/examples/inverse/snr_estimate.py b/examples/inverse/snr_estimate.py index 6422c0a1d05..fda8ada5d0c 100644 --- a/examples/inverse/snr_estimate.py +++ b/examples/inverse/snr_estimate.py @@ -11,6 +11,7 @@ # Author: Eric Larson # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/examples/inverse/source_space_snr.py b/examples/inverse/source_space_snr.py index 690f16f7eb8..04b429fe218 100644 --- a/examples/inverse/source_space_snr.py +++ b/examples/inverse/source_space_snr.py @@ -12,6 +12,7 @@ # Kaisu Lankinen # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/examples/inverse/time_frequency_mixed_norm_inverse.py b/examples/inverse/time_frequency_mixed_norm_inverse.py index 5d1e680776a..693c4ec88d5 100644 --- a/examples/inverse/time_frequency_mixed_norm_inverse.py +++ b/examples/inverse/time_frequency_mixed_norm_inverse.py @@ -23,6 +23,7 @@ # Daniel Strohmeier # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/examples/inverse/trap_music.py b/examples/inverse/trap_music.py index 5262b4b9515..08dee1e54a9 100644 --- a/examples/inverse/trap_music.py +++ b/examples/inverse/trap_music.py @@ -12,6 +12,7 @@ # Author: Théodore Papadopoulo # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/examples/inverse/vector_mne_solution.py b/examples/inverse/vector_mne_solution.py index 0511a2c7821..ca953cd2f24 100644 --- a/examples/inverse/vector_mne_solution.py +++ b/examples/inverse/vector_mne_solution.py @@ -22,6 +22,7 @@ # Author: Marijn van Vliet # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/examples/io/elekta_epochs.py b/examples/io/elekta_epochs.py index 0b8a3a0f162..5619a0e5174 100644 --- a/examples/io/elekta_epochs.py +++ b/examples/io/elekta_epochs.py @@ -11,6 +11,7 @@ # Author: Jussi Nurminen (jnu@iki.fi) # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/examples/io/read_neo_format.py b/examples/io/read_neo_format.py index 2146764d522..a22fec663aa 100644 --- a/examples/io/read_neo_format.py +++ b/examples/io/read_neo_format.py @@ -11,6 +11,8 @@ :ref:`tut-creating-data-structures`. """ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% import neo diff --git a/examples/io/read_noise_covariance_matrix.py b/examples/io/read_noise_covariance_matrix.py index ba9e126a4ea..b8b1dc5832c 100644 --- a/examples/io/read_noise_covariance_matrix.py +++ b/examples/io/read_noise_covariance_matrix.py @@ -10,6 +10,7 @@ # Author: Alexandre Gramfort # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/examples/io/read_xdf.py b/examples/io/read_xdf.py index 1edc8faf2e6..8ed69a3289b 100644 --- a/examples/io/read_xdf.py +++ b/examples/io/read_xdf.py @@ -14,6 +14,7 @@ # Eric Larson # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/examples/preprocessing/contralateral_referencing.py b/examples/preprocessing/contralateral_referencing.py index c3aff2afe16..e9c8818c8e5 100644 --- a/examples/preprocessing/contralateral_referencing.py +++ b/examples/preprocessing/contralateral_referencing.py @@ -12,6 +12,8 @@ contralateral EEG reference. """ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import mne ssvep_folder = mne.datasets.ssvep.data_path() diff --git a/examples/preprocessing/css.py b/examples/preprocessing/css.py index ab7309d98d3..9095094d93c 100644 --- a/examples/preprocessing/css.py +++ b/examples/preprocessing/css.py @@ -16,6 +16,8 @@ """ # Author: John G Samuelsson +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import matplotlib.pyplot as plt import numpy as np diff --git a/examples/preprocessing/define_target_events.py b/examples/preprocessing/define_target_events.py index 86ed28c7505..5672b8d69ad 100644 --- a/examples/preprocessing/define_target_events.py +++ b/examples/preprocessing/define_target_events.py @@ -18,6 +18,7 @@ # Authors: Denis Engemann # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/examples/preprocessing/eeg_bridging.py b/examples/preprocessing/eeg_bridging.py index 6d2c1aec165..6c7052cb028 100644 --- a/examples/preprocessing/eeg_bridging.py +++ b/examples/preprocessing/eeg_bridging.py @@ -30,6 +30,7 @@ # Authors: Alex Rockhill # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/examples/preprocessing/eeg_csd.py b/examples/preprocessing/eeg_csd.py index 98d968d4c94..73515e1f043 100644 --- a/examples/preprocessing/eeg_csd.py +++ b/examples/preprocessing/eeg_csd.py @@ -15,6 +15,7 @@ # Authors: Alex Rockhill # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/examples/preprocessing/eog_artifact_histogram.py b/examples/preprocessing/eog_artifact_histogram.py index 0f9de66fda7..d883fa427f8 100644 --- a/examples/preprocessing/eog_artifact_histogram.py +++ b/examples/preprocessing/eog_artifact_histogram.py @@ -11,6 +11,7 @@ # Authors: Eric Larson # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/examples/preprocessing/eog_regression.py b/examples/preprocessing/eog_regression.py index 621195d5818..e3b8341e744 100644 --- a/examples/preprocessing/eog_regression.py +++ b/examples/preprocessing/eog_regression.py @@ -13,7 +13,8 @@ # Author: Marijn van Vliet # -# License: BSD (3-clause) +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% # Import packages and load data diff --git a/examples/preprocessing/find_ref_artifacts.py b/examples/preprocessing/find_ref_artifacts.py index ca6a2833298..93b96e89e9c 100644 --- a/examples/preprocessing/find_ref_artifacts.py +++ b/examples/preprocessing/find_ref_artifacts.py @@ -29,6 +29,7 @@ # Authors: Jeff Hanna # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/examples/preprocessing/fnirs_artifact_removal.py b/examples/preprocessing/fnirs_artifact_removal.py index 3d842ce92a3..7c4855086a7 100644 --- a/examples/preprocessing/fnirs_artifact_removal.py +++ b/examples/preprocessing/fnirs_artifact_removal.py @@ -12,6 +12,7 @@ # Authors: Robert Luke # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/examples/preprocessing/ica_comparison.py b/examples/preprocessing/ica_comparison.py index 51d1bc7974e..02930174435 100644 --- a/examples/preprocessing/ica_comparison.py +++ b/examples/preprocessing/ica_comparison.py @@ -12,6 +12,7 @@ # Authors: Pierre Ablin # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/examples/preprocessing/interpolate_bad_channels.py b/examples/preprocessing/interpolate_bad_channels.py index 7040e24299e..a56aec7d8f7 100644 --- a/examples/preprocessing/interpolate_bad_channels.py +++ b/examples/preprocessing/interpolate_bad_channels.py @@ -17,6 +17,7 @@ # Mainak Jas # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/examples/preprocessing/movement_compensation.py b/examples/preprocessing/movement_compensation.py index 97d183533a8..146a8b6f290 100644 --- a/examples/preprocessing/movement_compensation.py +++ b/examples/preprocessing/movement_compensation.py @@ -16,6 +16,7 @@ # Authors: Eric Larson # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/examples/preprocessing/movement_detection.py b/examples/preprocessing/movement_detection.py index 2984c53fea2..9bcac562588 100644 --- a/examples/preprocessing/movement_detection.py +++ b/examples/preprocessing/movement_detection.py @@ -19,6 +19,7 @@ # Authors: Adonay Nunes # Luke Bloy # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/examples/preprocessing/muscle_detection.py b/examples/preprocessing/muscle_detection.py index 011e6c23e30..3e0e140c802 100644 --- a/examples/preprocessing/muscle_detection.py +++ b/examples/preprocessing/muscle_detection.py @@ -27,6 +27,7 @@ # Authors: Adonay Nunes # Luke Bloy # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/examples/preprocessing/muscle_ica.py b/examples/preprocessing/muscle_ica.py index 8b1e0480266..f57e24a678b 100644 --- a/examples/preprocessing/muscle_ica.py +++ b/examples/preprocessing/muscle_ica.py @@ -16,6 +16,7 @@ # Authors: Alex Rockhill # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/examples/preprocessing/otp.py b/examples/preprocessing/otp.py index afef134c61d..aa235e79a78 100644 --- a/examples/preprocessing/otp.py +++ b/examples/preprocessing/otp.py @@ -11,6 +11,7 @@ # Author: Eric Larson # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% import numpy as np diff --git a/examples/preprocessing/shift_evoked.py b/examples/preprocessing/shift_evoked.py index 27c4bc45f02..0e8c52676fe 100644 --- a/examples/preprocessing/shift_evoked.py +++ b/examples/preprocessing/shift_evoked.py @@ -9,6 +9,7 @@ # Author: Mainak Jas # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/examples/preprocessing/virtual_evoked.py b/examples/preprocessing/virtual_evoked.py index 096165910da..20f7527f1da 100644 --- a/examples/preprocessing/virtual_evoked.py +++ b/examples/preprocessing/virtual_evoked.py @@ -17,6 +17,7 @@ # Author: Mainak Jas # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/examples/preprocessing/xdawn_denoising.py b/examples/preprocessing/xdawn_denoising.py index 67082d60947..6fc38a55b94 100644 --- a/examples/preprocessing/xdawn_denoising.py +++ b/examples/preprocessing/xdawn_denoising.py @@ -21,6 +21,7 @@ # Authors: Alexandre Barachant # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/examples/simulation/plot_stc_metrics.py b/examples/simulation/plot_stc_metrics.py index f7dfa657569..8b481aed9e6 100644 --- a/examples/simulation/plot_stc_metrics.py +++ b/examples/simulation/plot_stc_metrics.py @@ -11,7 +11,8 @@ """ # Author: Kostiantyn Maksymenko # -# License: BSD (3-clause) +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. from functools import partial diff --git a/examples/simulation/simulate_evoked_data.py b/examples/simulation/simulate_evoked_data.py index 8f09c12e40c..447f548e779 100644 --- a/examples/simulation/simulate_evoked_data.py +++ b/examples/simulation/simulate_evoked_data.py @@ -11,6 +11,7 @@ # Alexandre Gramfort # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/examples/simulation/simulate_raw_data.py b/examples/simulation/simulate_raw_data.py index 4cf88a9930b..ef375bfec38 100644 --- a/examples/simulation/simulate_raw_data.py +++ b/examples/simulation/simulate_raw_data.py @@ -13,6 +13,7 @@ # Eric Larson # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/examples/simulation/simulated_raw_data_using_subject_anatomy.py b/examples/simulation/simulated_raw_data_using_subject_anatomy.py index af13d124383..08f30497998 100644 --- a/examples/simulation/simulated_raw_data_using_subject_anatomy.py +++ b/examples/simulation/simulated_raw_data_using_subject_anatomy.py @@ -17,6 +17,7 @@ # Samuel Deslauriers-Gauthier # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/examples/simulation/source_simulator.py b/examples/simulation/source_simulator.py index 69cb803c134..f417b96f181 100644 --- a/examples/simulation/source_simulator.py +++ b/examples/simulation/source_simulator.py @@ -14,6 +14,7 @@ class to generate source estimates and raw data. It is meant to be a brief # Samuel Deslauriers-Gauthier # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/examples/stats/cluster_stats_evoked.py b/examples/stats/cluster_stats_evoked.py index 112ee80d9ab..b51601f2f32 100644 --- a/examples/stats/cluster_stats_evoked.py +++ b/examples/stats/cluster_stats_evoked.py @@ -13,6 +13,7 @@ # Authors: Alexandre Gramfort # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/examples/stats/fdr_stats_evoked.py b/examples/stats/fdr_stats_evoked.py index 0b0b1b5f935..f7b78f7c559 100644 --- a/examples/stats/fdr_stats_evoked.py +++ b/examples/stats/fdr_stats_evoked.py @@ -13,6 +13,7 @@ # Authors: Alexandre Gramfort # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/examples/stats/linear_regression_raw.py b/examples/stats/linear_regression_raw.py index 407051e33c5..5c09e5a9443 100644 --- a/examples/stats/linear_regression_raw.py +++ b/examples/stats/linear_regression_raw.py @@ -19,6 +19,7 @@ # Authors: Jona Sassenhagen # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/examples/stats/sensor_permutation_test.py b/examples/stats/sensor_permutation_test.py index ee548b364f3..7aaa75ba023 100644 --- a/examples/stats/sensor_permutation_test.py +++ b/examples/stats/sensor_permutation_test.py @@ -13,6 +13,7 @@ # Authors: Alexandre Gramfort # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/examples/stats/sensor_regression.py b/examples/stats/sensor_regression.py index 28d63360776..e3f1452badb 100644 --- a/examples/stats/sensor_regression.py +++ b/examples/stats/sensor_regression.py @@ -24,6 +24,7 @@ # Jona Sassenhagen # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/examples/time_frequency/compute_csd.py b/examples/time_frequency/compute_csd.py index b87f284ec63..7d46770c14c 100644 --- a/examples/time_frequency/compute_csd.py +++ b/examples/time_frequency/compute_csd.py @@ -18,6 +18,7 @@ """ # Author: Marijn van Vliet # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% import mne diff --git a/examples/time_frequency/compute_source_psd_epochs.py b/examples/time_frequency/compute_source_psd_epochs.py index e28e28bf5e9..0fa7558e9f4 100644 --- a/examples/time_frequency/compute_source_psd_epochs.py +++ b/examples/time_frequency/compute_source_psd_epochs.py @@ -12,6 +12,7 @@ # Author: Martin Luessi # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/examples/time_frequency/source_label_time_frequency.py b/examples/time_frequency/source_label_time_frequency.py index a9c32934e38..80a25fffab9 100644 --- a/examples/time_frequency/source_label_time_frequency.py +++ b/examples/time_frequency/source_label_time_frequency.py @@ -16,6 +16,7 @@ # Authors: Alexandre Gramfort # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/examples/time_frequency/source_power_spectrum.py b/examples/time_frequency/source_power_spectrum.py index 5eaec30cb78..77af97a8726 100644 --- a/examples/time_frequency/source_power_spectrum.py +++ b/examples/time_frequency/source_power_spectrum.py @@ -11,6 +11,7 @@ # Authors: Alexandre Gramfort # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/examples/time_frequency/source_power_spectrum_opm.py b/examples/time_frequency/source_power_spectrum_opm.py index ce2ad03f607..dd142138784 100644 --- a/examples/time_frequency/source_power_spectrum_opm.py +++ b/examples/time_frequency/source_power_spectrum_opm.py @@ -26,6 +26,7 @@ # Eric Larson # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/examples/time_frequency/source_space_time_frequency.py b/examples/time_frequency/source_space_time_frequency.py index 61c3959c232..119c06e9230 100644 --- a/examples/time_frequency/source_space_time_frequency.py +++ b/examples/time_frequency/source_space_time_frequency.py @@ -13,6 +13,7 @@ # Authors: Alexandre Gramfort # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/examples/time_frequency/temporal_whitening.py b/examples/time_frequency/temporal_whitening.py index 38863baf227..3a0a04c01c5 100644 --- a/examples/time_frequency/temporal_whitening.py +++ b/examples/time_frequency/temporal_whitening.py @@ -12,6 +12,7 @@ # Authors: Alexandre Gramfort # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/examples/time_frequency/time_frequency_erds.py b/examples/time_frequency/time_frequency_erds.py index 8f7f9a9d5fd..ee2dd62a2ba 100644 --- a/examples/time_frequency/time_frequency_erds.py +++ b/examples/time_frequency/time_frequency_erds.py @@ -30,6 +30,7 @@ # Felix Klotzsche # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% # As usual, we import everything we need. diff --git a/examples/time_frequency/time_frequency_global_field_power.py b/examples/time_frequency/time_frequency_global_field_power.py index 905b2bb646a..0bf72082442 100644 --- a/examples/time_frequency/time_frequency_global_field_power.py +++ b/examples/time_frequency/time_frequency_global_field_power.py @@ -41,6 +41,7 @@ # Stefan Appelhoff # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% import matplotlib.pyplot as plt diff --git a/examples/time_frequency/time_frequency_simulated.py b/examples/time_frequency/time_frequency_simulated.py index 7b3a08faee5..9dfe38eab8b 100644 --- a/examples/time_frequency/time_frequency_simulated.py +++ b/examples/time_frequency/time_frequency_simulated.py @@ -17,6 +17,7 @@ # Alex Rockhill # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/examples/visualization/3d_to_2d.py b/examples/visualization/3d_to_2d.py index 586f8bac734..6d8e8674fa3 100644 --- a/examples/visualization/3d_to_2d.py +++ b/examples/visualization/3d_to_2d.py @@ -20,6 +20,7 @@ # Alex Rockhill # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% from os.path import dirname diff --git a/examples/visualization/brain.py b/examples/visualization/brain.py index 35a7ac77bfd..004842f0052 100644 --- a/examples/visualization/brain.py +++ b/examples/visualization/brain.py @@ -10,6 +10,7 @@ # Author: Alex Rockhill # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% # Load data diff --git a/examples/visualization/channel_epochs_image.py b/examples/visualization/channel_epochs_image.py index 86c8f626db2..9281270a8c1 100644 --- a/examples/visualization/channel_epochs_image.py +++ b/examples/visualization/channel_epochs_image.py @@ -17,6 +17,7 @@ # Authors: Alexandre Gramfort # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/examples/visualization/eeg_on_scalp.py b/examples/visualization/eeg_on_scalp.py index f27bc63ecdd..704ce77d7d1 100644 --- a/examples/visualization/eeg_on_scalp.py +++ b/examples/visualization/eeg_on_scalp.py @@ -10,6 +10,7 @@ # Author: Eric Larson # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/examples/visualization/evoked_arrowmap.py b/examples/visualization/evoked_arrowmap.py index 01bb7bfb405..bdad8d4ad79 100644 --- a/examples/visualization/evoked_arrowmap.py +++ b/examples/visualization/evoked_arrowmap.py @@ -20,6 +20,7 @@ # Authors: Sheraz Khan # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/examples/visualization/evoked_topomap.py b/examples/visualization/evoked_topomap.py index 0f64f60cfe6..f75869383a9 100644 --- a/examples/visualization/evoked_topomap.py +++ b/examples/visualization/evoked_topomap.py @@ -16,6 +16,7 @@ # Alex Rockhill # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% # sphinx_gallery_thumbnail_number = 5 diff --git a/examples/visualization/evoked_whitening.py b/examples/visualization/evoked_whitening.py index ce460778b07..e213408276a 100644 --- a/examples/visualization/evoked_whitening.py +++ b/examples/visualization/evoked_whitening.py @@ -20,6 +20,7 @@ # Denis A. Engemann # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/examples/visualization/eyetracking_plot_heatmap.py b/examples/visualization/eyetracking_plot_heatmap.py index 00c9fee6611..c12aa689984 100644 --- a/examples/visualization/eyetracking_plot_heatmap.py +++ b/examples/visualization/eyetracking_plot_heatmap.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ .. _tut-eyetrack-heatmap: @@ -15,6 +14,8 @@ """ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% # Data loading # ------------ diff --git a/examples/visualization/meg_sensors.py b/examples/visualization/meg_sensors.py index 3685ee68543..182a8ee8940 100644 --- a/examples/visualization/meg_sensors.py +++ b/examples/visualization/meg_sensors.py @@ -10,6 +10,7 @@ # Author: Eric Larson # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/examples/visualization/mne_helmet.py b/examples/visualization/mne_helmet.py index fa73f643f31..944d1c387cc 100644 --- a/examples/visualization/mne_helmet.py +++ b/examples/visualization/mne_helmet.py @@ -8,6 +8,8 @@ This tutorial shows how to make the MNE helmet + brain image. """ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% import mne diff --git a/examples/visualization/montage_sgskip.py b/examples/visualization/montage_sgskip.py index 95e9912c47d..b3d05247855 100644 --- a/examples/visualization/montage_sgskip.py +++ b/examples/visualization/montage_sgskip.py @@ -11,6 +11,7 @@ # Joan Massich # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/examples/visualization/parcellation.py b/examples/visualization/parcellation.py index 9e416c97c48..d92a849b970 100644 --- a/examples/visualization/parcellation.py +++ b/examples/visualization/parcellation.py @@ -22,6 +22,7 @@ # Denis Engemann # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/examples/visualization/roi_erpimage_by_rt.py b/examples/visualization/roi_erpimage_by_rt.py index 770fa2e13e3..f9cd9f708cf 100644 --- a/examples/visualization/roi_erpimage_by_rt.py +++ b/examples/visualization/roi_erpimage_by_rt.py @@ -18,6 +18,7 @@ # Authors: Jona Sassenhagen # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/examples/visualization/ssp_projs_sensitivity_map.py b/examples/visualization/ssp_projs_sensitivity_map.py index 02e8efa45ec..65a96cd9908 100644 --- a/examples/visualization/ssp_projs_sensitivity_map.py +++ b/examples/visualization/ssp_projs_sensitivity_map.py @@ -11,6 +11,7 @@ # Author: Alexandre Gramfort # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/examples/visualization/topo_compare_conditions.py b/examples/visualization/topo_compare_conditions.py index 208f5f16125..7572eab47e5 100644 --- a/examples/visualization/topo_compare_conditions.py +++ b/examples/visualization/topo_compare_conditions.py @@ -15,6 +15,7 @@ # Alexandre Gramfort # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/examples/visualization/topo_customized.py b/examples/visualization/topo_customized.py index 62052d65aa9..2d3c6662ebc 100644 --- a/examples/visualization/topo_customized.py +++ b/examples/visualization/topo_customized.py @@ -15,6 +15,7 @@ # Author: Denis A. Engemann # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/examples/visualization/xhemi.py b/examples/visualization/xhemi.py index 693d702629c..e0974a30a58 100644 --- a/examples/visualization/xhemi.py +++ b/examples/visualization/xhemi.py @@ -13,6 +13,7 @@ # Author: Christian Brodbeck # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/logo/generate_mne_logos.py b/logo/generate_mne_logos.py index 419dbe4279d..7b08325bf85 100644 --- a/logo/generate_mne_logos.py +++ b/logo/generate_mne_logos.py @@ -3,6 +3,7 @@ # @author: drmccloy # Created on Mon Jul 20 11:28:16 2015 # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import pathlib diff --git a/mne/__init__.py b/mne/__init__.py index d1c53c9eccf..594eddefdd2 100644 --- a/mne/__init__.py +++ b/mne/__init__.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. """MNE software for MEG and EEG data analysis.""" # PEP0440 compatible formatted version, see: # https://www.python.org/dev/peps/pep-0440/ diff --git a/mne/__main__.py b/mne/__main__.py index 5a3bfa5abb6..955c7af4936 100644 --- a/mne/__main__.py +++ b/mne/__main__.py @@ -1,5 +1,6 @@ # Authors: Eric Larson -# License: BSD Style. +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. from .commands.utils import main diff --git a/mne/_fiff/__init__.py b/mne/_fiff/__init__.py index 7171baf19d0..6402d78b325 100644 --- a/mne/_fiff/__init__.py +++ b/mne/_fiff/__init__.py @@ -3,6 +3,7 @@ # Authors: Eric Larson # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # All imports should be done directly to submodules, so we don't import # anything here or use lazy_loader. diff --git a/mne/_fiff/_digitization.py b/mne/_fiff/_digitization.py index b8cae6f3f92..dab0427ac6a 100644 --- a/mne/_fiff/_digitization.py +++ b/mne/_fiff/_digitization.py @@ -5,6 +5,7 @@ # Joan Massich # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import heapq from collections import Counter diff --git a/mne/_fiff/compensator.py b/mne/_fiff/compensator.py index 6b28c94d9ab..84a60f39614 100644 --- a/mne/_fiff/compensator.py +++ b/mne/_fiff/compensator.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np from ..utils import fill_doc diff --git a/mne/_fiff/constants.py b/mne/_fiff/constants.py index b3b4583677e..116b9662bc5 100644 --- a/mne/_fiff/constants.py +++ b/mne/_fiff/constants.py @@ -2,6 +2,7 @@ # Matti Hämäläinen # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from ..utils._bunch import BunchConstNamed diff --git a/mne/_fiff/ctf_comp.py b/mne/_fiff/ctf_comp.py index 6fc2aa90c0b..940ef02e848 100644 --- a/mne/_fiff/ctf_comp.py +++ b/mne/_fiff/ctf_comp.py @@ -3,6 +3,7 @@ # Denis Engemann # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from copy import deepcopy diff --git a/mne/_fiff/matrix.py b/mne/_fiff/matrix.py index 189f0dbf227..db5cafbfc11 100644 --- a/mne/_fiff/matrix.py +++ b/mne/_fiff/matrix.py @@ -2,6 +2,7 @@ # Matti Hämäläinen # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from ..utils import logger from .constants import FIFF diff --git a/mne/_fiff/meas_info.py b/mne/_fiff/meas_info.py index 8c2395be480..8630053e78e 100644 --- a/mne/_fiff/meas_info.py +++ b/mne/_fiff/meas_info.py @@ -4,6 +4,7 @@ # Stefan Appelhoff # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import contextlib import datetime diff --git a/mne/_fiff/open.py b/mne/_fiff/open.py index 66e9f40184a..d1794317772 100644 --- a/mne/_fiff/open.py +++ b/mne/_fiff/open.py @@ -2,6 +2,7 @@ # Matti Hämäläinen # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import os.path as op from gzip import GzipFile diff --git a/mne/_fiff/pick.py b/mne/_fiff/pick.py index 5328c7fbf37..4c5854f36fe 100644 --- a/mne/_fiff/pick.py +++ b/mne/_fiff/pick.py @@ -3,6 +3,7 @@ # Martin Luessi # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import re from copy import deepcopy diff --git a/mne/_fiff/proc_history.py b/mne/_fiff/proc_history.py index d0057778974..34203bfbb61 100644 --- a/mne/_fiff/proc_history.py +++ b/mne/_fiff/proc_history.py @@ -1,6 +1,7 @@ # Authors: Denis A. Engemann # Eric Larson -# License: Simplified BSD +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np diff --git a/mne/_fiff/proj.py b/mne/_fiff/proj.py index 46b8430a3d4..26bba36bc13 100644 --- a/mne/_fiff/proj.py +++ b/mne/_fiff/proj.py @@ -4,6 +4,7 @@ # Teon Brooks # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import re from copy import deepcopy diff --git a/mne/_fiff/reference.py b/mne/_fiff/reference.py index 0062bc4f40f..6bd422637bc 100644 --- a/mne/_fiff/reference.py +++ b/mne/_fiff/reference.py @@ -3,6 +3,7 @@ # Teon Brooks # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np diff --git a/mne/_fiff/tag.py b/mne/_fiff/tag.py index 11a983debbe..1b87d828619 100644 --- a/mne/_fiff/tag.py +++ b/mne/_fiff/tag.py @@ -2,6 +2,7 @@ # Matti Hämäläinen # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import html import re diff --git a/mne/_fiff/tests/__init__.py b/mne/_fiff/tests/__init__.py index ca22217d57a..f5523f5d662 100644 --- a/mne/_fiff/tests/__init__.py +++ b/mne/_fiff/tests/__init__.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import os.path as op data_dir = op.join(op.dirname(__file__), "data") diff --git a/mne/_fiff/tests/test_compensator.py b/mne/_fiff/tests/test_compensator.py index 88a7046feda..0a1b6f65fb3 100644 --- a/mne/_fiff/tests/test_compensator.py +++ b/mne/_fiff/tests/test_compensator.py @@ -1,6 +1,7 @@ # Author: Alexandre Gramfort # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from pathlib import Path diff --git a/mne/_fiff/tests/test_constants.py b/mne/_fiff/tests/test_constants.py index 8c05fffd249..3fc33513635 100644 --- a/mne/_fiff/tests/test_constants.py +++ b/mne/_fiff/tests/test_constants.py @@ -1,6 +1,7 @@ # Author: Eric Larson # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import os import re diff --git a/mne/_fiff/tests/test_meas_info.py b/mne/_fiff/tests/test_meas_info.py index bcb8b96f8c8..b8aa28b9e1d 100644 --- a/mne/_fiff/tests/test_meas_info.py +++ b/mne/_fiff/tests/test_meas_info.py @@ -2,6 +2,7 @@ # Stefan Appelhoff # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import pickle import string diff --git a/mne/_fiff/tests/test_pick.py b/mne/_fiff/tests/test_pick.py index fd2658f1da8..841ce2be9bd 100644 --- a/mne/_fiff/tests/test_pick.py +++ b/mne/_fiff/tests/test_pick.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. from copy import deepcopy from pathlib import Path diff --git a/mne/_fiff/tests/test_proc_history.py b/mne/_fiff/tests/test_proc_history.py index 580afdd204b..d63fafc1648 100644 --- a/mne/_fiff/tests/test_proc_history.py +++ b/mne/_fiff/tests/test_proc_history.py @@ -1,6 +1,7 @@ # Authors: Denis A. Engemann # Eric Larson -# License: Simplified BSD +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. from pathlib import Path diff --git a/mne/_fiff/tests/test_reference.py b/mne/_fiff/tests/test_reference.py index 3bd540779ed..d82338e5f63 100644 --- a/mne/_fiff/tests/test_reference.py +++ b/mne/_fiff/tests/test_reference.py @@ -3,6 +3,7 @@ # Teon Brooks # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import itertools from contextlib import nullcontext diff --git a/mne/_fiff/tests/test_show_fiff.py b/mne/_fiff/tests/test_show_fiff.py index f65e1abfa37..41fad7c22d5 100644 --- a/mne/_fiff/tests/test_show_fiff.py +++ b/mne/_fiff/tests/test_show_fiff.py @@ -1,6 +1,7 @@ # Author: Eric Larson # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from pathlib import Path diff --git a/mne/_fiff/tests/test_utils.py b/mne/_fiff/tests/test_utils.py index b85cc1f5689..c8cac9a6cae 100644 --- a/mne/_fiff/tests/test_utils.py +++ b/mne/_fiff/tests/test_utils.py @@ -2,6 +2,7 @@ # Author: Stefan Appelhoff # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from mne._fiff.utils import _check_orig_units diff --git a/mne/_fiff/tests/test_what.py b/mne/_fiff/tests/test_what.py index 4bba93b8807..f8d86b637f0 100644 --- a/mne/_fiff/tests/test_what.py +++ b/mne/_fiff/tests/test_what.py @@ -1,5 +1,6 @@ # Authors: Eric Larson -# License: BSD +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import glob from pathlib import Path diff --git a/mne/_fiff/tests/test_write.py b/mne/_fiff/tests/test_write.py index f009ebe3dc2..95639a4e0ba 100644 --- a/mne/_fiff/tests/test_write.py +++ b/mne/_fiff/tests/test_write.py @@ -2,6 +2,7 @@ # Author: Eric Larson # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import pytest diff --git a/mne/_fiff/tree.py b/mne/_fiff/tree.py index 3197bee1d88..6aa7b5f4539 100644 --- a/mne/_fiff/tree.py +++ b/mne/_fiff/tree.py @@ -2,6 +2,7 @@ # Matti Hämäläinen # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np diff --git a/mne/_fiff/utils.py b/mne/_fiff/utils.py index 5ecca1ddd53..cdda8784e8a 100644 --- a/mne/_fiff/utils.py +++ b/mne/_fiff/utils.py @@ -8,6 +8,7 @@ # Stefan Appelhoff # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import os import os.path as op diff --git a/mne/_fiff/what.py b/mne/_fiff/what.py index e007da77aae..9f0efa67453 100644 --- a/mne/_fiff/what.py +++ b/mne/_fiff/what.py @@ -1,6 +1,7 @@ # Authors: Eric Larson # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from collections import OrderedDict from inspect import signature diff --git a/mne/_fiff/write.py b/mne/_fiff/write.py index d8cc34e312b..3e6621d0069 100644 --- a/mne/_fiff/write.py +++ b/mne/_fiff/write.py @@ -2,6 +2,7 @@ # Matti Hämäläinen # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import os.path as op import re diff --git a/mne/_freesurfer.py b/mne/_freesurfer.py index fa5d65c0b95..6938e4f39fc 100644 --- a/mne/_freesurfer.py +++ b/mne/_freesurfer.py @@ -3,6 +3,7 @@ # Eric Larson # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import os.path as op from gzip import GzipFile diff --git a/mne/_ola.py b/mne/_ola.py index ba6c2a7b66c..f16ef5cc164 100644 --- a/mne/_ola.py +++ b/mne/_ola.py @@ -1,6 +1,7 @@ # Authors: Eric Larson # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np from scipy.signal import get_window diff --git a/mne/annotations.py b/mne/annotations.py index 83c209eec80..be62dac9dba 100644 --- a/mne/annotations.py +++ b/mne/annotations.py @@ -2,6 +2,7 @@ # Robert Luke # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import json import os.path as op diff --git a/mne/baseline.py b/mne/baseline.py index 1cb38e6c557..3994c5522e5 100644 --- a/mne/baseline.py +++ b/mne/baseline.py @@ -3,6 +3,7 @@ # Authors: Alexandre Gramfort # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np diff --git a/mne/beamformer/__init__.py b/mne/beamformer/__init__.py index 25c2156e7d0..cc00feae76f 100644 --- a/mne/beamformer/__init__.py +++ b/mne/beamformer/__init__.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. """Beamformers for source localization.""" import lazy_loader as lazy diff --git a/mne/beamformer/_compute_beamformer.py b/mne/beamformer/_compute_beamformer.py index a2c2e774d94..975f0852208 100644 --- a/mne/beamformer/_compute_beamformer.py +++ b/mne/beamformer/_compute_beamformer.py @@ -5,6 +5,7 @@ # Britta Westner # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from copy import deepcopy diff --git a/mne/beamformer/_dics.py b/mne/beamformer/_dics.py index a368b7fce0a..f1b8cc9f771 100644 --- a/mne/beamformer/_dics.py +++ b/mne/beamformer/_dics.py @@ -6,6 +6,7 @@ # Roman Goj # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np from .._fiff.pick import pick_channels, pick_info diff --git a/mne/beamformer/_lcmv.py b/mne/beamformer/_lcmv.py index d89fbc35342..a730795fff1 100644 --- a/mne/beamformer/_lcmv.py +++ b/mne/beamformer/_lcmv.py @@ -5,6 +5,7 @@ # Britta Westner # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np from .._fiff.meas_info import _simplify_info diff --git a/mne/beamformer/_rap_music.py b/mne/beamformer/_rap_music.py index 6ab538e6cce..317feff6ea3 100644 --- a/mne/beamformer/_rap_music.py +++ b/mne/beamformer/_rap_music.py @@ -4,6 +4,7 @@ # Alexandre Gramfort # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np from scipy import linalg diff --git a/mne/beamformer/resolution_matrix.py b/mne/beamformer/resolution_matrix.py index fe09b71769a..108fb7a4dbf 100644 --- a/mne/beamformer/resolution_matrix.py +++ b/mne/beamformer/resolution_matrix.py @@ -2,6 +2,7 @@ # Authors: olaf.hauk@mrc-cbu.cam.ac.uk # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np from .._fiff.pick import pick_channels, pick_channels_forward, pick_info diff --git a/mne/beamformer/tests/__init__.py b/mne/beamformer/tests/__init__.py index e69de29bb2d..bdfc6e7b46e 100644 --- a/mne/beamformer/tests/__init__.py +++ b/mne/beamformer/tests/__init__.py @@ -0,0 +1,2 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. diff --git a/mne/beamformer/tests/test_dics.py b/mne/beamformer/tests/test_dics.py index 60b5980adee..1daaaf17eb0 100644 --- a/mne/beamformer/tests/test_dics.py +++ b/mne/beamformer/tests/test_dics.py @@ -2,6 +2,7 @@ # Britta Westner # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import copy as cp diff --git a/mne/beamformer/tests/test_external.py b/mne/beamformer/tests/test_external.py index 7180385bb74..d159632594f 100644 --- a/mne/beamformer/tests/test_external.py +++ b/mne/beamformer/tests/test_external.py @@ -1,6 +1,7 @@ # Authors: Britta Westner # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np import pytest diff --git a/mne/beamformer/tests/test_lcmv.py b/mne/beamformer/tests/test_lcmv.py index 2cdcf800215..15c1a2ba5eb 100644 --- a/mne/beamformer/tests/test_lcmv.py +++ b/mne/beamformer/tests/test_lcmv.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. from contextlib import nullcontext from copy import deepcopy from inspect import signature diff --git a/mne/beamformer/tests/test_rap_music.py b/mne/beamformer/tests/test_rap_music.py index b85a9e3e8c7..c98c83a7722 100644 --- a/mne/beamformer/tests/test_rap_music.py +++ b/mne/beamformer/tests/test_rap_music.py @@ -2,6 +2,7 @@ # Alexandre Gramfort # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np import pytest diff --git a/mne/beamformer/tests/test_resolution_matrix.py b/mne/beamformer/tests/test_resolution_matrix.py index 67ed83004a7..5ce552cb1c9 100755 --- a/mne/beamformer/tests/test_resolution_matrix.py +++ b/mne/beamformer/tests/test_resolution_matrix.py @@ -1,6 +1,7 @@ # Authors: Olaf Hauk # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. """ Test computation of resolution matrix for LCMV beamformers. diff --git a/mne/bem.py b/mne/bem.py index eaa5b1f2d4f..a78309ef626 100644 --- a/mne/bem.py +++ b/mne/bem.py @@ -4,6 +4,7 @@ # Lorenzo De Santis # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # The computations in this code were primarily derived from Matti Hämäläinen's # C code. diff --git a/mne/channels/__init__.py b/mne/channels/__init__.py index 6c63c47525c..ddf274c39aa 100644 --- a/mne/channels/__init__.py +++ b/mne/channels/__init__.py @@ -2,6 +2,8 @@ Can be used for setting of sensor locations used for processing and plotting. """ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import lazy_loader as lazy (__getattr__, __dir__, __all__) = lazy.attach_stub(__name__, __file__) diff --git a/mne/channels/_dig_montage_utils.py b/mne/channels/_dig_montage_utils.py index 9998d94f65f..4d2e9e6af3f 100644 --- a/mne/channels/_dig_montage_utils.py +++ b/mne/channels/_dig_montage_utils.py @@ -9,7 +9,8 @@ # Stefan Appelhoff # Joan Massich # -# License: Simplified BSD +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np from defusedxml import ElementTree diff --git a/mne/channels/_standard_montage_utils.py b/mne/channels/_standard_montage_utils.py index a10a866fdfb..43c8fa6aecd 100644 --- a/mne/channels/_standard_montage_utils.py +++ b/mne/channels/_standard_montage_utils.py @@ -2,6 +2,7 @@ # Alexandre Gramfort # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import csv import os.path as op from collections import OrderedDict diff --git a/mne/channels/channels.py b/mne/channels/channels.py index 85484fa26f1..bc0f52cb56c 100644 --- a/mne/channels/channels.py +++ b/mne/channels/channels.py @@ -9,6 +9,7 @@ # Erica Peterson # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import os.path as op diff --git a/mne/channels/data/neighbors/__init__.py b/mne/channels/data/neighbors/__init__.py index b49a56bb334..d7a601ac8a8 100644 --- a/mne/channels/data/neighbors/__init__.py +++ b/mne/channels/data/neighbors/__init__.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. """Neighbor definitions for clustering permutation analysis.""" # This is a selection of files from http://fieldtrip.fcdonders.nl/template # Additional definitions can be obtained through the FieldTrip software. diff --git a/mne/channels/interpolation.py b/mne/channels/interpolation.py index 3b7eebac419..77e660901a9 100644 --- a/mne/channels/interpolation.py +++ b/mne/channels/interpolation.py @@ -2,6 +2,7 @@ # Ana Radanovic # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np from numpy.polynomial.legendre import legval diff --git a/mne/channels/layout.py b/mne/channels/layout.py index 10b341d36b4..a0f12cc594f 100644 --- a/mne/channels/layout.py +++ b/mne/channels/layout.py @@ -7,7 +7,8 @@ # Teon Brooks # Robert Luke # -# License: Simplified BSD +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import logging from collections import defaultdict diff --git a/mne/channels/montage.py b/mne/channels/montage.py index b442c905908..3d7ad340df8 100644 --- a/mne/channels/montage.py +++ b/mne/channels/montage.py @@ -9,7 +9,8 @@ # Stefan Appelhoff # Joan Massich # -# License: Simplified BSD +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import os.path as op import re diff --git a/mne/channels/tests/__init__.py b/mne/channels/tests/__init__.py index e69de29bb2d..bdfc6e7b46e 100644 --- a/mne/channels/tests/__init__.py +++ b/mne/channels/tests/__init__.py @@ -0,0 +1,2 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. diff --git a/mne/channels/tests/test_channels.py b/mne/channels/tests/test_channels.py index 2924aa2dfd9..c3bbcdb33dc 100644 --- a/mne/channels/tests/test_channels.py +++ b/mne/channels/tests/test_channels.py @@ -2,6 +2,7 @@ # Denis A. Engemann # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import hashlib from copy import deepcopy diff --git a/mne/channels/tests/test_interpolation.py b/mne/channels/tests/test_interpolation.py index 4f37494e652..9630607caae 100644 --- a/mne/channels/tests/test_interpolation.py +++ b/mne/channels/tests/test_interpolation.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. from itertools import compress from pathlib import Path diff --git a/mne/channels/tests/test_layout.py b/mne/channels/tests/test_layout.py index 97fc882a666..05caa37735b 100644 --- a/mne/channels/tests/test_layout.py +++ b/mne/channels/tests/test_layout.py @@ -3,7 +3,8 @@ # Martin Luessi # Eric Larson # -# License: Simplified BSD +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import copy from pathlib import Path diff --git a/mne/channels/tests/test_montage.py b/mne/channels/tests/test_montage.py index bd61a6338fb..f4da1e6932e 100644 --- a/mne/channels/tests/test_montage.py +++ b/mne/channels/tests/test_montage.py @@ -2,6 +2,7 @@ # Stefan Appelhoff # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import shutil from contextlib import nullcontext diff --git a/mne/channels/tests/test_standard_montage.py b/mne/channels/tests/test_standard_montage.py index d154a38b6cc..67b57c5cc75 100644 --- a/mne/channels/tests/test_standard_montage.py +++ b/mne/channels/tests/test_standard_montage.py @@ -2,6 +2,7 @@ # Alexandre Gramfort # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np diff --git a/mne/channels/tests/test_unify_bads.py b/mne/channels/tests/test_unify_bads.py index a502f5ffb92..f87c7e3ed8d 100644 --- a/mne/channels/tests/test_unify_bads.py +++ b/mne/channels/tests/test_unify_bads.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import pytest from mne.channels import unify_bad_channels diff --git a/mne/chpi.py b/mne/chpi.py index 939b22ac60e..7512f184eb1 100644 --- a/mne/chpi.py +++ b/mne/chpi.py @@ -17,6 +17,7 @@ # Authors: Eric Larson # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import copy import itertools diff --git a/mne/commands/__init__.py b/mne/commands/__init__.py index 11b3610605d..e331bc0a1cc 100644 --- a/mne/commands/__init__.py +++ b/mne/commands/__init__.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. """Command-line utilities.""" import lazy_loader as lazy diff --git a/mne/commands/mne_anonymize.py b/mne/commands/mne_anonymize.py index 65bb0c9e4f5..3c0a7ebfd27 100644 --- a/mne/commands/mne_anonymize.py +++ b/mne/commands/mne_anonymize.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # Authors : Dominik Krzeminski # Luke Bloy +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. """Anonymize raw fif file. diff --git a/mne/commands/mne_browse_raw.py b/mne/commands/mne_browse_raw.py index c3a3d144e33..17a8f8bcb88 100644 --- a/mne/commands/mne_browse_raw.py +++ b/mne/commands/mne_browse_raw.py @@ -14,6 +14,8 @@ """ # Authors : Eric Larson, PhD +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import sys diff --git a/mne/commands/mne_bti2fiff.py b/mne/commands/mne_bti2fiff.py index 88510626822..c8664ca5a35 100644 --- a/mne/commands/mne_bti2fiff.py +++ b/mne/commands/mne_bti2fiff.py @@ -27,6 +27,8 @@ # Yuval Harpaz # # simplified bsd-3 license +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import sys diff --git a/mne/commands/mne_clean_eog_ecg.py b/mne/commands/mne_clean_eog_ecg.py index b1ffaa74edd..8f18f16f6cb 100644 --- a/mne/commands/mne_clean_eog_ecg.py +++ b/mne/commands/mne_clean_eog_ecg.py @@ -11,6 +11,8 @@ # Authors : Dr Engr. Sheraz Khan, P.Eng, Ph.D. # Engr. Nandita Shetty, MS. # Alexandre Gramfort, Ph.D. +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import sys diff --git a/mne/commands/mne_compare_fiff.py b/mne/commands/mne_compare_fiff.py index d38e95761b0..89aad23cb5c 100644 --- a/mne/commands/mne_compare_fiff.py +++ b/mne/commands/mne_compare_fiff.py @@ -10,6 +10,8 @@ """ # Authors : Eric Larson, PhD +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import sys diff --git a/mne/commands/mne_compute_proj_ecg.py b/mne/commands/mne_compute_proj_ecg.py index 1e8b6aab973..f5f798a4968 100644 --- a/mne/commands/mne_compute_proj_ecg.py +++ b/mne/commands/mne_compute_proj_ecg.py @@ -12,6 +12,8 @@ """ # Authors : Alexandre Gramfort, Ph.D. # Martin Luessi, Ph.D. +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import os import sys diff --git a/mne/commands/mne_compute_proj_eog.py b/mne/commands/mne_compute_proj_eog.py index 4b3dcae75dd..456bf3b6080 100644 --- a/mne/commands/mne_compute_proj_eog.py +++ b/mne/commands/mne_compute_proj_eog.py @@ -22,6 +22,8 @@ """ # Authors : Alexandre Gramfort, Ph.D. # Martin Luessi, Ph.D. +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import os import sys diff --git a/mne/commands/mne_coreg.py b/mne/commands/mne_coreg.py index ed440e987e3..b32e8b9e3d7 100644 --- a/mne/commands/mne_coreg.py +++ b/mne/commands/mne_coreg.py @@ -1,5 +1,7 @@ #!/usr/bin/env python # Authors: Christian Brodbeck +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. """Open the coregistration GUI. diff --git a/mne/commands/mne_flash_bem.py b/mne/commands/mne_flash_bem.py index 8ffaf57b816..24923bd78a1 100644 --- a/mne/commands/mne_flash_bem.py +++ b/mne/commands/mne_flash_bem.py @@ -24,6 +24,8 @@ """ # noqa E501 # Authors: Lorenzo De Santis # Alexandre Gramfort +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import mne from mne.bem import convert_flash_mris, make_flash_bem diff --git a/mne/commands/mne_freeview_bem_surfaces.py b/mne/commands/mne_freeview_bem_surfaces.py index b428cd3f7d0..7f1c6491ba1 100644 --- a/mne/commands/mne_freeview_bem_surfaces.py +++ b/mne/commands/mne_freeview_bem_surfaces.py @@ -9,6 +9,8 @@ """ # Authors: Alexandre Gramfort +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import os import os.path as op diff --git a/mne/commands/mne_kit2fiff.py b/mne/commands/mne_kit2fiff.py index 0c6b4545203..a3a294d8312 100644 --- a/mne/commands/mne_kit2fiff.py +++ b/mne/commands/mne_kit2fiff.py @@ -1,5 +1,7 @@ #!/usr/bin/env python # Authors: Teon Brooks +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. """Import KIT / NYU data to fif file. diff --git a/mne/commands/mne_make_scalp_surfaces.py b/mne/commands/mne_make_scalp_surfaces.py index dffb97fd591..91ed2fdae60 100644 --- a/mne/commands/mne_make_scalp_surfaces.py +++ b/mne/commands/mne_make_scalp_surfaces.py @@ -5,6 +5,8 @@ # Matti Hämäläinen # # simplified bsd-3 license +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. """Create high-resolution head surfaces for coordinate alignment. diff --git a/mne/commands/mne_maxfilter.py b/mne/commands/mne_maxfilter.py index c5f83b9176f..5c631dcf457 100644 --- a/mne/commands/mne_maxfilter.py +++ b/mne/commands/mne_maxfilter.py @@ -13,6 +13,8 @@ """ # Authors : Martin Luessi +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import os import sys diff --git a/mne/commands/mne_prepare_bem_model.py b/mne/commands/mne_prepare_bem_model.py index cc8620d0ecd..1c1ae874355 100644 --- a/mne/commands/mne_prepare_bem_model.py +++ b/mne/commands/mne_prepare_bem_model.py @@ -1,4 +1,6 @@ #!/usr/bin/env python +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. """Create a BEM solution using the linear collocation approach. Examples diff --git a/mne/commands/mne_report.py b/mne/commands/mne_report.py index 4b5dd54b30f..bf7010cc8a3 100644 --- a/mne/commands/mne_report.py +++ b/mne/commands/mne_report.py @@ -1,4 +1,6 @@ #!/usr/bin/env python +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. r"""Create mne report for a folder. Examples diff --git a/mne/commands/mne_setup_forward_model.py b/mne/commands/mne_setup_forward_model.py index c06b725a2d6..f5dda2d061c 100644 --- a/mne/commands/mne_setup_forward_model.py +++ b/mne/commands/mne_setup_forward_model.py @@ -1,4 +1,6 @@ #!/usr/bin/env python +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. """Create a BEM model for a subject. Examples diff --git a/mne/commands/mne_setup_source_space.py b/mne/commands/mne_setup_source_space.py index 963a27fca07..b5654ecab7f 100644 --- a/mne/commands/mne_setup_source_space.py +++ b/mne/commands/mne_setup_source_space.py @@ -1,4 +1,6 @@ #!/usr/bin/env python +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. """Set up bilateral hemisphere surface-based source space with subsampling. Examples diff --git a/mne/commands/mne_show_fiff.py b/mne/commands/mne_show_fiff.py index 128e8fee55f..de1623393f0 100644 --- a/mne/commands/mne_show_fiff.py +++ b/mne/commands/mne_show_fiff.py @@ -17,6 +17,8 @@ """ # Authors : Eric Larson, PhD +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import sys diff --git a/mne/commands/mne_show_info.py b/mne/commands/mne_show_info.py index 80f546f6d80..d81e5c8f2a6 100644 --- a/mne/commands/mne_show_info.py +++ b/mne/commands/mne_show_info.py @@ -10,6 +10,8 @@ """ # Authors : Alexandre Gramfort, Ph.D. +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import sys diff --git a/mne/commands/mne_surf2bem.py b/mne/commands/mne_surf2bem.py index 93a154b2477..18a09a6402d 100644 --- a/mne/commands/mne_surf2bem.py +++ b/mne/commands/mne_surf2bem.py @@ -13,6 +13,7 @@ # Authors: Alexandre Gramfort # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import sys diff --git a/mne/commands/mne_sys_info.py b/mne/commands/mne_sys_info.py index ae355c2cef5..b216cb8db3a 100644 --- a/mne/commands/mne_sys_info.py +++ b/mne/commands/mne_sys_info.py @@ -10,6 +10,8 @@ """ # Authors : Eric Larson +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import sys diff --git a/mne/commands/mne_watershed_bem.py b/mne/commands/mne_watershed_bem.py index 279cd826adc..23c7e3ebbe5 100644 --- a/mne/commands/mne_watershed_bem.py +++ b/mne/commands/mne_watershed_bem.py @@ -1,5 +1,7 @@ #!/usr/bin/env python # Authors: Lorenzo De Santis +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. """Create BEM surfaces using the watershed algorithm included with FreeSurfer. Examples diff --git a/mne/commands/mne_what.py b/mne/commands/mne_what.py index ab4a9d5ea8f..c899887048d 100644 --- a/mne/commands/mne_what.py +++ b/mne/commands/mne_what.py @@ -10,6 +10,8 @@ """ # Authors : Eric Larson, PhD +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import mne diff --git a/mne/commands/tests/__init__.py b/mne/commands/tests/__init__.py index e69de29bb2d..bdfc6e7b46e 100644 --- a/mne/commands/tests/__init__.py +++ b/mne/commands/tests/__init__.py @@ -0,0 +1,2 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. diff --git a/mne/commands/tests/test_commands.py b/mne/commands/tests/test_commands.py index b2a93abfa96..ae5e84cbd58 100644 --- a/mne/commands/tests/test_commands.py +++ b/mne/commands/tests/test_commands.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import glob import os import shutil diff --git a/mne/commands/utils.py b/mne/commands/utils.py index 68ede4e807f..10334ce0acb 100644 --- a/mne/commands/utils.py +++ b/mne/commands/utils.py @@ -4,6 +4,7 @@ # Stefan Appelhoff # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import glob import importlib diff --git a/mne/conftest.py b/mne/conftest.py index 33396621890..40a317b7da9 100644 --- a/mne/conftest.py +++ b/mne/conftest.py @@ -1,6 +1,7 @@ # Author: Eric Larson # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import gc import inspect diff --git a/mne/coreg.py b/mne/coreg.py index 7591eff1322..fe6895270b7 100644 --- a/mne/coreg.py +++ b/mne/coreg.py @@ -3,6 +3,7 @@ # Authors: Christian Brodbeck # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import configparser import fnmatch diff --git a/mne/cov.py b/mne/cov.py index 64d82492023..1b2d4cd8ebe 100644 --- a/mne/cov.py +++ b/mne/cov.py @@ -3,6 +3,7 @@ # Denis A. Engemann # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import itertools as itt from copy import deepcopy diff --git a/mne/cuda.py b/mne/cuda.py index 410a81fd36e..b4aa7c37bf3 100644 --- a/mne/cuda.py +++ b/mne/cuda.py @@ -1,6 +1,7 @@ # Authors: Eric Larson # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np from scipy.fft import irfft, rfft diff --git a/mne/data/__init__.py b/mne/data/__init__.py index 6f92b469cf7..4f61734f57d 100644 --- a/mne/data/__init__.py +++ b/mne/data/__init__.py @@ -1 +1,3 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. """MNE-Python data.""" diff --git a/mne/datasets/__init__.py b/mne/datasets/__init__.py index 5c15b0c69d7..3cc4db8cfc0 100644 --- a/mne/datasets/__init__.py +++ b/mne/datasets/__init__.py @@ -2,6 +2,8 @@ See :ref:`datasets` for more information. """ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import lazy_loader as lazy (__getattr__, __dir__, __all__) = lazy.attach_stub(__name__, __file__) diff --git a/mne/datasets/_fake/__init__.py b/mne/datasets/_fake/__init__.py index 57b8d214c70..511caee1aee 100644 --- a/mne/datasets/_fake/__init__.py +++ b/mne/datasets/_fake/__init__.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. """Fake dataset for testing.""" from ._fake import data_path, get_version diff --git a/mne/datasets/_fake/_fake.py b/mne/datasets/_fake/_fake.py index 475b7aeb640..155c636f9fc 100644 --- a/mne/datasets/_fake/_fake.py +++ b/mne/datasets/_fake/_fake.py @@ -1,7 +1,8 @@ # Authors: Alexandre Gramfort # Martin Luessi # Eric Larson -# License: BSD Style. +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. from ...utils import verbose from ..utils import _data_path_doc, _download_mne_dataset, _get_version, _version_doc diff --git a/mne/datasets/_fetch.py b/mne/datasets/_fetch.py index 48fb609d81a..82d68d6e9f6 100644 --- a/mne/datasets/_fetch.py +++ b/mne/datasets/_fetch.py @@ -1,6 +1,7 @@ # Authors: Adam Li # -# License: BSD Style. +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import os import os.path as op diff --git a/mne/datasets/_fsaverage/__init__.py b/mne/datasets/_fsaverage/__init__.py index e69de29bb2d..bdfc6e7b46e 100644 --- a/mne/datasets/_fsaverage/__init__.py +++ b/mne/datasets/_fsaverage/__init__.py @@ -0,0 +1,2 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. diff --git a/mne/datasets/_fsaverage/base.py b/mne/datasets/_fsaverage/base.py index 934f76ca520..2360bcfb60c 100644 --- a/mne/datasets/_fsaverage/base.py +++ b/mne/datasets/_fsaverage/base.py @@ -1,5 +1,6 @@ # Authors: Eric Larson -# License: BSD Style. +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import os import os.path as op diff --git a/mne/datasets/_infant/base.py b/mne/datasets/_infant/base.py index e709b740691..5a57bb4f87c 100644 --- a/mne/datasets/_infant/base.py +++ b/mne/datasets/_infant/base.py @@ -1,5 +1,6 @@ # Authors: Eric Larson -# License: BSD Style. +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import os import os.path as op diff --git a/mne/datasets/_phantom/__init__.py b/mne/datasets/_phantom/__init__.py index e69de29bb2d..bdfc6e7b46e 100644 --- a/mne/datasets/_phantom/__init__.py +++ b/mne/datasets/_phantom/__init__.py @@ -0,0 +1,2 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. diff --git a/mne/datasets/_phantom/base.py b/mne/datasets/_phantom/base.py index 19ab1db89ee..98eaf1cc9bd 100644 --- a/mne/datasets/_phantom/base.py +++ b/mne/datasets/_phantom/base.py @@ -1,5 +1,6 @@ # Authors: Eric Larson -# License: BSD Style. +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import os import os.path as op diff --git a/mne/datasets/brainstorm/__init__.py b/mne/datasets/brainstorm/__init__.py index e97790f52c6..d9b73e48df3 100644 --- a/mne/datasets/brainstorm/__init__.py +++ b/mne/datasets/brainstorm/__init__.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. """Brainstorm datasets.""" from . import bst_raw, bst_resting, bst_auditory, bst_phantom_ctf, bst_phantom_elekta diff --git a/mne/datasets/brainstorm/bst_auditory.py b/mne/datasets/brainstorm/bst_auditory.py index 6dbbe45407b..b5b0ba0509c 100644 --- a/mne/datasets/brainstorm/bst_auditory.py +++ b/mne/datasets/brainstorm/bst_auditory.py @@ -1,6 +1,7 @@ # Authors: Mainak Jas # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from ...utils import verbose from ..utils import ( _data_path_doc_accept, diff --git a/mne/datasets/brainstorm/bst_phantom_ctf.py b/mne/datasets/brainstorm/bst_phantom_ctf.py index c73e8798623..b522a60a132 100644 --- a/mne/datasets/brainstorm/bst_phantom_ctf.py +++ b/mne/datasets/brainstorm/bst_phantom_ctf.py @@ -1,6 +1,7 @@ # Authors: Eric Larson # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from ...utils import verbose from ..utils import ( _data_path_doc_accept, diff --git a/mne/datasets/brainstorm/bst_phantom_elekta.py b/mne/datasets/brainstorm/bst_phantom_elekta.py index b30770bbe34..2bafc2e98ef 100644 --- a/mne/datasets/brainstorm/bst_phantom_elekta.py +++ b/mne/datasets/brainstorm/bst_phantom_elekta.py @@ -1,6 +1,7 @@ # Authors: Eric Larson # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from ...utils import verbose from ..utils import ( _data_path_doc_accept, diff --git a/mne/datasets/brainstorm/bst_raw.py b/mne/datasets/brainstorm/bst_raw.py index 3c9ea2ae965..7d35a89ce5f 100644 --- a/mne/datasets/brainstorm/bst_raw.py +++ b/mne/datasets/brainstorm/bst_raw.py @@ -1,6 +1,7 @@ # Authors: Mainak Jas # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from functools import partial from ...utils import get_config, verbose diff --git a/mne/datasets/brainstorm/bst_resting.py b/mne/datasets/brainstorm/bst_resting.py index efe86c0ce69..59aabea690b 100644 --- a/mne/datasets/brainstorm/bst_resting.py +++ b/mne/datasets/brainstorm/bst_resting.py @@ -1,6 +1,7 @@ # Authors: Mainak Jas # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from ...utils import verbose from ..utils import ( _data_path_doc_accept, diff --git a/mne/datasets/config.py b/mne/datasets/config.py index 88444be352a..6778a1e7cc9 100644 --- a/mne/datasets/config.py +++ b/mne/datasets/config.py @@ -1,7 +1,8 @@ # Authors: Adam Li # Daniel McCloy # -# License: BSD Style. +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. _bst_license_text = """ diff --git a/mne/datasets/eegbci/__init__.py b/mne/datasets/eegbci/__init__.py index 7be4fbc2858..98e79db1c80 100644 --- a/mne/datasets/eegbci/__init__.py +++ b/mne/datasets/eegbci/__init__.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. """EEG Motor Movement/Imagery Dataset.""" from .eegbci import data_path, load_data, standardize diff --git a/mne/datasets/eegbci/eegbci.py b/mne/datasets/eegbci/eegbci.py index 00f7fff4767..3af5661e5f7 100644 --- a/mne/datasets/eegbci/eegbci.py +++ b/mne/datasets/eegbci/eegbci.py @@ -1,7 +1,8 @@ # Author: Martin Billinger # Adam Li # Daniel McCloy -# License: BSD Style. +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import os import re diff --git a/mne/datasets/eegbci/tests/test_eegbci.py b/mne/datasets/eegbci/tests/test_eegbci.py index c59c6802ede..9912587c836 100644 --- a/mne/datasets/eegbci/tests/test_eegbci.py +++ b/mne/datasets/eegbci/tests/test_eegbci.py @@ -1,6 +1,7 @@ # Authors: Eric Larson # -# License: BSD Style. +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. from mne.datasets import eegbci diff --git a/mne/datasets/epilepsy_ecog/__init__.py b/mne/datasets/epilepsy_ecog/__init__.py index 10982c2f504..76d7426d07b 100644 --- a/mne/datasets/epilepsy_ecog/__init__.py +++ b/mne/datasets/epilepsy_ecog/__init__.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. """Clinical epilepsy datasets.""" from ._data import data_path, get_version diff --git a/mne/datasets/epilepsy_ecog/_data.py b/mne/datasets/epilepsy_ecog/_data.py index 8192a7bf369..c3f1863994b 100644 --- a/mne/datasets/epilepsy_ecog/_data.py +++ b/mne/datasets/epilepsy_ecog/_data.py @@ -1,6 +1,7 @@ # Authors: Adam Li # Alex Rockhill -# License: BSD Style. +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. from ...utils import verbose from ..utils import _data_path_doc, _download_mne_dataset, _get_version, _version_doc diff --git a/mne/datasets/erp_core/__init__.py b/mne/datasets/erp_core/__init__.py index 9e2588347da..55164bee284 100644 --- a/mne/datasets/erp_core/__init__.py +++ b/mne/datasets/erp_core/__init__.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. """ERP-CORE EEG dataset.""" from .erp_core import data_path, get_version diff --git a/mne/datasets/erp_core/erp_core.py b/mne/datasets/erp_core/erp_core.py index 843777412bc..5a9ec38e3b6 100644 --- a/mne/datasets/erp_core/erp_core.py +++ b/mne/datasets/erp_core/erp_core.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. from ...utils import verbose from ..utils import _data_path_doc, _download_mne_dataset, _get_version, _version_doc diff --git a/mne/datasets/eyelink/__init__.py b/mne/datasets/eyelink/__init__.py index 85931aba72d..686881d34dc 100644 --- a/mne/datasets/eyelink/__init__.py +++ b/mne/datasets/eyelink/__init__.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. """Eyelink test dataset.""" from .eyelink import data_path, get_version diff --git a/mne/datasets/eyelink/eyelink.py b/mne/datasets/eyelink/eyelink.py index 596f0f79e47..1d99596a28e 100644 --- a/mne/datasets/eyelink/eyelink.py +++ b/mne/datasets/eyelink/eyelink.py @@ -1,5 +1,6 @@ # Authors: Dominik Welke -# License: BSD Style. +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. from ...utils import verbose from ..utils import _data_path_doc, _download_mne_dataset, _get_version, _version_doc diff --git a/mne/datasets/fieldtrip_cmc/__init__.py b/mne/datasets/fieldtrip_cmc/__init__.py index 328d81ff5ba..41aa6c4696b 100644 --- a/mne/datasets/fieldtrip_cmc/__init__.py +++ b/mne/datasets/fieldtrip_cmc/__init__.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. """fieldtrip Cortico-Muscular Coherence (CMC) Dataset.""" from .fieldtrip_cmc import data_path, get_version diff --git a/mne/datasets/fieldtrip_cmc/fieldtrip_cmc.py b/mne/datasets/fieldtrip_cmc/fieldtrip_cmc.py index c825eafef05..9fc0a3eca3a 100644 --- a/mne/datasets/fieldtrip_cmc/fieldtrip_cmc.py +++ b/mne/datasets/fieldtrip_cmc/fieldtrip_cmc.py @@ -1,7 +1,8 @@ # Authors: Chris Holdgraf # Alexandre Barachant # -# License: BSD Style. +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. from ...utils import verbose from ..utils import _data_path_doc, _download_mne_dataset, _get_version, _version_doc diff --git a/mne/datasets/fnirs_motor/__init__.py b/mne/datasets/fnirs_motor/__init__.py index 66ec175ef1b..3ce6e67c508 100644 --- a/mne/datasets/fnirs_motor/__init__.py +++ b/mne/datasets/fnirs_motor/__init__.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. """fNIRS motor dataset.""" from .fnirs_motor import data_path, get_version diff --git a/mne/datasets/fnirs_motor/fnirs_motor.py b/mne/datasets/fnirs_motor/fnirs_motor.py index 9ae0844dacd..81a8dd0de7b 100644 --- a/mne/datasets/fnirs_motor/fnirs_motor.py +++ b/mne/datasets/fnirs_motor/fnirs_motor.py @@ -1,5 +1,6 @@ # Authors: Eric Larson -# License: BSD Style. +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. from ...utils import verbose from ..utils import _data_path_doc, _download_mne_dataset, _get_version, _version_doc diff --git a/mne/datasets/hf_sef/__init__.py b/mne/datasets/hf_sef/__init__.py index 08fe8ca5651..abb6e43b341 100644 --- a/mne/datasets/hf_sef/__init__.py +++ b/mne/datasets/hf_sef/__init__.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. """HF-SEF dataset.""" from .hf_sef import data_path diff --git a/mne/datasets/hf_sef/hf_sef.py b/mne/datasets/hf_sef/hf_sef.py index dc1586cac9f..1e07a66522b 100644 --- a/mne/datasets/hf_sef/hf_sef.py +++ b/mne/datasets/hf_sef/hf_sef.py @@ -1,6 +1,7 @@ #!/usr/bin/env python2 # Authors: Jussi Nurminen -# License: BSD Style. +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import os diff --git a/mne/datasets/kiloword/__init__.py b/mne/datasets/kiloword/__init__.py index 18a22f9ecd1..783cac5eb1f 100644 --- a/mne/datasets/kiloword/__init__.py +++ b/mne/datasets/kiloword/__init__.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. """MNE visual_92_categories dataset.""" from .kiloword import data_path, get_version diff --git a/mne/datasets/kiloword/kiloword.py b/mne/datasets/kiloword/kiloword.py index 67014bcadb9..794bf7758c2 100644 --- a/mne/datasets/kiloword/kiloword.py +++ b/mne/datasets/kiloword/kiloword.py @@ -1,4 +1,5 @@ -# License: BSD Style. +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. from ...utils import verbose from ..utils import _download_mne_dataset, _get_version, _version_doc diff --git a/mne/datasets/limo/__init__.py b/mne/datasets/limo/__init__.py index f83eac67505..abf7aa14289 100644 --- a/mne/datasets/limo/__init__.py +++ b/mne/datasets/limo/__init__.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. """LIMO Dataset.""" from .limo import data_path, load_data diff --git a/mne/datasets/limo/limo.py b/mne/datasets/limo/limo.py index 9c5b3861b88..db1d8958169 100644 --- a/mne/datasets/limo/limo.py +++ b/mne/datasets/limo/limo.py @@ -1,6 +1,7 @@ # Authors: Jose C. Garcia Alanis # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import os.path as op import time diff --git a/mne/datasets/misc/__init__.py b/mne/datasets/misc/__init__.py index 884848d4817..d7a473559a7 100644 --- a/mne/datasets/misc/__init__.py +++ b/mne/datasets/misc/__init__.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. """MNE misc dataset.""" from ._misc import data_path, _pytest_mark diff --git a/mne/datasets/misc/_misc.py b/mne/datasets/misc/_misc.py index 65f873af635..152ad7dda47 100644 --- a/mne/datasets/misc/_misc.py +++ b/mne/datasets/misc/_misc.py @@ -1,7 +1,8 @@ # Authors: Alexandre Gramfort # Martin Luessi # Eric Larson -# License: BSD Style. +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. from ...utils import verbose from ..utils import _data_path_doc, _download_mne_dataset, has_dataset diff --git a/mne/datasets/mtrf/__init__.py b/mne/datasets/mtrf/__init__.py index dffa76e0230..33d5d76e150 100644 --- a/mne/datasets/mtrf/__init__.py +++ b/mne/datasets/mtrf/__init__.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. """mTRF Dataset.""" from .mtrf import data_path, get_version diff --git a/mne/datasets/mtrf/mtrf.py b/mne/datasets/mtrf/mtrf.py index fdcb6bf9c3b..b5e404a58f3 100644 --- a/mne/datasets/mtrf/mtrf.py +++ b/mne/datasets/mtrf/mtrf.py @@ -1,6 +1,7 @@ # Authors: Chris Holdgraf # -# License: BSD Style. +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. from ...utils import verbose from ..utils import _data_path_doc, _download_mne_dataset, _get_version, _version_doc diff --git a/mne/datasets/multimodal/__init__.py b/mne/datasets/multimodal/__init__.py index 753f0cf952c..ba6c658ab0e 100644 --- a/mne/datasets/multimodal/__init__.py +++ b/mne/datasets/multimodal/__init__.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. """Multimodal dataset.""" from .multimodal import data_path, get_version diff --git a/mne/datasets/multimodal/multimodal.py b/mne/datasets/multimodal/multimodal.py index a1789e30623..1cf58def1cc 100644 --- a/mne/datasets/multimodal/multimodal.py +++ b/mne/datasets/multimodal/multimodal.py @@ -1,7 +1,8 @@ # Authors: Alexandre Gramfort # Martin Luessi # Eric Larson -# License: BSD Style. +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. from ...utils import verbose from ..utils import _data_path_doc, _download_mne_dataset, _get_version, _version_doc diff --git a/mne/datasets/opm/__init__.py b/mne/datasets/opm/__init__.py index 6ff15e6868c..e70077c7086 100644 --- a/mne/datasets/opm/__init__.py +++ b/mne/datasets/opm/__init__.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. """OPM dataset.""" from .opm import data_path, get_version diff --git a/mne/datasets/opm/opm.py b/mne/datasets/opm/opm.py index b21ed9ffd1c..a7047e0ebdd 100644 --- a/mne/datasets/opm/opm.py +++ b/mne/datasets/opm/opm.py @@ -1,7 +1,8 @@ # Authors: Alexandre Gramfort # Martin Luessi # Eric Larson -# License: BSD Style. +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. from ...utils import verbose from ..utils import _data_path_doc, _download_mne_dataset, _get_version, _version_doc diff --git a/mne/datasets/phantom_4dbti/__init__.py b/mne/datasets/phantom_4dbti/__init__.py index 0d9323adc06..fc36bbede64 100644 --- a/mne/datasets/phantom_4dbti/__init__.py +++ b/mne/datasets/phantom_4dbti/__init__.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. """Multimodal dataset.""" from .phantom_4dbti import data_path, get_version diff --git a/mne/datasets/phantom_4dbti/phantom_4dbti.py b/mne/datasets/phantom_4dbti/phantom_4dbti.py index 6963b72fb09..96bd04d0f8e 100644 --- a/mne/datasets/phantom_4dbti/phantom_4dbti.py +++ b/mne/datasets/phantom_4dbti/phantom_4dbti.py @@ -1,6 +1,7 @@ # Authors: Alexandre Gramfort # -# License: BSD Style. +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. from ...utils import verbose from ..utils import _data_path_doc, _download_mne_dataset, _get_version, _version_doc diff --git a/mne/datasets/phantom_kernel/__init__.py b/mne/datasets/phantom_kernel/__init__.py index 7bab3332b62..ca49adb6f45 100644 --- a/mne/datasets/phantom_kernel/__init__.py +++ b/mne/datasets/phantom_kernel/__init__.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. """Multimodal dataset.""" from .phantom_kernel import data_path, get_version diff --git a/mne/datasets/phantom_kernel/phantom_kernel.py b/mne/datasets/phantom_kernel/phantom_kernel.py index b9c8cdaafb8..a83da820e08 100644 --- a/mne/datasets/phantom_kernel/phantom_kernel.py +++ b/mne/datasets/phantom_kernel/phantom_kernel.py @@ -1,6 +1,7 @@ # Authors: Eric Larson # -# License: BSD Style. +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. from ...utils import verbose from ..utils import _data_path_doc, _download_mne_dataset, _get_version, _version_doc diff --git a/mne/datasets/phantom_kit/__init__.py b/mne/datasets/phantom_kit/__init__.py index 6efd7be9e15..684cf36a4c8 100644 --- a/mne/datasets/phantom_kit/__init__.py +++ b/mne/datasets/phantom_kit/__init__.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. """KIT phantom dataset.""" from .phantom_kit import data_path, get_version diff --git a/mne/datasets/phantom_kit/phantom_kit.py b/mne/datasets/phantom_kit/phantom_kit.py index e6d3275ccef..a4eac6c4a50 100644 --- a/mne/datasets/phantom_kit/phantom_kit.py +++ b/mne/datasets/phantom_kit/phantom_kit.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. from ...utils import verbose from ..utils import _data_path_doc, _download_mne_dataset, _get_version, _version_doc diff --git a/mne/datasets/refmeg_noise/__init__.py b/mne/datasets/refmeg_noise/__init__.py index 00460d173bb..38d4c5f5518 100644 --- a/mne/datasets/refmeg_noise/__init__.py +++ b/mne/datasets/refmeg_noise/__init__.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. """MEG reference-noise data set.""" from .refmeg_noise import data_path, get_version diff --git a/mne/datasets/refmeg_noise/refmeg_noise.py b/mne/datasets/refmeg_noise/refmeg_noise.py index 5c27a469a2d..69b1cc47f4b 100644 --- a/mne/datasets/refmeg_noise/refmeg_noise.py +++ b/mne/datasets/refmeg_noise/refmeg_noise.py @@ -1,5 +1,6 @@ # Authors: Jeff Hanna -# License: BSD Style. +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. from ...utils import verbose from ..utils import _data_path_doc, _download_mne_dataset, _get_version, _version_doc diff --git a/mne/datasets/sample/__init__.py b/mne/datasets/sample/__init__.py index c94c6d50a7f..0e6f9e94b15 100644 --- a/mne/datasets/sample/__init__.py +++ b/mne/datasets/sample/__init__.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. """MNE sample dataset.""" from .sample import data_path, get_version diff --git a/mne/datasets/sample/sample.py b/mne/datasets/sample/sample.py index 06118fa8409..da83c4f0e7e 100644 --- a/mne/datasets/sample/sample.py +++ b/mne/datasets/sample/sample.py @@ -1,7 +1,8 @@ # Authors: Alexandre Gramfort # Martin Luessi # Eric Larson -# License: BSD Style. +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. from ...utils import verbose from ..utils import _data_path_doc, _download_mne_dataset, _get_version, _version_doc diff --git a/mne/datasets/sleep_physionet/__init__.py b/mne/datasets/sleep_physionet/__init__.py index 04536a93134..9736f45961e 100644 --- a/mne/datasets/sleep_physionet/__init__.py +++ b/mne/datasets/sleep_physionet/__init__.py @@ -1 +1,3 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. from . import age, temazepam, _utils diff --git a/mne/datasets/sleep_physionet/_utils.py b/mne/datasets/sleep_physionet/_utils.py index ff89155ece7..db9982e8c71 100644 --- a/mne/datasets/sleep_physionet/_utils.py +++ b/mne/datasets/sleep_physionet/_utils.py @@ -1,7 +1,8 @@ # Authors: Alexandre Gramfort # Joan Massich # -# License: BSD Style. +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import os import os.path as op diff --git a/mne/datasets/sleep_physionet/age.py b/mne/datasets/sleep_physionet/age.py index bdc98f1be9e..29afe9d9562 100644 --- a/mne/datasets/sleep_physionet/age.py +++ b/mne/datasets/sleep_physionet/age.py @@ -1,7 +1,8 @@ # Authors: Alexandre Gramfort # Joan Massich # -# License: BSD Style. +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import os import time diff --git a/mne/datasets/sleep_physionet/temazepam.py b/mne/datasets/sleep_physionet/temazepam.py index a20cde1e366..b3b02c6bd85 100644 --- a/mne/datasets/sleep_physionet/temazepam.py +++ b/mne/datasets/sleep_physionet/temazepam.py @@ -1,7 +1,8 @@ # Authors: Alexandre Gramfort # Joan Massich # -# License: BSD Style. +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import os import time diff --git a/mne/datasets/sleep_physionet/tests/test_physionet.py b/mne/datasets/sleep_physionet/tests/test_physionet.py index db3600290b1..08b13c832c7 100644 --- a/mne/datasets/sleep_physionet/tests/test_physionet.py +++ b/mne/datasets/sleep_physionet/tests/test_physionet.py @@ -1,7 +1,8 @@ # Authors: Alexandre Gramfort # Joan Massich # -# License: BSD Style. +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. from pathlib import Path diff --git a/mne/datasets/somato/__init__.py b/mne/datasets/somato/__init__.py index 4777bbe8d5c..ab2b8e18da8 100644 --- a/mne/datasets/somato/__init__.py +++ b/mne/datasets/somato/__init__.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. """Somatosensory dataset.""" from .somato import data_path, get_version diff --git a/mne/datasets/somato/somato.py b/mne/datasets/somato/somato.py index 507cc3d32a8..1f679fc317d 100644 --- a/mne/datasets/somato/somato.py +++ b/mne/datasets/somato/somato.py @@ -1,7 +1,8 @@ # Authors: Alexandre Gramfort # Martin Luessi # Eric Larson -# License: BSD Style. +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. from ...utils import verbose from ..utils import _data_path_doc, _download_mne_dataset, _get_version, _version_doc diff --git a/mne/datasets/spm_face/__init__.py b/mne/datasets/spm_face/__init__.py index dfe2edd5486..ebee6f81f1d 100644 --- a/mne/datasets/spm_face/__init__.py +++ b/mne/datasets/spm_face/__init__.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. """SPM face dataset.""" from .spm_data import data_path, has_spm_data, get_version, requires_spm_data diff --git a/mne/datasets/spm_face/spm_data.py b/mne/datasets/spm_face/spm_data.py index d622e4b8453..72ab6f2b264 100644 --- a/mne/datasets/spm_face/spm_data.py +++ b/mne/datasets/spm_face/spm_data.py @@ -1,6 +1,7 @@ # Authors: Denis Engemann # -# License: BSD Style. +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. from functools import partial diff --git a/mne/datasets/ssvep/__init__.py b/mne/datasets/ssvep/__init__.py index a7a3d1db0a8..e5ad9259679 100644 --- a/mne/datasets/ssvep/__init__.py +++ b/mne/datasets/ssvep/__init__.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. """SSVEP dataset.""" from .ssvep import data_path, get_version diff --git a/mne/datasets/ssvep/ssvep.py b/mne/datasets/ssvep/ssvep.py index 6d1861ffca2..4ba85be3bec 100644 --- a/mne/datasets/ssvep/ssvep.py +++ b/mne/datasets/ssvep/ssvep.py @@ -1,5 +1,6 @@ # Authors: Dominik Welke -# License: BSD Style. +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. from ...utils import verbose from ..utils import _data_path_doc, _download_mne_dataset, _get_version, _version_doc diff --git a/mne/datasets/testing/__init__.py b/mne/datasets/testing/__init__.py index 7b60bd35b49..3aa0704d4f3 100644 --- a/mne/datasets/testing/__init__.py +++ b/mne/datasets/testing/__init__.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. """MNE testing dataset.""" from ._testing import ( diff --git a/mne/datasets/testing/_testing.py b/mne/datasets/testing/_testing.py index aee35e59372..2ead4437bb4 100644 --- a/mne/datasets/testing/_testing.py +++ b/mne/datasets/testing/_testing.py @@ -1,7 +1,8 @@ # Authors: Alexandre Gramfort # Martin Luessi # Eric Larson -# License: BSD Style. +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. from functools import partial diff --git a/mne/datasets/tests/__init__.py b/mne/datasets/tests/__init__.py index e69de29bb2d..bdfc6e7b46e 100644 --- a/mne/datasets/tests/__init__.py +++ b/mne/datasets/tests/__init__.py @@ -0,0 +1,2 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. diff --git a/mne/datasets/tests/test_datasets.py b/mne/datasets/tests/test_datasets.py index f1569317f8e..b84b3a2f367 100644 --- a/mne/datasets/tests/test_datasets.py +++ b/mne/datasets/tests/test_datasets.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import os import re import shutil diff --git a/mne/datasets/ucl_opm_auditory/__init__.py b/mne/datasets/ucl_opm_auditory/__init__.py index 4269d12b0c6..88407e6b87d 100644 --- a/mne/datasets/ucl_opm_auditory/__init__.py +++ b/mne/datasets/ucl_opm_auditory/__init__.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. """fNIRS motor dataset.""" from .ucl_opm_auditory import data_path, get_version diff --git a/mne/datasets/ucl_opm_auditory/ucl_opm_auditory.py b/mne/datasets/ucl_opm_auditory/ucl_opm_auditory.py index 205dd2ec8ac..cd872fdd16a 100644 --- a/mne/datasets/ucl_opm_auditory/ucl_opm_auditory.py +++ b/mne/datasets/ucl_opm_auditory/ucl_opm_auditory.py @@ -1,5 +1,6 @@ # Authors: Eric Larson -# License: BSD Style. +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. from ...utils import verbose from ..utils import _data_path_doc, _download_mne_dataset, _get_version, _version_doc diff --git a/mne/datasets/utils.py b/mne/datasets/utils.py index 568436a2898..eddee6f5684 100644 --- a/mne/datasets/utils.py +++ b/mne/datasets/utils.py @@ -6,7 +6,8 @@ # Adam Li # Daniel McCloy # -# License: BSD Style. +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import importlib import inspect diff --git a/mne/datasets/visual_92_categories/__init__.py b/mne/datasets/visual_92_categories/__init__.py index a0b26c17fed..ecd85a5fe95 100644 --- a/mne/datasets/visual_92_categories/__init__.py +++ b/mne/datasets/visual_92_categories/__init__.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. """MNE visual_92_categories dataset.""" from .visual_92_categories import data_path, get_version diff --git a/mne/datasets/visual_92_categories/visual_92_categories.py b/mne/datasets/visual_92_categories/visual_92_categories.py index e3a3dfaaae7..f7d626cf5ac 100644 --- a/mne/datasets/visual_92_categories/visual_92_categories.py +++ b/mne/datasets/visual_92_categories/visual_92_categories.py @@ -1,4 +1,5 @@ -# License: BSD Style. +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. from ...utils import verbose from ..utils import _data_path_doc, _download_mne_dataset, _get_version, _version_doc diff --git a/mne/decoding/__init__.py b/mne/decoding/__init__.py index ea140501e26..925fa73e5e9 100644 --- a/mne/decoding/__init__.py +++ b/mne/decoding/__init__.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. """Decoding and encoding, including machine learning and receptive fields.""" import lazy_loader as lazy diff --git a/mne/decoding/base.py b/mne/decoding/base.py index 88838efeeb1..08a0d65e951 100644 --- a/mne/decoding/base.py +++ b/mne/decoding/base.py @@ -5,6 +5,7 @@ # Jean-Remi King # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import datetime as dt import numbers diff --git a/mne/decoding/csp.py b/mne/decoding/csp.py index 53cecd53ab0..1656db50b36 100644 --- a/mne/decoding/csp.py +++ b/mne/decoding/csp.py @@ -5,6 +5,7 @@ # Jean-Remi King # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import copy as cp diff --git a/mne/decoding/ems.py b/mne/decoding/ems.py index e8b3ac1da43..81fde2a1ed8 100644 --- a/mne/decoding/ems.py +++ b/mne/decoding/ems.py @@ -3,6 +3,7 @@ # Jean-Remi King # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from collections import Counter diff --git a/mne/decoding/mixin.py b/mne/decoding/mixin.py index d009e0a23ba..2a0adee19eb 100644 --- a/mne/decoding/mixin.py +++ b/mne/decoding/mixin.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. class TransformerMixin: """Mixin class for all transformers in scikit-learn.""" diff --git a/mne/decoding/receptive_field.py b/mne/decoding/receptive_field.py index 4a274fd3997..c3c07cfa42f 100644 --- a/mne/decoding/receptive_field.py +++ b/mne/decoding/receptive_field.py @@ -2,6 +2,7 @@ # Eric Larson # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numbers diff --git a/mne/decoding/search_light.py b/mne/decoding/search_light.py index a176c4accac..06b7b010651 100644 --- a/mne/decoding/search_light.py +++ b/mne/decoding/search_light.py @@ -1,6 +1,7 @@ # Author: Jean-Remi King # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import logging import numpy as np diff --git a/mne/decoding/ssd.py b/mne/decoding/ssd.py index 04c42a1970a..961444b122c 100644 --- a/mne/decoding/ssd.py +++ b/mne/decoding/ssd.py @@ -2,6 +2,7 @@ # Victoria Peterson # Thomas S. Binns # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np from scipy.linalg import eigh diff --git a/mne/decoding/tests/__init__.py b/mne/decoding/tests/__init__.py index e69de29bb2d..bdfc6e7b46e 100644 --- a/mne/decoding/tests/__init__.py +++ b/mne/decoding/tests/__init__.py @@ -0,0 +1,2 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. diff --git a/mne/decoding/tests/test_base.py b/mne/decoding/tests/test_base.py index 885d7ff04f5..206628d3799 100644 --- a/mne/decoding/tests/test_base.py +++ b/mne/decoding/tests/test_base.py @@ -2,6 +2,7 @@ # Marijn van Vliet, # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np import pytest diff --git a/mne/decoding/tests/test_csp.py b/mne/decoding/tests/test_csp.py index 5ad66885392..1f72eacbc48 100644 --- a/mne/decoding/tests/test_csp.py +++ b/mne/decoding/tests/test_csp.py @@ -4,6 +4,7 @@ # Jean-Remi King # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from pathlib import Path diff --git a/mne/decoding/tests/test_ems.py b/mne/decoding/tests/test_ems.py index 44bb0f86135..6b52ee7f6e1 100644 --- a/mne/decoding/tests/test_ems.py +++ b/mne/decoding/tests/test_ems.py @@ -1,6 +1,7 @@ # Author: Denis A. Engemann # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from pathlib import Path diff --git a/mne/decoding/tests/test_receptive_field.py b/mne/decoding/tests/test_receptive_field.py index 6322b7506b1..dfc570e374e 100644 --- a/mne/decoding/tests/test_receptive_field.py +++ b/mne/decoding/tests/test_receptive_field.py @@ -1,6 +1,7 @@ # Authors: Chris Holdgraf # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from pathlib import Path diff --git a/mne/decoding/tests/test_search_light.py b/mne/decoding/tests/test_search_light.py index 3d5009763eb..992efbfec30 100644 --- a/mne/decoding/tests/test_search_light.py +++ b/mne/decoding/tests/test_search_light.py @@ -1,6 +1,7 @@ # Author: Jean-Remi King, # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import platform from inspect import signature diff --git a/mne/decoding/tests/test_ssd.py b/mne/decoding/tests/test_ssd.py index 18ab707137a..bdb3f74c545 100644 --- a/mne/decoding/tests/test_ssd.py +++ b/mne/decoding/tests/test_ssd.py @@ -2,6 +2,7 @@ # Victoria Peterson # Thomas S. Binns # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np import pytest diff --git a/mne/decoding/tests/test_time_frequency.py b/mne/decoding/tests/test_time_frequency.py index c2c24f5d808..24708f4e00c 100644 --- a/mne/decoding/tests/test_time_frequency.py +++ b/mne/decoding/tests/test_time_frequency.py @@ -1,6 +1,7 @@ # Author: Jean-Remi King, # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np diff --git a/mne/decoding/tests/test_transformer.py b/mne/decoding/tests/test_transformer.py index cbd586601db..88a8345d4b8 100644 --- a/mne/decoding/tests/test_transformer.py +++ b/mne/decoding/tests/test_transformer.py @@ -2,6 +2,7 @@ # Romain Trachel # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from pathlib import Path diff --git a/mne/decoding/time_delaying_ridge.py b/mne/decoding/time_delaying_ridge.py index 849719d56aa..b89b4e98ac2 100644 --- a/mne/decoding/time_delaying_ridge.py +++ b/mne/decoding/time_delaying_ridge.py @@ -3,6 +3,7 @@ # Ross Maddox # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np from scipy import linalg diff --git a/mne/decoding/time_frequency.py b/mne/decoding/time_frequency.py index a138f085b59..e085e9e2706 100644 --- a/mne/decoding/time_frequency.py +++ b/mne/decoding/time_frequency.py @@ -1,6 +1,7 @@ # Author: Jean-Remi King # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np diff --git a/mne/decoding/transformer.py b/mne/decoding/transformer.py index 97482dec64e..9cb22a43355 100644 --- a/mne/decoding/transformer.py +++ b/mne/decoding/transformer.py @@ -3,6 +3,7 @@ # Romain Trachel # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np diff --git a/mne/defaults.py b/mne/defaults.py index 3d3b4d45761..8732280998f 100644 --- a/mne/defaults.py +++ b/mne/defaults.py @@ -3,6 +3,7 @@ # Eric Larson # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from copy import deepcopy diff --git a/mne/dipole.py b/mne/dipole.py index 31663d31e94..59531463da8 100644 --- a/mne/dipole.py +++ b/mne/dipole.py @@ -3,7 +3,8 @@ # Authors: Alexandre Gramfort # Eric Larson # -# License: Simplified BSD +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import functools import re diff --git a/mne/epochs.py b/mne/epochs.py index 59edcd139ab..864f4021b42 100644 --- a/mne/epochs.py +++ b/mne/epochs.py @@ -8,6 +8,7 @@ # Stefan Appelhoff # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import json import operator diff --git a/mne/event.py b/mne/event.py index 9b4e3a3f725..211ed4e5d5d 100644 --- a/mne/event.py +++ b/mne/event.py @@ -6,6 +6,7 @@ # Clement Moutard # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from collections.abc import Sequence from pathlib import Path diff --git a/mne/evoked.py b/mne/evoked.py index f2c75f3754b..31083795507 100644 --- a/mne/evoked.py +++ b/mne/evoked.py @@ -6,6 +6,7 @@ # Jona Sassenhagen # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from copy import deepcopy diff --git a/mne/export/__init__.py b/mne/export/__init__.py index eb213750708..b4f10c1606f 100644 --- a/mne/export/__init__.py +++ b/mne/export/__init__.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. """Functions for exporting data to non-FIF formats.""" import lazy_loader as lazy diff --git a/mne/export/_brainvision.py b/mne/export/_brainvision.py index ff61ee939fb..0da7647ecb7 100644 --- a/mne/export/_brainvision.py +++ b/mne/export/_brainvision.py @@ -1,6 +1,7 @@ # Authors: MNE Developers # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import os diff --git a/mne/export/_edf.py b/mne/export/_edf.py index ae141d64f28..7097f7bd85d 100644 --- a/mne/export/_edf.py +++ b/mne/export/_edf.py @@ -1,6 +1,7 @@ # Authors: MNE Developers # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from contextlib import contextmanager diff --git a/mne/export/_eeglab.py b/mne/export/_eeglab.py index 6d9173f1329..f8095cfc4f0 100644 --- a/mne/export/_eeglab.py +++ b/mne/export/_eeglab.py @@ -1,6 +1,7 @@ # Authors: MNE Developers # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np diff --git a/mne/export/_egimff.py b/mne/export/_egimff.py index 0e44bd9531b..ef10c71acfc 100644 --- a/mne/export/_egimff.py +++ b/mne/export/_egimff.py @@ -1,6 +1,7 @@ # Authors: MNE Developers # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import datetime import os diff --git a/mne/export/_export.py b/mne/export/_export.py index faeebbb7cec..80a18d090d2 100644 --- a/mne/export/_export.py +++ b/mne/export/_export.py @@ -1,6 +1,7 @@ # Authors: MNE Developers # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import os.path as op diff --git a/mne/export/tests/test_export.py b/mne/export/tests/test_export.py index 723bac9606a..67bd417bb50 100644 --- a/mne/export/tests/test_export.py +++ b/mne/export/tests/test_export.py @@ -2,6 +2,7 @@ # Authors: MNE Developers # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from contextlib import nullcontext from datetime import datetime, timezone diff --git a/mne/filter.py b/mne/filter.py index 0592aaf6fbb..528128822b8 100644 --- a/mne/filter.py +++ b/mne/filter.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. """IIR and FIR filtering and resampling functions.""" from collections import Counter diff --git a/mne/fixes.py b/mne/fixes.py index fe9396f1996..1d3cc5aadb4 100644 --- a/mne/fixes.py +++ b/mne/fixes.py @@ -10,7 +10,8 @@ # Gael Varoquaux # Fabian Pedregosa # Lars Buitinck -# License: BSD +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. # NOTE: # Imports for SciPy submodules need to stay nested in this module diff --git a/mne/forward/__init__.py b/mne/forward/__init__.py index 4b16f0ab55d..57dfb1ca09c 100644 --- a/mne/forward/__init__.py +++ b/mne/forward/__init__.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. """Forward modeling code.""" import lazy_loader as lazy # for testing purposes diff --git a/mne/forward/_compute_forward.py b/mne/forward/_compute_forward.py index e6e0ec22d81..6c4e157f7f9 100644 --- a/mne/forward/_compute_forward.py +++ b/mne/forward/_compute_forward.py @@ -5,6 +5,7 @@ # Mark Wronkiewicz # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # The computations in this code were primarily derived from Matti Hämäläinen's # C code. diff --git a/mne/forward/_field_interpolation.py b/mne/forward/_field_interpolation.py index 272d58e69f3..00ea5bc9e50 100644 --- a/mne/forward/_field_interpolation.py +++ b/mne/forward/_field_interpolation.py @@ -1,6 +1,8 @@ # Authors: Matti Hämäläinen # Alexandre Gramfort # Eric Larson +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. # The computations in this code were primarily derived from Matti Hämäläinen's # C code. diff --git a/mne/forward/_lead_dots.py b/mne/forward/_lead_dots.py index 8aef9d77002..b158f6db07f 100644 --- a/mne/forward/_lead_dots.py +++ b/mne/forward/_lead_dots.py @@ -3,6 +3,7 @@ # Mainak Jas # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # The computations in this code were primarily derived from Matti Hämäläinen's # C code. diff --git a/mne/forward/_make_forward.py b/mne/forward/_make_forward.py index d8d4509c19c..04b0eaf9592 100644 --- a/mne/forward/_make_forward.py +++ b/mne/forward/_make_forward.py @@ -4,6 +4,7 @@ # Eric Larson # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # The computations in this code were primarily derived from Matti Hämäläinen's # C code. diff --git a/mne/forward/forward.py b/mne/forward/forward.py index 03f5b8f5eae..9531445edd1 100644 --- a/mne/forward/forward.py +++ b/mne/forward/forward.py @@ -3,6 +3,7 @@ # Martin Luessi # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # The computations in this code were primarily derived from Matti Hämäläinen's # C code. diff --git a/mne/forward/tests/__init__.py b/mne/forward/tests/__init__.py index e69de29bb2d..bdfc6e7b46e 100644 --- a/mne/forward/tests/__init__.py +++ b/mne/forward/tests/__init__.py @@ -0,0 +1,2 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. diff --git a/mne/forward/tests/test_field_interpolation.py b/mne/forward/tests/test_field_interpolation.py index 2f2c179e694..f19b844d46c 100644 --- a/mne/forward/tests/test_field_interpolation.py +++ b/mne/forward/tests/test_field_interpolation.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. from os import path as op from pathlib import Path diff --git a/mne/forward/tests/test_forward.py b/mne/forward/tests/test_forward.py index fe9fe9b52bc..f636a424813 100644 --- a/mne/forward/tests/test_forward.py +++ b/mne/forward/tests/test_forward.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import gc from pathlib import Path diff --git a/mne/forward/tests/test_make_forward.py b/mne/forward/tests/test_make_forward.py index 86f3bf2556b..7c0dfa110aa 100644 --- a/mne/forward/tests/test_make_forward.py +++ b/mne/forward/tests/test_make_forward.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. from itertools import product from pathlib import Path diff --git a/mne/gui/__init__.py b/mne/gui/__init__.py index 3f7a393c4ac..0474ce59e82 100644 --- a/mne/gui/__init__.py +++ b/mne/gui/__init__.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. """Convenience functions for opening GUIs.""" import lazy_loader as lazy diff --git a/mne/gui/_coreg.py b/mne/gui/_coreg.py index 2ee787c7c31..6063f6f628f 100644 --- a/mne/gui/_coreg.py +++ b/mne/gui/_coreg.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import inspect import os import os.path as op diff --git a/mne/gui/_gui.py b/mne/gui/_gui.py index 82d7591651d..3c437f9d266 100644 --- a/mne/gui/_gui.py +++ b/mne/gui/_gui.py @@ -1,6 +1,7 @@ # Authors: Christian Brodbeck # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from ..utils import get_config, verbose, warn diff --git a/mne/gui/tests/__init__.py b/mne/gui/tests/__init__.py index e69de29bb2d..bdfc6e7b46e 100644 --- a/mne/gui/tests/__init__.py +++ b/mne/gui/tests/__init__.py @@ -0,0 +1,2 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. diff --git a/mne/gui/tests/test_coreg.py b/mne/gui/tests/test_coreg.py index 666ec7d28a0..aea6fba08ff 100644 --- a/mne/gui/tests/test_coreg.py +++ b/mne/gui/tests/test_coreg.py @@ -1,6 +1,7 @@ # Author: Christian Brodbeck # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import os from contextlib import nullcontext diff --git a/mne/gui/tests/test_gui_api.py b/mne/gui/tests/test_gui_api.py index 8f805a18878..004c670a5ca 100644 --- a/mne/gui/tests/test_gui_api.py +++ b/mne/gui/tests/test_gui_api.py @@ -1,6 +1,7 @@ # Authors: Guillaume Favelier # -# License: Simplified BSD +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import pytest diff --git a/mne/html_templates/__init__.py b/mne/html_templates/__init__.py index 851c785db58..845e5fe07f9 100644 --- a/mne/html_templates/__init__.py +++ b/mne/html_templates/__init__.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. """Jinja2 HTML templates.""" import lazy_loader as lazy diff --git a/mne/html_templates/_templates.py b/mne/html_templates/_templates.py index 2ece5fea66f..a54547679d2 100644 --- a/mne/html_templates/_templates.py +++ b/mne/html_templates/_templates.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import functools _COLLAPSED = False # will override in doc build diff --git a/mne/inverse_sparse/__init__.py b/mne/inverse_sparse/__init__.py index 134b2f3496c..1065c9d6bcc 100644 --- a/mne/inverse_sparse/__init__.py +++ b/mne/inverse_sparse/__init__.py @@ -1,7 +1,8 @@ """Non-Linear sparse inverse solvers.""" # Author: Alexandre Gramfort # -# License: Simplified BSD +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import lazy_loader as lazy (__getattr__, __dir__, __all__) = lazy.attach_stub(__name__, __file__) diff --git a/mne/inverse_sparse/_gamma_map.py b/mne/inverse_sparse/_gamma_map.py index 999e0274a25..6da9f9d2cc3 100644 --- a/mne/inverse_sparse/_gamma_map.py +++ b/mne/inverse_sparse/_gamma_map.py @@ -1,6 +1,7 @@ # Authors: Alexandre Gramfort # Martin Luessi -# License: Simplified BSD +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np diff --git a/mne/inverse_sparse/mxne_debiasing.py b/mne/inverse_sparse/mxne_debiasing.py index 6cb0159dde9..86fc281dcc9 100644 --- a/mne/inverse_sparse/mxne_debiasing.py +++ b/mne/inverse_sparse/mxne_debiasing.py @@ -2,6 +2,7 @@ # Alexandre Gramfort # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from math import sqrt diff --git a/mne/inverse_sparse/mxne_inverse.py b/mne/inverse_sparse/mxne_inverse.py index 48da331ff6d..9a2d8c4b5c8 100644 --- a/mne/inverse_sparse/mxne_inverse.py +++ b/mne/inverse_sparse/mxne_inverse.py @@ -1,7 +1,8 @@ # Author: Alexandre Gramfort # Daniel Strohmeier # -# License: Simplified BSD +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np diff --git a/mne/inverse_sparse/mxne_optim.py b/mne/inverse_sparse/mxne_optim.py index c9d4fc83618..b70476991a2 100644 --- a/mne/inverse_sparse/mxne_optim.py +++ b/mne/inverse_sparse/mxne_optim.py @@ -1,7 +1,8 @@ # Author: Alexandre Gramfort # Daniel Strohmeier # Mathurin Massias -# License: Simplified BSD +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import functools from math import sqrt diff --git a/mne/inverse_sparse/tests/__init__.py b/mne/inverse_sparse/tests/__init__.py index e69de29bb2d..bdfc6e7b46e 100644 --- a/mne/inverse_sparse/tests/__init__.py +++ b/mne/inverse_sparse/tests/__init__.py @@ -0,0 +1,2 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. diff --git a/mne/inverse_sparse/tests/test_gamma_map.py b/mne/inverse_sparse/tests/test_gamma_map.py index 8aabf13352f..162003ac633 100644 --- a/mne/inverse_sparse/tests/test_gamma_map.py +++ b/mne/inverse_sparse/tests/test_gamma_map.py @@ -1,6 +1,7 @@ # Author: Martin Luessi # -# License: Simplified BSD +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np import pytest diff --git a/mne/inverse_sparse/tests/test_mxne_debiasing.py b/mne/inverse_sparse/tests/test_mxne_debiasing.py index a81a65cc994..1143e3752ac 100644 --- a/mne/inverse_sparse/tests/test_mxne_debiasing.py +++ b/mne/inverse_sparse/tests/test_mxne_debiasing.py @@ -2,6 +2,7 @@ # Alexandre Gramfort # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np from numpy.testing import assert_almost_equal diff --git a/mne/inverse_sparse/tests/test_mxne_inverse.py b/mne/inverse_sparse/tests/test_mxne_inverse.py index 0376ed83f93..639b1daeef8 100644 --- a/mne/inverse_sparse/tests/test_mxne_inverse.py +++ b/mne/inverse_sparse/tests/test_mxne_inverse.py @@ -1,7 +1,8 @@ # Author: Alexandre Gramfort # Daniel Strohmeier # -# License: Simplified BSD +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np import pytest diff --git a/mne/inverse_sparse/tests/test_mxne_optim.py b/mne/inverse_sparse/tests/test_mxne_optim.py index bc1ed349acb..79d578532ea 100644 --- a/mne/inverse_sparse/tests/test_mxne_optim.py +++ b/mne/inverse_sparse/tests/test_mxne_optim.py @@ -1,7 +1,8 @@ # Author: Alexandre Gramfort # Daniel Strohmeier # -# License: Simplified BSD +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np import pytest diff --git a/mne/io/__init__.py b/mne/io/__init__.py index ba7f3113794..b2ba75e44a6 100644 --- a/mne/io/__init__.py +++ b/mne/io/__init__.py @@ -3,6 +3,7 @@ # Matti Hämäläinen # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import lazy_loader as lazy (__getattr__, __dir__, __all__) = lazy.attach_stub(__name__, __file__) diff --git a/mne/io/_fiff_wrap.py b/mne/io/_fiff_wrap.py index df9e48e7644..526868d7f4b 100644 --- a/mne/io/_fiff_wrap.py +++ b/mne/io/_fiff_wrap.py @@ -1,4 +1,6 @@ # ruff: noqa: F401 +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. # Backward compat since these were in the public API before switching to _fiff # (and _empty_info is convenient to keep here for tests and is private) diff --git a/mne/io/_read_raw.py b/mne/io/_read_raw.py index fafc43a0d9a..f9e715be6b0 100644 --- a/mne/io/_read_raw.py +++ b/mne/io/_read_raw.py @@ -3,6 +3,7 @@ # Authors: Clemens Brunner # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from functools import partial diff --git a/mne/io/array/__init__.py b/mne/io/array/__init__.py index 35778e47443..c730a538b2f 100644 --- a/mne/io/array/__init__.py +++ b/mne/io/array/__init__.py @@ -1,5 +1,7 @@ """Module to convert user data to FIF.""" # Author: Eric Larson +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. from .array import RawArray diff --git a/mne/io/array/array.py b/mne/io/array/array.py index 5b75ef838cb..456bd763015 100644 --- a/mne/io/array/array.py +++ b/mne/io/array/array.py @@ -3,6 +3,7 @@ # Authors: Eric Larson # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np diff --git a/mne/io/array/tests/__init__.py b/mne/io/array/tests/__init__.py index e69de29bb2d..bdfc6e7b46e 100644 --- a/mne/io/array/tests/__init__.py +++ b/mne/io/array/tests/__init__.py @@ -0,0 +1,2 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. diff --git a/mne/io/array/tests/test_array.py b/mne/io/array/tests/test_array.py index 315e921e1ab..59e9913175e 100644 --- a/mne/io/array/tests/test_array.py +++ b/mne/io/array/tests/test_array.py @@ -1,6 +1,7 @@ # Author: Eric Larson # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from pathlib import Path diff --git a/mne/io/artemis123/__init__.py b/mne/io/artemis123/__init__.py index d9becf44eb1..9f57c9587a1 100644 --- a/mne/io/artemis123/__init__.py +++ b/mne/io/artemis123/__init__.py @@ -3,5 +3,6 @@ # Author: Luke Bloy # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from .artemis123 import read_raw_artemis123 diff --git a/mne/io/artemis123/artemis123.py b/mne/io/artemis123/artemis123.py index 1d131da8376..3cdedb3770d 100644 --- a/mne/io/artemis123/artemis123.py +++ b/mne/io/artemis123/artemis123.py @@ -1,6 +1,7 @@ # Author: Luke Bloy # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import calendar import datetime diff --git a/mne/io/artemis123/tests/__init__.py b/mne/io/artemis123/tests/__init__.py index e69de29bb2d..bdfc6e7b46e 100644 --- a/mne/io/artemis123/tests/__init__.py +++ b/mne/io/artemis123/tests/__init__.py @@ -0,0 +1,2 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. diff --git a/mne/io/artemis123/tests/test_artemis123.py b/mne/io/artemis123/tests/test_artemis123.py index ed17ab0f118..9a1cdb36eec 100644 --- a/mne/io/artemis123/tests/test_artemis123.py +++ b/mne/io/artemis123/tests/test_artemis123.py @@ -1,6 +1,7 @@ # Author: Luke Bloy # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np import pytest diff --git a/mne/io/artemis123/utils.py b/mne/io/artemis123/utils.py index fb2e72f01bf..95f307058ea 100644 --- a/mne/io/artemis123/utils.py +++ b/mne/io/artemis123/utils.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import os.path as op import numpy as np diff --git a/mne/io/base.py b/mne/io/base.py index 658eb0e4ea2..de6f3aa589d 100644 --- a/mne/io/base.py +++ b/mne/io/base.py @@ -8,6 +8,7 @@ # Clemens Brunner # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import os import os.path as op diff --git a/mne/io/besa/__init__.py b/mne/io/besa/__init__.py index 37d2bb63e02..0d4d86983df 100644 --- a/mne/io/besa/__init__.py +++ b/mne/io/besa/__init__.py @@ -3,5 +3,6 @@ # Author: Marijn van Vliet # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from .besa import read_evoked_besa diff --git a/mne/io/besa/besa.py b/mne/io/besa/besa.py index 07fa1ee9eef..7af8a066204 100644 --- a/mne/io/besa/besa.py +++ b/mne/io/besa/besa.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. from collections import OrderedDict from pathlib import Path diff --git a/mne/io/besa/tests/test_besa.py b/mne/io/besa/tests/test_besa.py index 527ef86bff4..aeecf48cd63 100644 --- a/mne/io/besa/tests/test_besa.py +++ b/mne/io/besa/tests/test_besa.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. """Test reading BESA fileformats.""" import inspect from pathlib import Path diff --git a/mne/io/boxy/__init__.py b/mne/io/boxy/__init__.py index 701f5fd8d20..618a89dc5d0 100644 --- a/mne/io/boxy/__init__.py +++ b/mne/io/boxy/__init__.py @@ -3,5 +3,6 @@ # Authors: Kyle Mathewson, Jonathan Kuziek # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from .boxy import read_raw_boxy diff --git a/mne/io/boxy/boxy.py b/mne/io/boxy/boxy.py index 9d4351df67f..b2afe096f64 100644 --- a/mne/io/boxy/boxy.py +++ b/mne/io/boxy/boxy.py @@ -1,6 +1,7 @@ # Authors: Kyle Mathewson, Jonathan Kuziek # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import re as re diff --git a/mne/io/boxy/tests/__init__.py b/mne/io/boxy/tests/__init__.py index e69de29bb2d..bdfc6e7b46e 100644 --- a/mne/io/boxy/tests/__init__.py +++ b/mne/io/boxy/tests/__init__.py @@ -0,0 +1,2 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. diff --git a/mne/io/boxy/tests/test_boxy.py b/mne/io/boxy/tests/test_boxy.py index e557980f2f8..2e9e1900aaf 100644 --- a/mne/io/boxy/tests/test_boxy.py +++ b/mne/io/boxy/tests/test_boxy.py @@ -1,6 +1,7 @@ # Authors: Kyle Mathewson, Jonathan Kuziek # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np import pytest diff --git a/mne/io/brainvision/__init__.py b/mne/io/brainvision/__init__.py index f51241e8673..5c66c1a82e2 100644 --- a/mne/io/brainvision/__init__.py +++ b/mne/io/brainvision/__init__.py @@ -4,5 +4,6 @@ # Stefan Appelhoff # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from .brainvision import read_raw_brainvision diff --git a/mne/io/brainvision/brainvision.py b/mne/io/brainvision/brainvision.py index 2d75910ec4a..3a4f63718c3 100644 --- a/mne/io/brainvision/brainvision.py +++ b/mne/io/brainvision/brainvision.py @@ -8,6 +8,7 @@ # Stefan Appelhoff # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import configparser import os diff --git a/mne/io/brainvision/tests/__init__.py b/mne/io/brainvision/tests/__init__.py index 8b137891791..bdfc6e7b46e 100644 --- a/mne/io/brainvision/tests/__init__.py +++ b/mne/io/brainvision/tests/__init__.py @@ -1 +1,2 @@ - +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. diff --git a/mne/io/brainvision/tests/test_brainvision.py b/mne/io/brainvision/tests/test_brainvision.py index 6aa222db1ec..1688963296a 100644 --- a/mne/io/brainvision/tests/test_brainvision.py +++ b/mne/io/brainvision/tests/test_brainvision.py @@ -3,6 +3,7 @@ # Stefan Appelhoff # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import datetime import re diff --git a/mne/io/bti/__init__.py b/mne/io/bti/__init__.py index aeb4d184baf..bf43742efbf 100644 --- a/mne/io/bti/__init__.py +++ b/mne/io/bti/__init__.py @@ -1,5 +1,7 @@ """BTi module for conversion to FIF.""" # Author: Denis A. Engemann +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. from .bti import read_raw_bti diff --git a/mne/io/bti/bti.py b/mne/io/bti/bti.py index 07e0d60d153..190625f8ee0 100644 --- a/mne/io/bti/bti.py +++ b/mne/io/bti/bti.py @@ -7,6 +7,8 @@ # Teon Brooks # # simplified BSD-3 license +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import functools import os.path as op diff --git a/mne/io/bti/constants.py b/mne/io/bti/constants.py index 69c27da485c..3abec30d29a 100644 --- a/mne/io/bti/constants.py +++ b/mne/io/bti/constants.py @@ -1,6 +1,7 @@ # Authors: Denis Engemann # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from ...utils import BunchConst diff --git a/mne/io/bti/read.py b/mne/io/bti/read.py index d05e2d9d941..4c13ed2f426 100644 --- a/mne/io/bti/read.py +++ b/mne/io/bti/read.py @@ -1,5 +1,7 @@ # Authors: Denis A. Engemann # simplified BSD-3 license +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np diff --git a/mne/io/bti/tests/__init__.py b/mne/io/bti/tests/__init__.py index e69de29bb2d..bdfc6e7b46e 100644 --- a/mne/io/bti/tests/__init__.py +++ b/mne/io/bti/tests/__init__.py @@ -0,0 +1,2 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. diff --git a/mne/io/bti/tests/test_bti.py b/mne/io/bti/tests/test_bti.py index dfeb7ecab40..de2a5fdd79c 100644 --- a/mne/io/bti/tests/test_bti.py +++ b/mne/io/bti/tests/test_bti.py @@ -1,6 +1,7 @@ # Authors: Denis Engemann # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import os from collections import Counter diff --git a/mne/io/cnt/__init__.py b/mne/io/cnt/__init__.py index 5021fd75a4c..d85b6bce1db 100644 --- a/mne/io/cnt/__init__.py +++ b/mne/io/cnt/__init__.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. """CNT data reader.""" from .cnt import read_raw_cnt diff --git a/mne/io/cnt/_utils.py b/mne/io/cnt/_utils.py index 0cc5d277b48..86842ad60c6 100644 --- a/mne/io/cnt/_utils.py +++ b/mne/io/cnt/_utils.py @@ -1,6 +1,7 @@ # Author: Joan Massich # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from collections import namedtuple from datetime import datetime diff --git a/mne/io/cnt/cnt.py b/mne/io/cnt/cnt.py index 32f2611173e..a242e85952b 100644 --- a/mne/io/cnt/cnt.py +++ b/mne/io/cnt/cnt.py @@ -4,6 +4,7 @@ # Joan Massich # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from os import path import numpy as np diff --git a/mne/io/cnt/tests/__init__.py b/mne/io/cnt/tests/__init__.py index e69de29bb2d..bdfc6e7b46e 100644 --- a/mne/io/cnt/tests/__init__.py +++ b/mne/io/cnt/tests/__init__.py @@ -0,0 +1,2 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. diff --git a/mne/io/cnt/tests/test_cnt.py b/mne/io/cnt/tests/test_cnt.py index c7dd956e9f9..526cc893e69 100644 --- a/mne/io/cnt/tests/test_cnt.py +++ b/mne/io/cnt/tests/test_cnt.py @@ -2,6 +2,7 @@ # Joan Massich # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np import pytest diff --git a/mne/io/constants.py b/mne/io/constants.py index df058c2b741..61db99600f2 100644 --- a/mne/io/constants.py +++ b/mne/io/constants.py @@ -1,6 +1,7 @@ # Author: Eric Larson # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from .._fiff import _io_dep_getattr from .._fiff.constants import FIFF diff --git a/mne/io/ctf/__init__.py b/mne/io/ctf/__init__.py index 61481f2efff..0a9427f6825 100644 --- a/mne/io/ctf/__init__.py +++ b/mne/io/ctf/__init__.py @@ -3,5 +3,6 @@ # Author: Eric Larson # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from .ctf import read_raw_ctf, RawCTF diff --git a/mne/io/ctf/constants.py b/mne/io/ctf/constants.py index f7176232f4e..880109d3166 100644 --- a/mne/io/ctf/constants.py +++ b/mne/io/ctf/constants.py @@ -4,6 +4,7 @@ # Eric Larson # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from ...utils import BunchConst diff --git a/mne/io/ctf/ctf.py b/mne/io/ctf/ctf.py index 7f42da183d6..feb5a04dda2 100644 --- a/mne/io/ctf/ctf.py +++ b/mne/io/ctf/ctf.py @@ -4,6 +4,7 @@ # Eric Larson # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import os diff --git a/mne/io/ctf/eeg.py b/mne/io/ctf/eeg.py index c70fe8b626d..29ece5e9f74 100644 --- a/mne/io/ctf/eeg.py +++ b/mne/io/ctf/eeg.py @@ -3,6 +3,7 @@ # Author: Eric Larson # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from os import listdir from os.path import join diff --git a/mne/io/ctf/hc.py b/mne/io/ctf/hc.py index 5bb94e4ec13..7beb8149960 100644 --- a/mne/io/ctf/hc.py +++ b/mne/io/ctf/hc.py @@ -3,6 +3,7 @@ # Author: Eric Larson # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np diff --git a/mne/io/ctf/info.py b/mne/io/ctf/info.py index 9e2b594dd9f..b177e29bf9d 100644 --- a/mne/io/ctf/info.py +++ b/mne/io/ctf/info.py @@ -3,6 +3,7 @@ # Author: Eric Larson # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import os.path as op from calendar import timegm diff --git a/mne/io/ctf/markers.py b/mne/io/ctf/markers.py index 34d8c45b5a1..5138a4e385d 100644 --- a/mne/io/ctf/markers.py +++ b/mne/io/ctf/markers.py @@ -1,6 +1,7 @@ # Author: Joan Massich # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import os.path as op from io import BytesIO diff --git a/mne/io/ctf/res4.py b/mne/io/ctf/res4.py index 2ea2f619bcc..1f69e356c09 100644 --- a/mne/io/ctf/res4.py +++ b/mne/io/ctf/res4.py @@ -4,6 +4,7 @@ # Eric Larson # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import os.path as op diff --git a/mne/io/ctf/tests/__init__.py b/mne/io/ctf/tests/__init__.py index e69de29bb2d..bdfc6e7b46e 100644 --- a/mne/io/ctf/tests/__init__.py +++ b/mne/io/ctf/tests/__init__.py @@ -0,0 +1,2 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. diff --git a/mne/io/ctf/tests/test_ctf.py b/mne/io/ctf/tests/test_ctf.py index 992f7f6ae37..f5340421a70 100644 --- a/mne/io/ctf/tests/test_ctf.py +++ b/mne/io/ctf/tests/test_ctf.py @@ -1,6 +1,7 @@ # Authors: Eric Larson # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import copy import os diff --git a/mne/io/ctf/trans.py b/mne/io/ctf/trans.py index 3555dec7bff..5491b5fb972 100644 --- a/mne/io/ctf/trans.py +++ b/mne/io/ctf/trans.py @@ -3,6 +3,7 @@ # Author: Eric Larson # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np diff --git a/mne/io/curry/__init__.py b/mne/io/curry/__init__.py index ae514366e2b..1b46ae8afe2 100644 --- a/mne/io/curry/__init__.py +++ b/mne/io/curry/__init__.py @@ -3,5 +3,6 @@ # Author: Dirk Gütlin # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from .curry import read_raw_curry diff --git a/mne/io/curry/curry.py b/mne/io/curry/curry.py index 2d1e342ea8b..e5b8ce02ed3 100644 --- a/mne/io/curry/curry.py +++ b/mne/io/curry/curry.py @@ -4,6 +4,7 @@ # # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import os.path as op import re diff --git a/mne/io/curry/tests/__init__.py b/mne/io/curry/tests/__init__.py index e69de29bb2d..bdfc6e7b46e 100644 --- a/mne/io/curry/tests/__init__.py +++ b/mne/io/curry/tests/__init__.py @@ -0,0 +1,2 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. diff --git a/mne/io/curry/tests/test_curry.py b/mne/io/curry/tests/test_curry.py index 5e437101811..c4710ecb679 100644 --- a/mne/io/curry/tests/test_curry.py +++ b/mne/io/curry/tests/test_curry.py @@ -3,6 +3,7 @@ # # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from datetime import datetime, timezone from pathlib import Path diff --git a/mne/io/edf/__init__.py b/mne/io/edf/__init__.py index 221f6c7f698..67f309a6d80 100644 --- a/mne/io/edf/__init__.py +++ b/mne/io/edf/__init__.py @@ -3,5 +3,6 @@ # Author: Teon Brooks # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from .edf import read_raw_edf, read_raw_bdf, read_raw_gdf diff --git a/mne/io/edf/edf.py b/mne/io/edf/edf.py index d27aabae8a5..7c02642ec8f 100644 --- a/mne/io/edf/edf.py +++ b/mne/io/edf/edf.py @@ -9,6 +9,7 @@ # Jeroen Van Der Donckt (IDlab - imec) # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import os import re diff --git a/mne/io/edf/tests/__init__.py b/mne/io/edf/tests/__init__.py index e69de29bb2d..bdfc6e7b46e 100644 --- a/mne/io/edf/tests/__init__.py +++ b/mne/io/edf/tests/__init__.py @@ -0,0 +1,2 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. diff --git a/mne/io/edf/tests/test_edf.py b/mne/io/edf/tests/test_edf.py index 6dec227800a..38532e062c1 100644 --- a/mne/io/edf/tests/test_edf.py +++ b/mne/io/edf/tests/test_edf.py @@ -6,6 +6,7 @@ # Joan Massich # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import datetime from contextlib import nullcontext diff --git a/mne/io/edf/tests/test_gdf.py b/mne/io/edf/tests/test_gdf.py index a851e372cee..c029cc3280c 100644 --- a/mne/io/edf/tests/test_gdf.py +++ b/mne/io/edf/tests/test_gdf.py @@ -2,6 +2,7 @@ # Nicolas Barascud # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import shutil from datetime import datetime, timedelta, timezone diff --git a/mne/io/eeglab/__init__.py b/mne/io/eeglab/__init__.py index 1573360162d..7976ad992ce 100644 --- a/mne/io/eeglab/__init__.py +++ b/mne/io/eeglab/__init__.py @@ -1,5 +1,7 @@ """EEGLAB module for conversion to FIF.""" # Author: Mainak Jas +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. from .eeglab import read_raw_eeglab, read_epochs_eeglab diff --git a/mne/io/eeglab/_eeglab.py b/mne/io/eeglab/_eeglab.py index 57c01da225a..9d138beab38 100644 --- a/mne/io/eeglab/_eeglab.py +++ b/mne/io/eeglab/_eeglab.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np try: diff --git a/mne/io/eeglab/eeglab.py b/mne/io/eeglab/eeglab.py index 6cf92bfd7bf..413a8ae4bfc 100644 --- a/mne/io/eeglab/eeglab.py +++ b/mne/io/eeglab/eeglab.py @@ -3,6 +3,7 @@ # Stefan Appelhoff # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import os.path as op from os import PathLike diff --git a/mne/io/eeglab/tests/__init__.py b/mne/io/eeglab/tests/__init__.py index e69de29bb2d..bdfc6e7b46e 100644 --- a/mne/io/eeglab/tests/__init__.py +++ b/mne/io/eeglab/tests/__init__.py @@ -0,0 +1,2 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. diff --git a/mne/io/eeglab/tests/test_eeglab.py b/mne/io/eeglab/tests/test_eeglab.py index ffe67b9a902..7d78f95ef6a 100644 --- a/mne/io/eeglab/tests/test_eeglab.py +++ b/mne/io/eeglab/tests/test_eeglab.py @@ -3,6 +3,7 @@ # Stefan Appelhoff # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import os import shutil diff --git a/mne/io/egi/__init__.py b/mne/io/egi/__init__.py index dccf8e6e6bf..de803d94eff 100644 --- a/mne/io/egi/__init__.py +++ b/mne/io/egi/__init__.py @@ -1,6 +1,8 @@ """EGI module for conversion to FIF.""" # Author: Denis A. Engemann +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. from .egi import read_raw_egi from .egimff import read_evokeds_mff diff --git a/mne/io/egi/egi.py b/mne/io/egi/egi.py index 0bd669837a3..0b62d7b6389 100644 --- a/mne/io/egi/egi.py +++ b/mne/io/egi/egi.py @@ -2,6 +2,8 @@ # Teon Brooks # # simplified BSD-3 license +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import datetime import time diff --git a/mne/io/egi/egimff.py b/mne/io/egi/egimff.py index 1120324c58a..457cab90087 100644 --- a/mne/io/egi/egimff.py +++ b/mne/io/egi/egimff.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. """EGI NetStation Load Function.""" import datetime diff --git a/mne/io/egi/events.py b/mne/io/egi/events.py index 8e035d42681..6f0ea1472c8 100644 --- a/mne/io/egi/events.py +++ b/mne/io/egi/events.py @@ -1,5 +1,6 @@ # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from datetime import datetime from glob import glob diff --git a/mne/io/egi/general.py b/mne/io/egi/general.py index ed20fce0fbd..ebd5a700363 100644 --- a/mne/io/egi/general.py +++ b/mne/io/egi/general.py @@ -1,5 +1,6 @@ # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import os import re diff --git a/mne/io/egi/tests/__init__.py b/mne/io/egi/tests/__init__.py index e69de29bb2d..bdfc6e7b46e 100644 --- a/mne/io/egi/tests/__init__.py +++ b/mne/io/egi/tests/__init__.py @@ -0,0 +1,2 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. diff --git a/mne/io/egi/tests/test_egi.py b/mne/io/egi/tests/test_egi.py index d57cb27359c..8da704243fd 100644 --- a/mne/io/egi/tests/test_egi.py +++ b/mne/io/egi/tests/test_egi.py @@ -1,5 +1,7 @@ # Authors: Denis A. Engemann # simplified BSD-3 license +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import os diff --git a/mne/io/eximia/__init__.py b/mne/io/eximia/__init__.py index b6cd9403301..72922eeb9de 100644 --- a/mne/io/eximia/__init__.py +++ b/mne/io/eximia/__init__.py @@ -3,5 +3,6 @@ # Author: Eric Larson # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from .eximia import read_raw_eximia diff --git a/mne/io/eximia/eximia.py b/mne/io/eximia/eximia.py index 2f13049fa2c..0af9d9daf5d 100644 --- a/mne/io/eximia/eximia.py +++ b/mne/io/eximia/eximia.py @@ -2,6 +2,7 @@ # Federico Raimondo # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import os.path as op diff --git a/mne/io/eximia/tests/__init__.py b/mne/io/eximia/tests/__init__.py index e69de29bb2d..bdfc6e7b46e 100644 --- a/mne/io/eximia/tests/__init__.py +++ b/mne/io/eximia/tests/__init__.py @@ -0,0 +1,2 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. diff --git a/mne/io/eximia/tests/test_eximia.py b/mne/io/eximia/tests/test_eximia.py index d42e6b174a5..0883e86d2f5 100644 --- a/mne/io/eximia/tests/test_eximia.py +++ b/mne/io/eximia/tests/test_eximia.py @@ -1,5 +1,7 @@ # Authors: Federico Raimondo # simplified BSD-3 license +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. from numpy.testing import assert_array_equal from scipy import io as sio diff --git a/mne/io/eyelink/__init__.py b/mne/io/eyelink/__init__.py index e8f09e1aee5..c32b8b4c5d7 100644 --- a/mne/io/eyelink/__init__.py +++ b/mne/io/eyelink/__init__.py @@ -4,5 +4,6 @@ # Scott Huberty # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from .eyelink import read_raw_eyelink diff --git a/mne/io/eyelink/_utils.py b/mne/io/eyelink/_utils.py index 3cbd06c52a6..f6ab2f8790d 100644 --- a/mne/io/eyelink/_utils.py +++ b/mne/io/eyelink/_utils.py @@ -1,6 +1,7 @@ """Helper functions for reading eyelink ASCII files.""" # Authors: Scott Huberty # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import re diff --git a/mne/io/eyelink/eyelink.py b/mne/io/eyelink/eyelink.py index 9a465d76ce9..196aef408b1 100644 --- a/mne/io/eyelink/eyelink.py +++ b/mne/io/eyelink/eyelink.py @@ -5,6 +5,7 @@ # Christian O'Reilly # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from pathlib import Path diff --git a/mne/io/eyelink/tests/__init__.py b/mne/io/eyelink/tests/__init__.py index e69de29bb2d..bdfc6e7b46e 100644 --- a/mne/io/eyelink/tests/__init__.py +++ b/mne/io/eyelink/tests/__init__.py @@ -0,0 +1,2 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. diff --git a/mne/io/eyelink/tests/test_eyelink.py b/mne/io/eyelink/tests/test_eyelink.py index a578bce25a5..47b25e94489 100644 --- a/mne/io/eyelink/tests/test_eyelink.py +++ b/mne/io/eyelink/tests/test_eyelink.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. from pathlib import Path import numpy as np diff --git a/mne/io/fieldtrip/__init__.py b/mne/io/fieldtrip/__init__.py index 2330e7222c9..43e1cfbe50f 100644 --- a/mne/io/fieldtrip/__init__.py +++ b/mne/io/fieldtrip/__init__.py @@ -3,5 +3,6 @@ # Dirk Gütlin # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from .fieldtrip import read_evoked_fieldtrip, read_epochs_fieldtrip, read_raw_fieldtrip diff --git a/mne/io/fieldtrip/fieldtrip.py b/mne/io/fieldtrip/fieldtrip.py index a3d13512146..8d054b076ee 100644 --- a/mne/io/fieldtrip/fieldtrip.py +++ b/mne/io/fieldtrip/fieldtrip.py @@ -3,6 +3,7 @@ # Dirk Gütlin # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np diff --git a/mne/io/fieldtrip/tests/__init__.py b/mne/io/fieldtrip/tests/__init__.py index 047bd9a9574..3c50da09974 100644 --- a/mne/io/fieldtrip/tests/__init__.py +++ b/mne/io/fieldtrip/tests/__init__.py @@ -3,3 +3,4 @@ # Dirk Gütlin # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. diff --git a/mne/io/fieldtrip/tests/helpers.py b/mne/io/fieldtrip/tests/helpers.py index cf0102a8139..5ab02286b66 100644 --- a/mne/io/fieldtrip/tests/helpers.py +++ b/mne/io/fieldtrip/tests/helpers.py @@ -3,6 +3,7 @@ # Dirk Gütlin # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import os from functools import partial diff --git a/mne/io/fieldtrip/tests/test_fieldtrip.py b/mne/io/fieldtrip/tests/test_fieldtrip.py index fc773f85eb8..0f66d1b1fae 100644 --- a/mne/io/fieldtrip/tests/test_fieldtrip.py +++ b/mne/io/fieldtrip/tests/test_fieldtrip.py @@ -3,6 +3,7 @@ # Dirk Gütlin # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import copy import itertools diff --git a/mne/io/fieldtrip/utils.py b/mne/io/fieldtrip/utils.py index ae975dc309c..c4950d45bea 100644 --- a/mne/io/fieldtrip/utils.py +++ b/mne/io/fieldtrip/utils.py @@ -3,6 +3,7 @@ # Dirk Gütlin # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np from ..._fiff._digitization import DigPoint, _ensure_fiducials_head diff --git a/mne/io/fiff/__init__.py b/mne/io/fiff/__init__.py index f79fc0e3413..ed9f47290ca 100644 --- a/mne/io/fiff/__init__.py +++ b/mne/io/fiff/__init__.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. """FIF raw data reader.""" from .raw import Raw diff --git a/mne/io/fiff/raw.py b/mne/io/fiff/raw.py index eab274ba108..d81fd99c556 100644 --- a/mne/io/fiff/raw.py +++ b/mne/io/fiff/raw.py @@ -5,6 +5,7 @@ # Teon Brooks # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import copy import os diff --git a/mne/io/fiff/tests/__init__.py b/mne/io/fiff/tests/__init__.py index e69de29bb2d..bdfc6e7b46e 100644 --- a/mne/io/fiff/tests/__init__.py +++ b/mne/io/fiff/tests/__init__.py @@ -0,0 +1,2 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. diff --git a/mne/io/fiff/tests/test_raw_fiff.py b/mne/io/fiff/tests/test_raw_fiff.py index 9e4b6e5960a..985688a9c7e 100644 --- a/mne/io/fiff/tests/test_raw_fiff.py +++ b/mne/io/fiff/tests/test_raw_fiff.py @@ -2,6 +2,7 @@ # Denis Engemann # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import os import pathlib diff --git a/mne/io/fil/__init__.py b/mne/io/fil/__init__.py index 3e9acc6c329..9bf34124866 100644 --- a/mne/io/fil/__init__.py +++ b/mne/io/fil/__init__.py @@ -1,5 +1,6 @@ # Authors: George O'Neill # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from .fil import read_raw_fil diff --git a/mne/io/fil/fil.py b/mne/io/fil/fil.py index fc6472d0043..08b7778398a 100644 --- a/mne/io/fil/fil.py +++ b/mne/io/fil/fil.py @@ -1,6 +1,7 @@ # Authors: George O'Neill # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import json import pathlib diff --git a/mne/io/fil/sensors.py b/mne/io/fil/sensors.py index 3e251202fcf..044f8442b46 100644 --- a/mne/io/fil/sensors.py +++ b/mne/io/fil/sensors.py @@ -1,6 +1,7 @@ # Authors: George O'Neill # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from copy import deepcopy diff --git a/mne/io/fil/tests/test_fil.py b/mne/io/fil/tests/test_fil.py index a88c4fe4d12..62d4a587d47 100644 --- a/mne/io/fil/tests/test_fil.py +++ b/mne/io/fil/tests/test_fil.py @@ -1,6 +1,7 @@ # Authors: George O'Neill # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import shutil from os import remove diff --git a/mne/io/hitachi/__init__.py b/mne/io/hitachi/__init__.py index cdd39bad2fe..6fa7f0753ce 100644 --- a/mne/io/hitachi/__init__.py +++ b/mne/io/hitachi/__init__.py @@ -3,5 +3,6 @@ # Author: Eric Larson # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from .hitachi import read_raw_hitachi diff --git a/mne/io/hitachi/hitachi.py b/mne/io/hitachi/hitachi.py index 12219dacd8b..0f046bb37e6 100644 --- a/mne/io/hitachi/hitachi.py +++ b/mne/io/hitachi/hitachi.py @@ -1,6 +1,7 @@ # Authors: Eric Larson # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import datetime as dt import re diff --git a/mne/io/hitachi/tests/test_hitachi.py b/mne/io/hitachi/tests/test_hitachi.py index 72fa846c6fb..edad56dc75e 100644 --- a/mne/io/hitachi/tests/test_hitachi.py +++ b/mne/io/hitachi/tests/test_hitachi.py @@ -1,6 +1,7 @@ # Authors: Eric Larson # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import datetime as dt diff --git a/mne/io/kit/__init__.py b/mne/io/kit/__init__.py index a520b91da62..5c84d63705b 100644 --- a/mne/io/kit/__init__.py +++ b/mne/io/kit/__init__.py @@ -3,6 +3,7 @@ # Author: Teon Brooks # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from .kit import read_raw_kit, read_epochs_kit from .coreg import read_mrk diff --git a/mne/io/kit/constants.py b/mne/io/kit/constants.py index 541cb576b8f..5532b523c9f 100644 --- a/mne/io/kit/constants.py +++ b/mne/io/kit/constants.py @@ -4,6 +4,7 @@ # Christian Brodbeck # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from ..._fiff.constants import FIFF from ...utils import BunchConst diff --git a/mne/io/kit/coreg.py b/mne/io/kit/coreg.py index 209728dbaab..4e5bd0bdf8f 100644 --- a/mne/io/kit/coreg.py +++ b/mne/io/kit/coreg.py @@ -3,6 +3,7 @@ # Author: Teon Brooks # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import pickle import re diff --git a/mne/io/kit/kit.py b/mne/io/kit/kit.py index d6e5b370b2c..fa9ff8cfeea 100644 --- a/mne/io/kit/kit.py +++ b/mne/io/kit/kit.py @@ -8,6 +8,7 @@ # Christian Brodbeck # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from collections import OrderedDict, defaultdict from math import cos, sin diff --git a/mne/io/kit/tests/__init__.py b/mne/io/kit/tests/__init__.py index 4f7fb08a01d..7ed6f786293 100644 --- a/mne/io/kit/tests/__init__.py +++ b/mne/io/kit/tests/__init__.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. from pathlib import Path data_dir = Path(__file__).parent / "data" diff --git a/mne/io/kit/tests/test_coreg.py b/mne/io/kit/tests/test_coreg.py index 3017c06e4ed..7907832ea6c 100644 --- a/mne/io/kit/tests/test_coreg.py +++ b/mne/io/kit/tests/test_coreg.py @@ -1,6 +1,7 @@ # Authors: Christian Brodbeck # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import pickle from pathlib import Path diff --git a/mne/io/kit/tests/test_kit.py b/mne/io/kit/tests/test_kit.py index 244fa41f56b..6e325398414 100644 --- a/mne/io/kit/tests/test_kit.py +++ b/mne/io/kit/tests/test_kit.py @@ -1,6 +1,7 @@ # Author: Teon Brooks # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from pathlib import Path diff --git a/mne/io/meas_info.py b/mne/io/meas_info.py index b26a942ed5e..f971fff18d4 100644 --- a/mne/io/meas_info.py +++ b/mne/io/meas_info.py @@ -1,6 +1,7 @@ # Author: Eric Larson # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from .._fiff import _io_dep_getattr diff --git a/mne/io/nedf/__init__.py b/mne/io/nedf/__init__.py index 717669425d1..abf86990d77 100644 --- a/mne/io/nedf/__init__.py +++ b/mne/io/nedf/__init__.py @@ -3,5 +3,6 @@ # Author: Tristan Stenner # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from .nedf import read_raw_nedf, _parse_nedf_header diff --git a/mne/io/nedf/nedf.py b/mne/io/nedf/nedf.py index 8d8b85ba80b..55661511f83 100644 --- a/mne/io/nedf/nedf.py +++ b/mne/io/nedf/nedf.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. """Import NeuroElectrics DataFormat (NEDF) files.""" from copy import deepcopy diff --git a/mne/io/nedf/tests/__init__.py b/mne/io/nedf/tests/__init__.py index 8b137891791..bdfc6e7b46e 100644 --- a/mne/io/nedf/tests/__init__.py +++ b/mne/io/nedf/tests/__init__.py @@ -1 +1,2 @@ - +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. diff --git a/mne/io/nedf/tests/test_nedf.py b/mne/io/nedf/tests/test_nedf.py index 8b436d5017f..cf0043bbeeb 100644 --- a/mne/io/nedf/tests/test_nedf.py +++ b/mne/io/nedf/tests/test_nedf.py @@ -2,6 +2,7 @@ # Author: Tristan Stenner # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import pytest from numpy.testing import assert_allclose, assert_array_equal diff --git a/mne/io/neuralynx/__init__.py b/mne/io/neuralynx/__init__.py index bd9f226064c..70a2b0f38d7 100644 --- a/mne/io/neuralynx/__init__.py +++ b/mne/io/neuralynx/__init__.py @@ -1 +1,3 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. from .neuralynx import read_raw_neuralynx diff --git a/mne/io/neuralynx/neuralynx.py b/mne/io/neuralynx/neuralynx.py index cab4abdc0ab..4bfad0fea2c 100644 --- a/mne/io/neuralynx/neuralynx.py +++ b/mne/io/neuralynx/neuralynx.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import glob import os diff --git a/mne/io/neuralynx/tests/__init__.py b/mne/io/neuralynx/tests/__init__.py index e69de29bb2d..bdfc6e7b46e 100644 --- a/mne/io/neuralynx/tests/__init__.py +++ b/mne/io/neuralynx/tests/__init__.py @@ -0,0 +1,2 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. diff --git a/mne/io/neuralynx/tests/test_neuralynx.py b/mne/io/neuralynx/tests/test_neuralynx.py index ce5a6f8cb9f..21cb73927a8 100644 --- a/mne/io/neuralynx/tests/test_neuralynx.py +++ b/mne/io/neuralynx/tests/test_neuralynx.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import os from ast import literal_eval from typing import Dict diff --git a/mne/io/nicolet/__init__.py b/mne/io/nicolet/__init__.py index c3253d3cbdf..2ada6089209 100644 --- a/mne/io/nicolet/__init__.py +++ b/mne/io/nicolet/__init__.py @@ -3,5 +3,6 @@ # Author: Jaakko Leppakangas # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from .nicolet import read_raw_nicolet diff --git a/mne/io/nicolet/nicolet.py b/mne/io/nicolet/nicolet.py index 3ed1b75b63d..85a7d1e5607 100644 --- a/mne/io/nicolet/nicolet.py +++ b/mne/io/nicolet/nicolet.py @@ -1,6 +1,7 @@ # Author: Jaakko Leppakangas # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import calendar import datetime diff --git a/mne/io/nicolet/tests/__init__.py b/mne/io/nicolet/tests/__init__.py index e69de29bb2d..bdfc6e7b46e 100644 --- a/mne/io/nicolet/tests/__init__.py +++ b/mne/io/nicolet/tests/__init__.py @@ -0,0 +1,2 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. diff --git a/mne/io/nicolet/tests/test_nicolet.py b/mne/io/nicolet/tests/test_nicolet.py index 19e6e682b6d..6fe732a6dde 100644 --- a/mne/io/nicolet/tests/test_nicolet.py +++ b/mne/io/nicolet/tests/test_nicolet.py @@ -1,6 +1,7 @@ # Author: Jaakko Leppakangas # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from pathlib import Path diff --git a/mne/io/nihon/__init__.py b/mne/io/nihon/__init__.py index 09a735eb6bc..6cdfb64a17e 100644 --- a/mne/io/nihon/__init__.py +++ b/mne/io/nihon/__init__.py @@ -3,5 +3,6 @@ # Author: Fede Raimondo # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from .nihon import read_raw_nihon diff --git a/mne/io/nihon/nihon.py b/mne/io/nihon/nihon.py index b6b7e3179ff..b39a18af838 100644 --- a/mne/io/nihon/nihon.py +++ b/mne/io/nihon/nihon.py @@ -1,6 +1,7 @@ # Authors: Federico Raimondo # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from collections import OrderedDict from datetime import datetime, timezone diff --git a/mne/io/nihon/tests/test_nihon.py b/mne/io/nihon/tests/test_nihon.py index 666f293e820..4c84dd063a9 100644 --- a/mne/io/nihon/tests/test_nihon.py +++ b/mne/io/nihon/tests/test_nihon.py @@ -1,5 +1,7 @@ # Authors: Federico Raimondo # simplified BSD-3 license +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import pytest from numpy.testing import assert_array_almost_equal diff --git a/mne/io/nirx/__init__.py b/mne/io/nirx/__init__.py index 0a8ee5e709d..af604f32789 100644 --- a/mne/io/nirx/__init__.py +++ b/mne/io/nirx/__init__.py @@ -3,5 +3,6 @@ # Author: Robert Luke # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from .nirx import read_raw_nirx diff --git a/mne/io/nirx/_localized_abbr.py b/mne/io/nirx/_localized_abbr.py index c12133ef994..a3fe8e0b3c9 100644 --- a/mne/io/nirx/_localized_abbr.py +++ b/mne/io/nirx/_localized_abbr.py @@ -2,6 +2,7 @@ # Authors: Eric Larson # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # This file was generated on 2021/01/31 on an Ubuntu system. # When getting "unsupported locale setting" on Ubuntu (e.g., with localepurge), diff --git a/mne/io/nirx/nirx.py b/mne/io/nirx/nirx.py index f84d6329cb8..98d81f9c268 100644 --- a/mne/io/nirx/nirx.py +++ b/mne/io/nirx/nirx.py @@ -1,6 +1,7 @@ # Authors: Robert Luke # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import datetime as dt import glob as glob diff --git a/mne/io/nirx/tests/__init__.py b/mne/io/nirx/tests/__init__.py index e69de29bb2d..bdfc6e7b46e 100644 --- a/mne/io/nirx/tests/__init__.py +++ b/mne/io/nirx/tests/__init__.py @@ -0,0 +1,2 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. diff --git a/mne/io/nirx/tests/test_nirx.py b/mne/io/nirx/tests/test_nirx.py index 83df688cef0..3cc510612e0 100644 --- a/mne/io/nirx/tests/test_nirx.py +++ b/mne/io/nirx/tests/test_nirx.py @@ -1,6 +1,8 @@ # Authors: Robert Luke # Eric Larson # simplified BSD-3 license +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import datetime as dt import os diff --git a/mne/io/nsx/__init__.py b/mne/io/nsx/__init__.py index 1cdf93b11d6..90e626130d2 100644 --- a/mne/io/nsx/__init__.py +++ b/mne/io/nsx/__init__.py @@ -3,5 +3,6 @@ # Author: Proloy Das # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from .nsx import read_raw_nsx diff --git a/mne/io/nsx/nsx.py b/mne/io/nsx/nsx.py index a74bcd05f30..95448b1b22c 100644 --- a/mne/io/nsx/nsx.py +++ b/mne/io/nsx/nsx.py @@ -1,6 +1,7 @@ # Author: Proloy Das # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import os from datetime import datetime, timezone diff --git a/mne/io/nsx/tests/test_nsx.py b/mne/io/nsx/tests/test_nsx.py index 03b6ebb8606..399cfca5bd9 100644 --- a/mne/io/nsx/tests/test_nsx.py +++ b/mne/io/nsx/tests/test_nsx.py @@ -1,6 +1,7 @@ # Author: Proloy Das # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import os import numpy as np diff --git a/mne/io/persyst/__init__.py b/mne/io/persyst/__init__.py index cef562f4af6..c3f1e35682b 100644 --- a/mne/io/persyst/__init__.py +++ b/mne/io/persyst/__init__.py @@ -3,5 +3,6 @@ # Author: Adam Li # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from .persyst import read_raw_persyst diff --git a/mne/io/persyst/persyst.py b/mne/io/persyst/persyst.py index 873131ffad3..44334fa4555 100644 --- a/mne/io/persyst/persyst.py +++ b/mne/io/persyst/persyst.py @@ -1,6 +1,7 @@ # Authors: Adam Li # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import os import os.path as op from collections import OrderedDict diff --git a/mne/io/persyst/tests/__init__.py b/mne/io/persyst/tests/__init__.py index e69de29bb2d..bdfc6e7b46e 100644 --- a/mne/io/persyst/tests/__init__.py +++ b/mne/io/persyst/tests/__init__.py @@ -0,0 +1,2 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. diff --git a/mne/io/persyst/tests/test_persyst.py b/mne/io/persyst/tests/test_persyst.py index ac527157834..c81b53f2b79 100644 --- a/mne/io/persyst/tests/test_persyst.py +++ b/mne/io/persyst/tests/test_persyst.py @@ -1,6 +1,7 @@ # Authors: Adam Li # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import os import shutil diff --git a/mne/io/pick.py b/mne/io/pick.py index 7b9b38b8b2d..f7c77b1af14 100644 --- a/mne/io/pick.py +++ b/mne/io/pick.py @@ -1,6 +1,7 @@ # Author: Eric Larson # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from .._fiff import _io_dep_getattr diff --git a/mne/io/proj.py b/mne/io/proj.py index c40791ae2bb..98445f1ce7e 100644 --- a/mne/io/proj.py +++ b/mne/io/proj.py @@ -1,6 +1,7 @@ # Author: Eric Larson # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from .._fiff import _io_dep_getattr diff --git a/mne/io/reference.py b/mne/io/reference.py index 5bbe2bc6826..850d6bd7294 100644 --- a/mne/io/reference.py +++ b/mne/io/reference.py @@ -1,6 +1,7 @@ # Author: Eric Larson # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from .._fiff import _io_dep_getattr diff --git a/mne/io/snirf/__init__.py b/mne/io/snirf/__init__.py index ea3f11dc0a4..0ffe39de808 100644 --- a/mne/io/snirf/__init__.py +++ b/mne/io/snirf/__init__.py @@ -3,5 +3,6 @@ # Author: Robert Luke # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from ._snirf import read_raw_snirf diff --git a/mne/io/snirf/_snirf.py b/mne/io/snirf/_snirf.py index a0de3550c88..e32b32370b3 100644 --- a/mne/io/snirf/_snirf.py +++ b/mne/io/snirf/_snirf.py @@ -1,6 +1,7 @@ # Authors: Robert Luke # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import datetime import re diff --git a/mne/io/snirf/tests/__init__.py b/mne/io/snirf/tests/__init__.py index e69de29bb2d..bdfc6e7b46e 100644 --- a/mne/io/snirf/tests/__init__.py +++ b/mne/io/snirf/tests/__init__.py @@ -0,0 +1,2 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. diff --git a/mne/io/snirf/tests/test_snirf.py b/mne/io/snirf/tests/test_snirf.py index 276d003b791..2d2ad2c6324 100644 --- a/mne/io/snirf/tests/test_snirf.py +++ b/mne/io/snirf/tests/test_snirf.py @@ -1,5 +1,7 @@ # Authors: Robert Luke # simplified BSD-3 license +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import shutil diff --git a/mne/io/tag.py b/mne/io/tag.py index cf2dbc57221..41dc15fd40d 100644 --- a/mne/io/tag.py +++ b/mne/io/tag.py @@ -1,6 +1,7 @@ # Author: Eric Larson # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from .._fiff import _io_dep_getattr diff --git a/mne/io/tests/__init__.py b/mne/io/tests/__init__.py index ca22217d57a..f5523f5d662 100644 --- a/mne/io/tests/__init__.py +++ b/mne/io/tests/__init__.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import os.path as op data_dir = op.join(op.dirname(__file__), "data") diff --git a/mne/io/tests/data/__init__.py b/mne/io/tests/data/__init__.py index e69de29bb2d..bdfc6e7b46e 100644 --- a/mne/io/tests/data/__init__.py +++ b/mne/io/tests/data/__init__.py @@ -0,0 +1,2 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. diff --git a/mne/io/tests/test_apply_function.py b/mne/io/tests/test_apply_function.py index 94388ccf86e..b1869e1dae6 100644 --- a/mne/io/tests/test_apply_function.py +++ b/mne/io/tests/test_apply_function.py @@ -1,6 +1,7 @@ # Authors: Eric Larson # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np import pytest diff --git a/mne/io/tests/test_deprecation.py b/mne/io/tests/test_deprecation.py index f69c2d438d7..fecf9a78091 100644 --- a/mne/io/tests/test_deprecation.py +++ b/mne/io/tests/test_deprecation.py @@ -3,6 +3,7 @@ # Author: Eric Larson # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import pytest diff --git a/mne/io/tests/test_raw.py b/mne/io/tests/test_raw.py index c292de4c6c0..bac32f83f65 100644 --- a/mne/io/tests/test_raw.py +++ b/mne/io/tests/test_raw.py @@ -3,6 +3,7 @@ # Stefan Appelhoff # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import math import os diff --git a/mne/io/tests/test_read_raw.py b/mne/io/tests/test_read_raw.py index 4f44b9c7473..a1e27166b0a 100644 --- a/mne/io/tests/test_read_raw.py +++ b/mne/io/tests/test_read_raw.py @@ -3,6 +3,7 @@ # Authors: Clemens Brunner # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from pathlib import Path from shutil import copyfile diff --git a/mne/io/utils.py b/mne/io/utils.py index 2e06924287f..9460ceed55e 100644 --- a/mne/io/utils.py +++ b/mne/io/utils.py @@ -1,6 +1,7 @@ # Author: Eric Larson # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from .._fiff import _io_dep_getattr diff --git a/mne/io/write.py b/mne/io/write.py index e044f91b8e7..12c0ae00ca0 100644 --- a/mne/io/write.py +++ b/mne/io/write.py @@ -1,6 +1,7 @@ # Author: Eric Larson # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from .._fiff import _io_dep_getattr diff --git a/mne/label.py b/mne/label.py index 58db1379068..b57b466df27 100644 --- a/mne/label.py +++ b/mne/label.py @@ -3,6 +3,7 @@ # Denis Engemann # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import copy as cp import os diff --git a/mne/minimum_norm/__init__.py b/mne/minimum_norm/__init__.py index cb1308d9768..792c154170f 100644 --- a/mne/minimum_norm/__init__.py +++ b/mne/minimum_norm/__init__.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. """Linear inverse solvers based on L2 Minimum Norm Estimates (MNE).""" import lazy_loader as lazy diff --git a/mne/minimum_norm/_eloreta.py b/mne/minimum_norm/_eloreta.py index 0e497536825..8f15365e5b4 100644 --- a/mne/minimum_norm/_eloreta.py +++ b/mne/minimum_norm/_eloreta.py @@ -1,6 +1,7 @@ # Authors: Eric Larson # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from functools import partial diff --git a/mne/minimum_norm/inverse.py b/mne/minimum_norm/inverse.py index b8f1cdc11bd..f41f660ac4e 100644 --- a/mne/minimum_norm/inverse.py +++ b/mne/minimum_norm/inverse.py @@ -3,6 +3,7 @@ # Teon Brooks # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from copy import deepcopy from math import sqrt diff --git a/mne/minimum_norm/resolution_matrix.py b/mne/minimum_norm/resolution_matrix.py index 2d419c11484..3dd24ac6847 100644 --- a/mne/minimum_norm/resolution_matrix.py +++ b/mne/minimum_norm/resolution_matrix.py @@ -2,6 +2,7 @@ # Authors: olaf.hauk@mrc-cbu.cam.ac.uk # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from copy import deepcopy import numpy as np diff --git a/mne/minimum_norm/spatial_resolution.py b/mne/minimum_norm/spatial_resolution.py index b72e3cebdfd..d68be423494 100644 --- a/mne/minimum_norm/spatial_resolution.py +++ b/mne/minimum_norm/spatial_resolution.py @@ -1,6 +1,7 @@ # Authors: Olaf Hauk # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. """Compute resolution metrics from resolution matrix. Resolution metrics: localisation error, spatial extent, relative amplitude. diff --git a/mne/minimum_norm/tests/__init__.py b/mne/minimum_norm/tests/__init__.py index e69de29bb2d..bdfc6e7b46e 100644 --- a/mne/minimum_norm/tests/__init__.py +++ b/mne/minimum_norm/tests/__init__.py @@ -0,0 +1,2 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. diff --git a/mne/minimum_norm/tests/test_inverse.py b/mne/minimum_norm/tests/test_inverse.py index bf9537306e1..58722a19fd5 100644 --- a/mne/minimum_norm/tests/test_inverse.py +++ b/mne/minimum_norm/tests/test_inverse.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import copy import re from pathlib import Path diff --git a/mne/minimum_norm/tests/test_resolution_matrix.py b/mne/minimum_norm/tests/test_resolution_matrix.py index 6b2d56fd522..2fa6a39e4cf 100644 --- a/mne/minimum_norm/tests/test_resolution_matrix.py +++ b/mne/minimum_norm/tests/test_resolution_matrix.py @@ -3,6 +3,7 @@ # Eric Larson # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from contextlib import nullcontext diff --git a/mne/minimum_norm/tests/test_resolution_metrics.py b/mne/minimum_norm/tests/test_resolution_metrics.py index 72afd2eb406..938dc4759dc 100644 --- a/mne/minimum_norm/tests/test_resolution_metrics.py +++ b/mne/minimum_norm/tests/test_resolution_metrics.py @@ -2,6 +2,7 @@ # Daniel McCloy # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. """ Test the following properties for resolution metrics. diff --git a/mne/minimum_norm/tests/test_snr.py b/mne/minimum_norm/tests/test_snr.py index 92d8132dcce..befeb0c6e89 100644 --- a/mne/minimum_norm/tests/test_snr.py +++ b/mne/minimum_norm/tests/test_snr.py @@ -2,6 +2,7 @@ # Matti Hämäläinen # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import os from os import path as op diff --git a/mne/minimum_norm/tests/test_time_frequency.py b/mne/minimum_norm/tests/test_time_frequency.py index 7072faeda9d..5920b8ab4a3 100644 --- a/mne/minimum_norm/tests/test_time_frequency.py +++ b/mne/minimum_norm/tests/test_time_frequency.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np import pytest from numpy.testing import assert_allclose diff --git a/mne/minimum_norm/time_frequency.py b/mne/minimum_norm/time_frequency.py index a8699327ccf..9561e3cd53a 100644 --- a/mne/minimum_norm/time_frequency.py +++ b/mne/minimum_norm/time_frequency.py @@ -2,6 +2,7 @@ # Martin Luessi # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np diff --git a/mne/misc.py b/mne/misc.py index cbacda176c8..937f0eb4c9e 100644 --- a/mne/misc.py +++ b/mne/misc.py @@ -2,6 +2,7 @@ # Scott Burns # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. def parse_config(fname): diff --git a/mne/morph.py b/mne/morph.py index edfa6643c9c..eb201e34451 100644 --- a/mne/morph.py +++ b/mne/morph.py @@ -3,6 +3,7 @@ # Eric Larson # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import copy import os.path as op diff --git a/mne/morph_map.py b/mne/morph_map.py index b1153123174..64eb537b181 100644 --- a/mne/morph_map.py +++ b/mne/morph_map.py @@ -4,6 +4,7 @@ # Denis A. Engemann # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # Many of the computations in this code were derived from Matti Hämäläinen's # C code. diff --git a/mne/parallel.py b/mne/parallel.py index bb8a14d381d..8f314c07477 100644 --- a/mne/parallel.py +++ b/mne/parallel.py @@ -2,7 +2,8 @@ # Author: Alexandre Gramfort # -# License: Simplified BSD +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import logging import multiprocessing diff --git a/mne/preprocessing/__init__.py b/mne/preprocessing/__init__.py index 8a32cd31332..b409cb8e5d6 100644 --- a/mne/preprocessing/__init__.py +++ b/mne/preprocessing/__init__.py @@ -5,6 +5,7 @@ # Denis Engemann # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import lazy_loader as lazy (__getattr__, __dir__, __all__) = lazy.attach_stub(__name__, __file__) diff --git a/mne/preprocessing/_annotate_amplitude.py b/mne/preprocessing/_annotate_amplitude.py index 85c796258a2..527e74650f0 100644 --- a/mne/preprocessing/_annotate_amplitude.py +++ b/mne/preprocessing/_annotate_amplitude.py @@ -1,6 +1,7 @@ # Author: Mathieu Scheltienne # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np diff --git a/mne/preprocessing/_annotate_nan.py b/mne/preprocessing/_annotate_nan.py index 26ee12ab946..0d57ae7f807 100644 --- a/mne/preprocessing/_annotate_nan.py +++ b/mne/preprocessing/_annotate_nan.py @@ -1,6 +1,7 @@ # Author: David Julien # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np diff --git a/mne/preprocessing/_csd.py b/mne/preprocessing/_csd.py index 27f1481fcc0..a5f81cd3208 100644 --- a/mne/preprocessing/_csd.py +++ b/mne/preprocessing/_csd.py @@ -1,16 +1,16 @@ -# Copyright 2003-2010 Jürgen Kayser -# Copyright 2017 Federico Raimondo and -# Denis A. Engemann +# Authors: Denis A. Engeman +# Alex Rockhill # +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. + +# Copyright 2003-2010 Jürgen Kayser # -# The original CSD Toolbox can be find at +# The original CSD Toolbox can be found at # http://psychophysiology.cpmc.columbia.edu/Software/CSDtoolbox/ - -# Authors: Denis A. Engeman -# Alex Rockhill # -# License: Relicensed under BSD-3-Clause and adapted with -# permission from authors of original GPL code +# Relicensed under BSD-3-Clause and adapted with permission from authors of original GPL +# code. import numpy as np from scipy.optimize import minimize_scalar diff --git a/mne/preprocessing/_css.py b/mne/preprocessing/_css.py index 9132d578331..f3f4ba09f9b 100644 --- a/mne/preprocessing/_css.py +++ b/mne/preprocessing/_css.py @@ -1,4 +1,6 @@ # Author: John Samuelsson +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np diff --git a/mne/preprocessing/_fine_cal.py b/mne/preprocessing/_fine_cal.py index 1b89b758e9a..ca14c4de7e8 100644 --- a/mne/preprocessing/_fine_cal.py +++ b/mne/preprocessing/_fine_cal.py @@ -1,6 +1,7 @@ # Authors: Eric Larson # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from collections import defaultdict from functools import partial diff --git a/mne/preprocessing/_peak_finder.py b/mne/preprocessing/_peak_finder.py index ec43aeb2ff4..c1808397991 100644 --- a/mne/preprocessing/_peak_finder.py +++ b/mne/preprocessing/_peak_finder.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np from ..utils import _pl, logger, verbose diff --git a/mne/preprocessing/_regress.py b/mne/preprocessing/_regress.py index f748f7020be..31a842f7d4f 100644 --- a/mne/preprocessing/_regress.py +++ b/mne/preprocessing/_regress.py @@ -2,6 +2,7 @@ # Marijn van Vliet # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np diff --git a/mne/preprocessing/artifact_detection.py b/mne/preprocessing/artifact_detection.py index 2a34401734a..eadd54a260e 100644 --- a/mne/preprocessing/artifact_detection.py +++ b/mne/preprocessing/artifact_detection.py @@ -1,6 +1,7 @@ # Authors: Adonay Nunes # Luke Bloy # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np diff --git a/mne/preprocessing/bads.py b/mne/preprocessing/bads.py index 077145ff7c8..839f5774b80 100644 --- a/mne/preprocessing/bads.py +++ b/mne/preprocessing/bads.py @@ -1,5 +1,6 @@ # Authors: Denis Engemann # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np from scipy.stats import zscore diff --git a/mne/preprocessing/ctps_.py b/mne/preprocessing/ctps_.py index 37f67aa8e79..bbe99055752 100644 --- a/mne/preprocessing/ctps_.py +++ b/mne/preprocessing/ctps_.py @@ -1,7 +1,8 @@ # Authors: Juergen Dammers # Denis Engemann # -# License: Simplified BSD +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import math import numpy as np diff --git a/mne/preprocessing/ecg.py b/mne/preprocessing/ecg.py index 4ba1a4c0d1d..d773f72ba41 100644 --- a/mne/preprocessing/ecg.py +++ b/mne/preprocessing/ecg.py @@ -3,6 +3,7 @@ # Eric Larson # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np diff --git a/mne/preprocessing/eog.py b/mne/preprocessing/eog.py index a169ada87f3..2cd209a9b5f 100644 --- a/mne/preprocessing/eog.py +++ b/mne/preprocessing/eog.py @@ -3,6 +3,7 @@ # Eric Larson # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np diff --git a/mne/preprocessing/eyetracking/__init__.py b/mne/preprocessing/eyetracking/__init__.py index 41c30c0bc8d..01a30bf4436 100644 --- a/mne/preprocessing/eyetracking/__init__.py +++ b/mne/preprocessing/eyetracking/__init__.py @@ -3,6 +3,7 @@ # Authors: Dominik Welke # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from .eyetracking import set_channel_types_eyetrack from .calibration import Calibration, read_eyelink_calibration diff --git a/mne/preprocessing/eyetracking/_pupillometry.py b/mne/preprocessing/eyetracking/_pupillometry.py index 859e413ce6a..b1d544f24ab 100644 --- a/mne/preprocessing/eyetracking/_pupillometry.py +++ b/mne/preprocessing/eyetracking/_pupillometry.py @@ -1,6 +1,7 @@ # Authors: Scott Huberty # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np diff --git a/mne/preprocessing/eyetracking/calibration.py b/mne/preprocessing/eyetracking/calibration.py index 1891ebacb30..e405e72f9eb 100644 --- a/mne/preprocessing/eyetracking/calibration.py +++ b/mne/preprocessing/eyetracking/calibration.py @@ -4,6 +4,7 @@ # Eric Larson # Adapted from: https://github.com/pyeparse/pyeparse # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from copy import deepcopy diff --git a/mne/preprocessing/eyetracking/eyetracking.py b/mne/preprocessing/eyetracking/eyetracking.py index 96c49cb16b1..ab3d51c6af1 100644 --- a/mne/preprocessing/eyetracking/eyetracking.py +++ b/mne/preprocessing/eyetracking/eyetracking.py @@ -2,6 +2,7 @@ # Scott Huberty # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np diff --git a/mne/preprocessing/eyetracking/tests/__init__.py b/mne/preprocessing/eyetracking/tests/__init__.py index e69de29bb2d..bdfc6e7b46e 100644 --- a/mne/preprocessing/eyetracking/tests/__init__.py +++ b/mne/preprocessing/eyetracking/tests/__init__.py @@ -0,0 +1,2 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. diff --git a/mne/preprocessing/eyetracking/tests/test_calibration.py b/mne/preprocessing/eyetracking/tests/test_calibration.py index 26320688ed5..ac50024310f 100644 --- a/mne/preprocessing/eyetracking/tests/test_calibration.py +++ b/mne/preprocessing/eyetracking/tests/test_calibration.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np import pytest diff --git a/mne/preprocessing/eyetracking/tests/test_pupillometry.py b/mne/preprocessing/eyetracking/tests/test_pupillometry.py index 5bdd4866a53..bda1cf09f75 100644 --- a/mne/preprocessing/eyetracking/tests/test_pupillometry.py +++ b/mne/preprocessing/eyetracking/tests/test_pupillometry.py @@ -1,5 +1,6 @@ # Authors: Scott Huberty -# License: BSD +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np import pytest diff --git a/mne/preprocessing/hfc.py b/mne/preprocessing/hfc.py index 71870330dcc..ab396ca0c44 100644 --- a/mne/preprocessing/hfc.py +++ b/mne/preprocessing/hfc.py @@ -1,6 +1,7 @@ # Authors: George O'Neill # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np diff --git a/mne/preprocessing/ica.py b/mne/preprocessing/ica.py index f12c937bd72..64667185330 100644 --- a/mne/preprocessing/ica.py +++ b/mne/preprocessing/ica.py @@ -4,6 +4,7 @@ # Juergen Dammers # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import json import math diff --git a/mne/preprocessing/ieeg/__init__.py b/mne/preprocessing/ieeg/__init__.py index b906102d570..f12ea5ee1ab 100644 --- a/mne/preprocessing/ieeg/__init__.py +++ b/mne/preprocessing/ieeg/__init__.py @@ -3,6 +3,7 @@ # Authors: Alex Rockhill # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from ._projection import project_sensors_onto_brain from ._volume import make_montage_volume, warp_montage diff --git a/mne/preprocessing/ieeg/_projection.py b/mne/preprocessing/ieeg/_projection.py index 72292dd577d..779fd279ca9 100644 --- a/mne/preprocessing/ieeg/_projection.py +++ b/mne/preprocessing/ieeg/_projection.py @@ -1,6 +1,7 @@ # Authors: Alex Rockhill # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from itertools import combinations diff --git a/mne/preprocessing/ieeg/_volume.py b/mne/preprocessing/ieeg/_volume.py index 0e35e69de6d..4db6f4c29e5 100644 --- a/mne/preprocessing/ieeg/_volume.py +++ b/mne/preprocessing/ieeg/_volume.py @@ -1,6 +1,7 @@ # Authors: Alex Rockhill # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np diff --git a/mne/preprocessing/ieeg/tests/test_projection.py b/mne/preprocessing/ieeg/tests/test_projection.py index bd1ebffbc06..feffe863f65 100644 --- a/mne/preprocessing/ieeg/tests/test_projection.py +++ b/mne/preprocessing/ieeg/tests/test_projection.py @@ -2,6 +2,7 @@ # Authors: Alex Rockhill # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import os from shutil import copyfile diff --git a/mne/preprocessing/ieeg/tests/test_volume.py b/mne/preprocessing/ieeg/tests/test_volume.py index d08df4ecd30..066337cee76 100644 --- a/mne/preprocessing/ieeg/tests/test_volume.py +++ b/mne/preprocessing/ieeg/tests/test_volume.py @@ -2,6 +2,7 @@ # Authors: Alex Rockhill # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np import pytest diff --git a/mne/preprocessing/infomax_.py b/mne/preprocessing/infomax_.py index 556a7b4e4ad..9b2841caa20 100644 --- a/mne/preprocessing/infomax_.py +++ b/mne/preprocessing/infomax_.py @@ -3,6 +3,7 @@ # Denis A. Engeman # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import math diff --git a/mne/preprocessing/interpolate.py b/mne/preprocessing/interpolate.py index 830f0bfb57a..8e69f364a10 100644 --- a/mne/preprocessing/interpolate.py +++ b/mne/preprocessing/interpolate.py @@ -1,6 +1,8 @@ """Tools for data interpolation.""" # Authors: Alexandre Gramfort +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. from itertools import chain diff --git a/mne/preprocessing/maxfilter.py b/mne/preprocessing/maxfilter.py index 54fbaf532e2..64a48b68cf3 100644 --- a/mne/preprocessing/maxfilter.py +++ b/mne/preprocessing/maxfilter.py @@ -3,6 +3,7 @@ # Martin Luessi # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import os diff --git a/mne/preprocessing/maxwell.py b/mne/preprocessing/maxwell.py index 37521b0abeb..8ee465ddb8e 100644 --- a/mne/preprocessing/maxwell.py +++ b/mne/preprocessing/maxwell.py @@ -4,6 +4,7 @@ # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from collections import Counter, OrderedDict from functools import partial diff --git a/mne/preprocessing/nirs/__init__.py b/mne/preprocessing/nirs/__init__.py index 7e364929021..7e003c6dded 100644 --- a/mne/preprocessing/nirs/__init__.py +++ b/mne/preprocessing/nirs/__init__.py @@ -5,6 +5,7 @@ # Alexandre Gramfort # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from .nirs import ( short_channels, diff --git a/mne/preprocessing/nirs/_beer_lambert_law.py b/mne/preprocessing/nirs/_beer_lambert_law.py index 52ee73c13e8..f6f17a1ae04 100644 --- a/mne/preprocessing/nirs/_beer_lambert_law.py +++ b/mne/preprocessing/nirs/_beer_lambert_law.py @@ -3,6 +3,7 @@ # Alexandre Gramfort # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import os.path as op diff --git a/mne/preprocessing/nirs/_optical_density.py b/mne/preprocessing/nirs/_optical_density.py index a296ff94dec..c9b9c513960 100644 --- a/mne/preprocessing/nirs/_optical_density.py +++ b/mne/preprocessing/nirs/_optical_density.py @@ -3,6 +3,7 @@ # Alexandre Gramfort # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np diff --git a/mne/preprocessing/nirs/_scalp_coupling_index.py b/mne/preprocessing/nirs/_scalp_coupling_index.py index 23fefb49ea6..89751c6ebdb 100644 --- a/mne/preprocessing/nirs/_scalp_coupling_index.py +++ b/mne/preprocessing/nirs/_scalp_coupling_index.py @@ -3,6 +3,7 @@ # Alexandre Gramfort # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np diff --git a/mne/preprocessing/nirs/_tddr.py b/mne/preprocessing/nirs/_tddr.py index 4cc4587bedf..59c2ec926d9 100644 --- a/mne/preprocessing/nirs/_tddr.py +++ b/mne/preprocessing/nirs/_tddr.py @@ -2,6 +2,7 @@ # Frank Fishburn # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np diff --git a/mne/preprocessing/nirs/nirs.py b/mne/preprocessing/nirs/nirs.py index 2a1d821b4a6..b6a69aac312 100644 --- a/mne/preprocessing/nirs/nirs.py +++ b/mne/preprocessing/nirs/nirs.py @@ -3,6 +3,7 @@ # Alexandre Gramfort # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import re diff --git a/mne/preprocessing/nirs/tests/test_beer_lambert_law.py b/mne/preprocessing/nirs/tests/test_beer_lambert_law.py index 6fe7fd96803..29dd6b3bd4d 100644 --- a/mne/preprocessing/nirs/tests/test_beer_lambert_law.py +++ b/mne/preprocessing/nirs/tests/test_beer_lambert_law.py @@ -3,6 +3,7 @@ # Alexandre Gramfort # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np import pytest diff --git a/mne/preprocessing/nirs/tests/test_nirs.py b/mne/preprocessing/nirs/tests/test_nirs.py index 43850482c9c..16df22183b8 100644 --- a/mne/preprocessing/nirs/tests/test_nirs.py +++ b/mne/preprocessing/nirs/tests/test_nirs.py @@ -3,6 +3,7 @@ # Alexandre Gramfort # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np import pytest diff --git a/mne/preprocessing/nirs/tests/test_optical_density.py b/mne/preprocessing/nirs/tests/test_optical_density.py index b25fcc4a8cc..77d7a559bb9 100644 --- a/mne/preprocessing/nirs/tests/test_optical_density.py +++ b/mne/preprocessing/nirs/tests/test_optical_density.py @@ -3,6 +3,7 @@ # Alexandre Gramfort # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np import pytest as pytest diff --git a/mne/preprocessing/nirs/tests/test_scalp_coupling_index.py b/mne/preprocessing/nirs/tests/test_scalp_coupling_index.py index 240b50e8048..a9cfa4b1549 100644 --- a/mne/preprocessing/nirs/tests/test_scalp_coupling_index.py +++ b/mne/preprocessing/nirs/tests/test_scalp_coupling_index.py @@ -3,6 +3,7 @@ # Alexandre Gramfort # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np import pytest diff --git a/mne/preprocessing/nirs/tests/test_temporal_derivative_distribution_repair.py b/mne/preprocessing/nirs/tests/test_temporal_derivative_distribution_repair.py index c89d3180908..c3bf3492d03 100644 --- a/mne/preprocessing/nirs/tests/test_temporal_derivative_distribution_repair.py +++ b/mne/preprocessing/nirs/tests/test_temporal_derivative_distribution_repair.py @@ -1,6 +1,7 @@ # Authors: Robert Luke # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np import pytest diff --git a/mne/preprocessing/otp.py b/mne/preprocessing/otp.py index b110c0903c8..6cbd3822641 100644 --- a/mne/preprocessing/otp.py +++ b/mne/preprocessing/otp.py @@ -2,6 +2,7 @@ # Eric Larson # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from functools import partial diff --git a/mne/preprocessing/realign.py b/mne/preprocessing/realign.py index 09442ca9b1c..eee8947b0d2 100644 --- a/mne/preprocessing/realign.py +++ b/mne/preprocessing/realign.py @@ -2,6 +2,7 @@ # Qian Chu # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np from numpy.polynomial.polynomial import Polynomial diff --git a/mne/preprocessing/ssp.py b/mne/preprocessing/ssp.py index 82c5b78a741..985a30a6e9d 100644 --- a/mne/preprocessing/ssp.py +++ b/mne/preprocessing/ssp.py @@ -3,6 +3,7 @@ # Martin Luessi # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import copy as cp diff --git a/mne/preprocessing/stim.py b/mne/preprocessing/stim.py index a9d00dd66cc..2a095b73809 100644 --- a/mne/preprocessing/stim.py +++ b/mne/preprocessing/stim.py @@ -1,6 +1,7 @@ # Authors: Daniel Strohmeier # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np from scipy.interpolate import interp1d diff --git a/mne/preprocessing/tests/__init__.py b/mne/preprocessing/tests/__init__.py index e69de29bb2d..bdfc6e7b46e 100644 --- a/mne/preprocessing/tests/__init__.py +++ b/mne/preprocessing/tests/__init__.py @@ -0,0 +1,2 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. diff --git a/mne/preprocessing/tests/test_annotate_amplitude.py b/mne/preprocessing/tests/test_annotate_amplitude.py index 9eb35084b09..d39fabdb3ce 100644 --- a/mne/preprocessing/tests/test_annotate_amplitude.py +++ b/mne/preprocessing/tests/test_annotate_amplitude.py @@ -1,6 +1,7 @@ # Author: Mathieu Scheltienne # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import datetime import itertools diff --git a/mne/preprocessing/tests/test_annotate_nan.py b/mne/preprocessing/tests/test_annotate_nan.py index b5d4ba7b22e..48e8e95ce00 100644 --- a/mne/preprocessing/tests/test_annotate_nan.py +++ b/mne/preprocessing/tests/test_annotate_nan.py @@ -1,6 +1,7 @@ # Author: Stefan Appelhoff # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from pathlib import Path diff --git a/mne/preprocessing/tests/test_artifact_detection.py b/mne/preprocessing/tests/test_artifact_detection.py index 19094068019..af01fa4416d 100644 --- a/mne/preprocessing/tests/test_artifact_detection.py +++ b/mne/preprocessing/tests/test_artifact_detection.py @@ -1,6 +1,7 @@ # Author: Adonay Nunes # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np import pytest diff --git a/mne/preprocessing/tests/test_csd.py b/mne/preprocessing/tests/test_csd.py index 5ab63b098ff..31d3c64e5de 100644 --- a/mne/preprocessing/tests/test_csd.py +++ b/mne/preprocessing/tests/test_csd.py @@ -5,6 +5,7 @@ # Authors: Alex Rockhill # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from pathlib import Path diff --git a/mne/preprocessing/tests/test_css.py b/mne/preprocessing/tests/test_css.py index 88f196ee969..d6890908331 100644 --- a/mne/preprocessing/tests/test_css.py +++ b/mne/preprocessing/tests/test_css.py @@ -1,4 +1,6 @@ # Author: John G Samuelsson +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np diff --git a/mne/preprocessing/tests/test_ctps.py b/mne/preprocessing/tests/test_ctps.py index ec7918ac72a..20ec189229c 100644 --- a/mne/preprocessing/tests/test_ctps.py +++ b/mne/preprocessing/tests/test_ctps.py @@ -1,6 +1,7 @@ # Authors: Denis A. Engemann # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np import pytest diff --git a/mne/preprocessing/tests/test_ecg.py b/mne/preprocessing/tests/test_ecg.py index b540f0b2895..283009de5f1 100644 --- a/mne/preprocessing/tests/test_ecg.py +++ b/mne/preprocessing/tests/test_ecg.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. from pathlib import Path import numpy as np diff --git a/mne/preprocessing/tests/test_eeglab_infomax.py b/mne/preprocessing/tests/test_eeglab_infomax.py index dd98fc080da..f0835099c96 100644 --- a/mne/preprocessing/tests/test_eeglab_infomax.py +++ b/mne/preprocessing/tests/test_eeglab_infomax.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. from pathlib import Path import numpy as np diff --git a/mne/preprocessing/tests/test_eog.py b/mne/preprocessing/tests/test_eog.py index 439322859e0..ad977cd581a 100644 --- a/mne/preprocessing/tests/test_eog.py +++ b/mne/preprocessing/tests/test_eog.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. from pathlib import Path from mne import Annotations diff --git a/mne/preprocessing/tests/test_fine_cal.py b/mne/preprocessing/tests/test_fine_cal.py index d4adb0c1280..95c9e7d63ba 100644 --- a/mne/preprocessing/tests/test_fine_cal.py +++ b/mne/preprocessing/tests/test_fine_cal.py @@ -2,6 +2,7 @@ # Eric Larson # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np import pytest from numpy.testing import assert_allclose diff --git a/mne/preprocessing/tests/test_hfc.py b/mne/preprocessing/tests/test_hfc.py index 46ecc49037f..50157bc8551 100644 --- a/mne/preprocessing/tests/test_hfc.py +++ b/mne/preprocessing/tests/test_hfc.py @@ -1,6 +1,7 @@ # Authors: George O'Neill # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from pathlib import Path diff --git a/mne/preprocessing/tests/test_ica.py b/mne/preprocessing/tests/test_ica.py index 1625d5d493c..d96cfbfcbc9 100644 --- a/mne/preprocessing/tests/test_ica.py +++ b/mne/preprocessing/tests/test_ica.py @@ -2,6 +2,7 @@ # Alexandre Gramfort # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import os import shutil diff --git a/mne/preprocessing/tests/test_infomax.py b/mne/preprocessing/tests/test_infomax.py index 647f3de53fa..94cb4713ddc 100644 --- a/mne/preprocessing/tests/test_infomax.py +++ b/mne/preprocessing/tests/test_infomax.py @@ -1,6 +1,7 @@ # Authors: Denis A. Engemann # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # Parts of this code are taken from scikit-learn diff --git a/mne/preprocessing/tests/test_interpolate.py b/mne/preprocessing/tests/test_interpolate.py index 01309dbb250..b2b05446c84 100644 --- a/mne/preprocessing/tests/test_interpolate.py +++ b/mne/preprocessing/tests/test_interpolate.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import itertools from pathlib import Path diff --git a/mne/preprocessing/tests/test_maxwell.py b/mne/preprocessing/tests/test_maxwell.py index f6466047f40..4bfd5cd396c 100644 --- a/mne/preprocessing/tests/test_maxwell.py +++ b/mne/preprocessing/tests/test_maxwell.py @@ -1,6 +1,7 @@ # Author: Mark Wronkiewicz # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import pathlib import re diff --git a/mne/preprocessing/tests/test_otp.py b/mne/preprocessing/tests/test_otp.py index ae10d683d2e..9b050ca2dba 100644 --- a/mne/preprocessing/tests/test_otp.py +++ b/mne/preprocessing/tests/test_otp.py @@ -1,6 +1,7 @@ # Authors: Eric Larson # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np import pytest diff --git a/mne/preprocessing/tests/test_peak_finder.py b/mne/preprocessing/tests/test_peak_finder.py index 0ba97893d67..19aaa0fc385 100644 --- a/mne/preprocessing/tests/test_peak_finder.py +++ b/mne/preprocessing/tests/test_peak_finder.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np import pytest from numpy.testing import assert_array_equal, assert_equal diff --git a/mne/preprocessing/tests/test_realign.py b/mne/preprocessing/tests/test_realign.py index 6ab16276290..60ec5b0d5ba 100644 --- a/mne/preprocessing/tests/test_realign.py +++ b/mne/preprocessing/tests/test_realign.py @@ -2,6 +2,7 @@ # Qian Chu # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np import pytest diff --git a/mne/preprocessing/tests/test_regress.py b/mne/preprocessing/tests/test_regress.py index 860481207a4..8050b6bebf7 100644 --- a/mne/preprocessing/tests/test_regress.py +++ b/mne/preprocessing/tests/test_regress.py @@ -1,6 +1,7 @@ # Author: Eric Larson # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np import pytest diff --git a/mne/preprocessing/tests/test_ssp.py b/mne/preprocessing/tests/test_ssp.py index 181629541bf..fdcf4f9db23 100644 --- a/mne/preprocessing/tests/test_ssp.py +++ b/mne/preprocessing/tests/test_ssp.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. from pathlib import Path import numpy as np diff --git a/mne/preprocessing/tests/test_stim.py b/mne/preprocessing/tests/test_stim.py index e4934488a45..2ef1c6e367a 100644 --- a/mne/preprocessing/tests/test_stim.py +++ b/mne/preprocessing/tests/test_stim.py @@ -1,6 +1,7 @@ # Authors: Daniel Strohmeier # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from pathlib import Path diff --git a/mne/preprocessing/tests/test_xdawn.py b/mne/preprocessing/tests/test_xdawn.py index 59822799853..31e751acb37 100644 --- a/mne/preprocessing/tests/test_xdawn.py +++ b/mne/preprocessing/tests/test_xdawn.py @@ -2,6 +2,7 @@ # Jean-Remi King # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from pathlib import Path diff --git a/mne/preprocessing/xdawn.py b/mne/preprocessing/xdawn.py index aed801068ca..ffb0cb0e5cd 100644 --- a/mne/preprocessing/xdawn.py +++ b/mne/preprocessing/xdawn.py @@ -3,6 +3,7 @@ # Jean-Remi King # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np from scipy import linalg diff --git a/mne/proj.py b/mne/proj.py index e8079d151b9..a5bb406b844 100644 --- a/mne/proj.py +++ b/mne/proj.py @@ -1,6 +1,7 @@ # Authors: Alexandre Gramfort # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np diff --git a/mne/rank.py b/mne/rank.py index 34284db50de..539f897a253 100644 --- a/mne/rank.py +++ b/mne/rank.py @@ -2,6 +2,7 @@ # Authors: Alexandre Gramfort # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np from scipy import linalg diff --git a/mne/report/__init__.py b/mne/report/__init__.py index 0037b496551..71cde5609f2 100644 --- a/mne/report/__init__.py +++ b/mne/report/__init__.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. """Report-generation functions and classes.""" import lazy_loader as lazy diff --git a/mne/report/js_and_css/bootstrap-icons/gen_css_for_mne.py b/mne/report/js_and_css/bootstrap-icons/gen_css_for_mne.py index 4b0626ab1e0..95b99c306f7 100644 --- a/mne/report/js_and_css/bootstrap-icons/gen_css_for_mne.py +++ b/mne/report/js_and_css/bootstrap-icons/gen_css_for_mne.py @@ -13,6 +13,7 @@ # Author: Richard Höchenberger # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import base64 diff --git a/mne/report/report.py b/mne/report/report.py index 18ca4be3768..6a37f095c2f 100644 --- a/mne/report/report.py +++ b/mne/report/report.py @@ -5,6 +5,7 @@ # Teon Brooks # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import base64 import dataclasses diff --git a/mne/report/tests/test_report.py b/mne/report/tests/test_report.py index 67b065ad4fe..4f307367b6a 100644 --- a/mne/report/tests/test_report.py +++ b/mne/report/tests/test_report.py @@ -2,6 +2,7 @@ # Teon Brooks # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import base64 import copy diff --git a/mne/simulation/__init__.py b/mne/simulation/__init__.py index cfefe8658ac..3abcf5a34d0 100644 --- a/mne/simulation/__init__.py +++ b/mne/simulation/__init__.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. """Data simulation code.""" import lazy_loader as lazy diff --git a/mne/simulation/_metrics.py b/mne/simulation/_metrics.py index 5c8e1b8617d..89e0283cc2f 100644 --- a/mne/simulation/_metrics.py +++ b/mne/simulation/_metrics.py @@ -2,6 +2,7 @@ # Mark Wronkiewicz # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np diff --git a/mne/simulation/evoked.py b/mne/simulation/evoked.py index 53112df695e..a96c9c134fe 100644 --- a/mne/simulation/evoked.py +++ b/mne/simulation/evoked.py @@ -3,6 +3,7 @@ # Martin Luessi # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import math import numpy as np diff --git a/mne/simulation/metrics/__init__.py b/mne/simulation/metrics/__init__.py index da1648f433a..436551abdfc 100644 --- a/mne/simulation/metrics/__init__.py +++ b/mne/simulation/metrics/__init__.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. """Metrics module for compute stc-based metrics.""" from .metrics import ( diff --git a/mne/simulation/metrics/metrics.py b/mne/simulation/metrics/metrics.py index 4d904623b2e..745b0485d48 100644 --- a/mne/simulation/metrics/metrics.py +++ b/mne/simulation/metrics/metrics.py @@ -3,7 +3,8 @@ # Kostiantyn Maksymenko # Alexandre Gramfort # -# License: BSD (3-clause) +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. from functools import partial diff --git a/mne/simulation/metrics/tests/__init__.py b/mne/simulation/metrics/tests/__init__.py index e69de29bb2d..bdfc6e7b46e 100644 --- a/mne/simulation/metrics/tests/__init__.py +++ b/mne/simulation/metrics/tests/__init__.py @@ -0,0 +1,2 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. diff --git a/mne/simulation/metrics/tests/test_metrics.py b/mne/simulation/metrics/tests/test_metrics.py index a9abcd9ffad..328a30c88fb 100644 --- a/mne/simulation/metrics/tests/test_metrics.py +++ b/mne/simulation/metrics/tests/test_metrics.py @@ -1,7 +1,8 @@ # Authors: Kostiantyn Maksymenko # Alexandre Gramfort # -# License: BSD (3-clause) +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np diff --git a/mne/simulation/raw.py b/mne/simulation/raw.py index 37b15aa1dbc..5e2a00c060f 100644 --- a/mne/simulation/raw.py +++ b/mne/simulation/raw.py @@ -3,6 +3,7 @@ # Eric Larson # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import os from collections.abc import Iterable diff --git a/mne/simulation/source.py b/mne/simulation/source.py index 62f14ebe67c..f87c9b420de 100644 --- a/mne/simulation/source.py +++ b/mne/simulation/source.py @@ -7,6 +7,7 @@ # Ivana Kojcic # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np diff --git a/mne/simulation/tests/__init__.py b/mne/simulation/tests/__init__.py index e69de29bb2d..bdfc6e7b46e 100644 --- a/mne/simulation/tests/__init__.py +++ b/mne/simulation/tests/__init__.py @@ -0,0 +1,2 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. diff --git a/mne/simulation/tests/test_evoked.py b/mne/simulation/tests/test_evoked.py index 3fb21933fdb..bc33d6195a2 100644 --- a/mne/simulation/tests/test_evoked.py +++ b/mne/simulation/tests/test_evoked.py @@ -1,6 +1,7 @@ # Author: Alexandre Gramfort # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from pathlib import Path diff --git a/mne/simulation/tests/test_metrics.py b/mne/simulation/tests/test_metrics.py index d5902660b5f..378c7908743 100644 --- a/mne/simulation/tests/test_metrics.py +++ b/mne/simulation/tests/test_metrics.py @@ -2,6 +2,7 @@ # Mark Wronkiewicz # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np import pytest diff --git a/mne/simulation/tests/test_raw.py b/mne/simulation/tests/test_raw.py index 3fa07537956..bf4caf3bdeb 100644 --- a/mne/simulation/tests/test_raw.py +++ b/mne/simulation/tests/test_raw.py @@ -3,6 +3,7 @@ # Eric Larson # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from copy import deepcopy from pathlib import Path diff --git a/mne/simulation/tests/test_source.py b/mne/simulation/tests/test_source.py index 96ae9d7630e..d8cc42fec3b 100644 --- a/mne/simulation/tests/test_source.py +++ b/mne/simulation/tests/test_source.py @@ -2,6 +2,7 @@ # Samuel Deslauriers-Gauthier # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np import pytest diff --git a/mne/source_estimate.py b/mne/source_estimate.py index afdb5085c6e..efc5a06515a 100644 --- a/mne/source_estimate.py +++ b/mne/source_estimate.py @@ -4,6 +4,7 @@ # Mads Jensen # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import contextlib import copy diff --git a/mne/source_space/__init__.py b/mne/source_space/__init__.py index eca8b7f74c9..d1370283776 100644 --- a/mne/source_space/__init__.py +++ b/mne/source_space/__init__.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. """Forward modeling code.""" import lazy_loader as lazy diff --git a/mne/source_space/_source_space.py b/mne/source_space/_source_space.py index 7368cf4a0d5..3cfedb9d7a1 100644 --- a/mne/source_space/_source_space.py +++ b/mne/source_space/_source_space.py @@ -2,6 +2,7 @@ # Alexandre Gramfort # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # Many of the computations in this code were derived from Matti Hämäläinen's # C code. diff --git a/mne/source_space/tests/__init__.py b/mne/source_space/tests/__init__.py index e69de29bb2d..bdfc6e7b46e 100644 --- a/mne/source_space/tests/__init__.py +++ b/mne/source_space/tests/__init__.py @@ -0,0 +1,2 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. diff --git a/mne/source_space/tests/test_source_space.py b/mne/source_space/tests/test_source_space.py index afccc567074..4db0286a2a5 100644 --- a/mne/source_space/tests/test_source_space.py +++ b/mne/source_space/tests/test_source_space.py @@ -2,6 +2,7 @@ # Eric Larson # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from pathlib import Path from shutil import copytree diff --git a/mne/stats/__init__.py b/mne/stats/__init__.py index 7c4f1454a9b..ebf535d1339 100644 --- a/mne/stats/__init__.py +++ b/mne/stats/__init__.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. """Functions for statistical analysis.""" import lazy_loader as lazy diff --git a/mne/stats/_adjacency.py b/mne/stats/_adjacency.py index 516733c8aed..14e527a7428 100644 --- a/mne/stats/_adjacency.py +++ b/mne/stats/_adjacency.py @@ -2,6 +2,7 @@ # Stefan Appelhoff # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np from scipy import sparse diff --git a/mne/stats/cluster_level.py b/mne/stats/cluster_level.py index d428df8cef8..479bba3f45b 100644 --- a/mne/stats/cluster_level.py +++ b/mne/stats/cluster_level.py @@ -7,7 +7,8 @@ # Denis Engemann # Fernando Perez (bin_perm_rep function) # -# License: Simplified BSD +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np from scipy import ndimage, sparse diff --git a/mne/stats/multi_comp.py b/mne/stats/multi_comp.py index 0f7f911452e..228b2f90a2a 100644 --- a/mne/stats/multi_comp.py +++ b/mne/stats/multi_comp.py @@ -4,6 +4,7 @@ # Code borrowed from statsmodels # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np diff --git a/mne/stats/parametric.py b/mne/stats/parametric.py index 68e424e1a2f..e777bd7f53e 100644 --- a/mne/stats/parametric.py +++ b/mne/stats/parametric.py @@ -2,7 +2,8 @@ # Denis Engemann # Eric Larson # -# License: Simplified BSD +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. from functools import reduce from string import ascii_uppercase diff --git a/mne/stats/permutations.py b/mne/stats/permutations.py index ac983208670..3f515559c72 100644 --- a/mne/stats/permutations.py +++ b/mne/stats/permutations.py @@ -2,7 +2,8 @@ # Authors: Alexandre Gramfort # -# License: Simplified BSD +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. from math import sqrt diff --git a/mne/stats/regression.py b/mne/stats/regression.py index 5240f3c61cb..39bd8e63d95 100644 --- a/mne/stats/regression.py +++ b/mne/stats/regression.py @@ -5,6 +5,7 @@ # Marijn van Vliet # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from collections import namedtuple from inspect import isgenerator diff --git a/mne/stats/tests/__init__.py b/mne/stats/tests/__init__.py index e69de29bb2d..bdfc6e7b46e 100644 --- a/mne/stats/tests/__init__.py +++ b/mne/stats/tests/__init__.py @@ -0,0 +1,2 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. diff --git a/mne/stats/tests/test_adjacency.py b/mne/stats/tests/test_adjacency.py index 9c3ec8a3133..8a1e8b15aca 100644 --- a/mne/stats/tests/test_adjacency.py +++ b/mne/stats/tests/test_adjacency.py @@ -1,6 +1,7 @@ # Authors: Eric Larson # -# License: Simplified BSD +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np import pytest diff --git a/mne/stats/tests/test_cluster_level.py b/mne/stats/tests/test_cluster_level.py index 7f185042329..d0fe0672bde 100644 --- a/mne/stats/tests/test_cluster_level.py +++ b/mne/stats/tests/test_cluster_level.py @@ -2,6 +2,7 @@ # Alexandre Gramfort # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import os from functools import partial diff --git a/mne/stats/tests/test_multi_comp.py b/mne/stats/tests/test_multi_comp.py index 6bd2edd4f2a..e411d37c0a0 100644 --- a/mne/stats/tests/test_multi_comp.py +++ b/mne/stats/tests/test_multi_comp.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np import pytest from numpy.testing import assert_allclose, assert_almost_equal, assert_array_equal diff --git a/mne/stats/tests/test_parametric.py b/mne/stats/tests/test_parametric.py index 698a4b9271e..e1d64583777 100644 --- a/mne/stats/tests/test_parametric.py +++ b/mne/stats/tests/test_parametric.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. from functools import partial from itertools import product diff --git a/mne/stats/tests/test_permutations.py b/mne/stats/tests/test_permutations.py index 891b0fa8529..34694bbb142 100644 --- a/mne/stats/tests/test_permutations.py +++ b/mne/stats/tests/test_permutations.py @@ -1,6 +1,7 @@ # Authors: Alexandre Gramfort # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np import pytest diff --git a/mne/stats/tests/test_regression.py b/mne/stats/tests/test_regression.py index d36bd75f65b..dab09d4693f 100644 --- a/mne/stats/tests/test_regression.py +++ b/mne/stats/tests/test_regression.py @@ -3,6 +3,7 @@ # Jona Sassenhagen # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np import pytest diff --git a/mne/surface.py b/mne/surface.py index 93d9f89caca..285d6ab0be1 100644 --- a/mne/surface.py +++ b/mne/surface.py @@ -4,6 +4,7 @@ # Denis A. Engemann # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # Many of the computations in this code were derived from Matti Hämäläinen's # C code. diff --git a/mne/tests/__init__.py b/mne/tests/__init__.py index e69de29bb2d..bdfc6e7b46e 100644 --- a/mne/tests/__init__.py +++ b/mne/tests/__init__.py @@ -0,0 +1,2 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. diff --git a/mne/tests/test_annotations.py b/mne/tests/test_annotations.py index 10325544fcc..35ffca3d09a 100644 --- a/mne/tests/test_annotations.py +++ b/mne/tests/test_annotations.py @@ -2,6 +2,7 @@ # Robert Luke # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import sys from collections import OrderedDict diff --git a/mne/tests/test_bem.py b/mne/tests/test_bem.py index 82c40553622..0dd682606f6 100644 --- a/mne/tests/test_bem.py +++ b/mne/tests/test_bem.py @@ -1,6 +1,7 @@ # Authors: Marijn van Vliet # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import re from copy import deepcopy diff --git a/mne/tests/test_chpi.py b/mne/tests/test_chpi.py index 21145445ed9..3e0e3fb1e87 100644 --- a/mne/tests/test_chpi.py +++ b/mne/tests/test_chpi.py @@ -1,6 +1,7 @@ # Author: Eric Larson # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from pathlib import Path diff --git a/mne/tests/test_coreg.py b/mne/tests/test_coreg.py index b106b14c4b2..af5801114a9 100644 --- a/mne/tests/test_coreg.py +++ b/mne/tests/test_coreg.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import os from functools import reduce from glob import glob diff --git a/mne/tests/test_cov.py b/mne/tests/test_cov.py index dcbe1b30ad9..5398c07ace7 100644 --- a/mne/tests/test_cov.py +++ b/mne/tests/test_cov.py @@ -2,6 +2,7 @@ # Denis Engemann # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import itertools as itt import sys diff --git a/mne/tests/test_defaults.py b/mne/tests/test_defaults.py index 235f8457623..2a490ad19ee 100644 --- a/mne/tests/test_defaults.py +++ b/mne/tests/test_defaults.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. from copy import deepcopy import pytest diff --git a/mne/tests/test_dipole.py b/mne/tests/test_dipole.py index f4a1215a128..73aaeb7ad68 100644 --- a/mne/tests/test_dipole.py +++ b/mne/tests/test_dipole.py @@ -1,6 +1,7 @@ # Author: Eric Larson # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import os diff --git a/mne/tests/test_docstring_parameters.py b/mne/tests/test_docstring_parameters.py index 181a757d0d2..222165901a3 100644 --- a/mne/tests/test_docstring_parameters.py +++ b/mne/tests/test_docstring_parameters.py @@ -1,6 +1,7 @@ # Author: Eric Larson # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import importlib import inspect diff --git a/mne/tests/test_epochs.py b/mne/tests/test_epochs.py index e5ac3892ca8..0200fb45f79 100644 --- a/mne/tests/test_epochs.py +++ b/mne/tests/test_epochs.py @@ -3,6 +3,7 @@ # Stefan Appelhoff # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import pickle from copy import deepcopy diff --git a/mne/tests/test_event.py b/mne/tests/test_event.py index 3f2b8137345..0d6ad7e0416 100644 --- a/mne/tests/test_event.py +++ b/mne/tests/test_event.py @@ -2,6 +2,7 @@ # Eric Larson # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import os from pathlib import Path diff --git a/mne/tests/test_evoked.py b/mne/tests/test_evoked.py index fe3f41f141c..8c1dd5631c4 100644 --- a/mne/tests/test_evoked.py +++ b/mne/tests/test_evoked.py @@ -4,6 +4,7 @@ # Mads Jensen # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import pickle from copy import deepcopy diff --git a/mne/tests/test_filter.py b/mne/tests/test_filter.py index f2b5ec1b2e7..110a8f136c3 100644 --- a/mne/tests/test_filter.py +++ b/mne/tests/test_filter.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np import pytest from numpy.fft import fft, fftfreq diff --git a/mne/tests/test_freesurfer.py b/mne/tests/test_freesurfer.py index f60993bddf1..e6d75484ca9 100644 --- a/mne/tests/test_freesurfer.py +++ b/mne/tests/test_freesurfer.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. from pathlib import Path import numpy as np diff --git a/mne/tests/test_import_nesting.py b/mne/tests/test_import_nesting.py index f4fe63d1469..d597d730cc1 100644 --- a/mne/tests/test_import_nesting.py +++ b/mne/tests/test_import_nesting.py @@ -1,6 +1,7 @@ # Author: Eric Larson # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import ast import glob diff --git a/mne/tests/test_label.py b/mne/tests/test_label.py index a3b4f74435c..35e41d91f6c 100644 --- a/mne/tests/test_label.py +++ b/mne/tests/test_label.py @@ -1,6 +1,7 @@ # Author: Eric Larson # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import glob import os diff --git a/mne/tests/test_line_endings.py b/mne/tests/test_line_endings.py index 6dcacf554a2..5c91c29fd9a 100644 --- a/mne/tests/test_line_endings.py +++ b/mne/tests/test_line_endings.py @@ -2,6 +2,7 @@ # Adapted from vispy # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import os import sys diff --git a/mne/tests/test_misc.py b/mne/tests/test_misc.py index 55a6ce41067..887d45a6ffb 100644 --- a/mne/tests/test_misc.py +++ b/mne/tests/test_misc.py @@ -1,6 +1,7 @@ # Authors: Alexandre Gramfort # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from pathlib import Path diff --git a/mne/tests/test_morph.py b/mne/tests/test_morph.py index f26da4ca6b2..90b9f99382a 100644 --- a/mne/tests/test_morph.py +++ b/mne/tests/test_morph.py @@ -1,6 +1,7 @@ # Author: Tommy Clausner # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from inspect import signature import numpy as np diff --git a/mne/tests/test_morph_map.py b/mne/tests/test_morph_map.py index a5dfe563027..67c5d74b01b 100644 --- a/mne/tests/test_morph_map.py +++ b/mne/tests/test_morph_map.py @@ -1,6 +1,7 @@ # Authors: Alexandre Gramfort # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import os from shutil import copyfile diff --git a/mne/tests/test_ola.py b/mne/tests/test_ola.py index ab8935added..966636fbcf7 100644 --- a/mne/tests/test_ola.py +++ b/mne/tests/test_ola.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np import pytest from numpy.testing import assert_allclose diff --git a/mne/tests/test_parallel.py b/mne/tests/test_parallel.py index 6f90e12de1b..7e7812bcace 100644 --- a/mne/tests/test_parallel.py +++ b/mne/tests/test_parallel.py @@ -1,6 +1,7 @@ # Author: Eric Larson # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import multiprocessing import os diff --git a/mne/tests/test_proj.py b/mne/tests/test_proj.py index 562d4e846f6..f13272dd2c1 100644 --- a/mne/tests/test_proj.py +++ b/mne/tests/test_proj.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import copy as cp from pathlib import Path diff --git a/mne/tests/test_rank.py b/mne/tests/test_rank.py index bde640e276c..fb9efcba615 100644 --- a/mne/tests/test_rank.py +++ b/mne/tests/test_rank.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import itertools from pathlib import Path diff --git a/mne/tests/test_read_vectorview_selection.py b/mne/tests/test_read_vectorview_selection.py index 7e4add1c594..844a30edc5d 100644 --- a/mne/tests/test_read_vectorview_selection.py +++ b/mne/tests/test_read_vectorview_selection.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. from pathlib import Path import pytest diff --git a/mne/tests/test_source_estimate.py b/mne/tests/test_source_estimate.py index 2bfba7ce5fb..8c9e7df9389 100644 --- a/mne/tests/test_source_estimate.py +++ b/mne/tests/test_source_estimate.py @@ -1,5 +1,6 @@ # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import os import re diff --git a/mne/tests/test_surface.py b/mne/tests/test_surface.py index 3513a32bb32..646b2793706 100644 --- a/mne/tests/test_surface.py +++ b/mne/tests/test_surface.py @@ -1,6 +1,7 @@ # Authors: Alexandre Gramfort # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from pathlib import Path diff --git a/mne/tests/test_transforms.py b/mne/tests/test_transforms.py index 36c533a59a0..2246609bdb8 100644 --- a/mne/tests/test_transforms.py +++ b/mne/tests/test_transforms.py @@ -1,6 +1,7 @@ # Author: Alexandre Gramfort # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import itertools import os diff --git a/mne/time_frequency/__init__.py b/mne/time_frequency/__init__.py index c95662bbc26..51189f461d7 100644 --- a/mne/time_frequency/__init__.py +++ b/mne/time_frequency/__init__.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. """Time frequency analysis tools.""" import lazy_loader as lazy diff --git a/mne/time_frequency/_stft.py b/mne/time_frequency/_stft.py index b1fb9c02188..50599947b90 100644 --- a/mne/time_frequency/_stft.py +++ b/mne/time_frequency/_stft.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. from math import ceil import numpy as np diff --git a/mne/time_frequency/_stockwell.py b/mne/time_frequency/_stockwell.py index e8c94be08ad..1abf0c8e5a6 100644 --- a/mne/time_frequency/_stockwell.py +++ b/mne/time_frequency/_stockwell.py @@ -2,6 +2,8 @@ # Alexandre Gramfort # # License : BSD-3-Clause +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. from copy import deepcopy diff --git a/mne/time_frequency/ar.py b/mne/time_frequency/ar.py index 0baccac31f0..db632a77aa0 100644 --- a/mne/time_frequency/ar.py +++ b/mne/time_frequency/ar.py @@ -2,6 +2,7 @@ # The statsmodels folks for AR yule_walker # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np from scipy import linalg diff --git a/mne/time_frequency/csd.py b/mne/time_frequency/csd.py index e3499f45e2e..ed395137103 100644 --- a/mne/time_frequency/csd.py +++ b/mne/time_frequency/csd.py @@ -3,6 +3,7 @@ # Roman Goj # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import copy as cp import numbers diff --git a/mne/time_frequency/multitaper.py b/mne/time_frequency/multitaper.py index 5cb42e7b9b7..c6af2b20c60 100644 --- a/mne/time_frequency/multitaper.py +++ b/mne/time_frequency/multitaper.py @@ -1,5 +1,7 @@ # Author : Martin Luessi mluessi@nmr.mgh.harvard.edu (2012) # License : BSD-3-Clause +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. # Parts of this code were copied from NiTime http://nipy.sourceforge.net/nitime diff --git a/mne/time_frequency/psd.py b/mne/time_frequency/psd.py index 79b269b645e..33bcd16df8c 100644 --- a/mne/time_frequency/psd.py +++ b/mne/time_frequency/psd.py @@ -1,6 +1,8 @@ # Authors : Alexandre Gramfort, alexandre.gramfort@inria.fr (2011) # Denis A. Engemann # License : BSD-3-Clause +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. from functools import partial diff --git a/mne/time_frequency/spectrum.py b/mne/time_frequency/spectrum.py index c39d5f8a078..aa459124347 100644 --- a/mne/time_frequency/spectrum.py +++ b/mne/time_frequency/spectrum.py @@ -3,6 +3,7 @@ # Authors: Dan McCloy # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from copy import deepcopy from functools import partial diff --git a/mne/time_frequency/tests/__init__.py b/mne/time_frequency/tests/__init__.py index e69de29bb2d..bdfc6e7b46e 100644 --- a/mne/time_frequency/tests/__init__.py +++ b/mne/time_frequency/tests/__init__.py @@ -0,0 +1,2 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. diff --git a/mne/time_frequency/tests/test_ar.py b/mne/time_frequency/tests/test_ar.py index 7d1d06d46dc..bef37e7dd18 100644 --- a/mne/time_frequency/tests/test_ar.py +++ b/mne/time_frequency/tests/test_ar.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. from pathlib import Path import numpy as np diff --git a/mne/time_frequency/tests/test_csd.py b/mne/time_frequency/tests/test_csd.py index 3763e6bdbb4..aefc3e2aaac 100644 --- a/mne/time_frequency/tests/test_csd.py +++ b/mne/time_frequency/tests/test_csd.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import pickle from itertools import product from os import path as op diff --git a/mne/time_frequency/tests/test_multitaper.py b/mne/time_frequency/tests/test_multitaper.py index f67aea1bf54..9ff16159bf4 100644 --- a/mne/time_frequency/tests/test_multitaper.py +++ b/mne/time_frequency/tests/test_multitaper.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np import pytest from numpy.testing import assert_array_almost_equal diff --git a/mne/time_frequency/tests/test_psd.py b/mne/time_frequency/tests/test_psd.py index 3631363083d..e02e561384f 100644 --- a/mne/time_frequency/tests/test_psd.py +++ b/mne/time_frequency/tests/test_psd.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np import pytest from numpy.testing import assert_allclose, assert_array_almost_equal, assert_array_equal diff --git a/mne/time_frequency/tests/test_spectrum.py b/mne/time_frequency/tests/test_spectrum.py index 75768aff130..58e3309bcc8 100644 --- a/mne/time_frequency/tests/test_spectrum.py +++ b/mne/time_frequency/tests/test_spectrum.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. from functools import partial import numpy as np diff --git a/mne/time_frequency/tests/test_stft.py b/mne/time_frequency/tests/test_stft.py index 4829d975aad..4e9fb0ece34 100644 --- a/mne/time_frequency/tests/test_stft.py +++ b/mne/time_frequency/tests/test_stft.py @@ -2,6 +2,8 @@ # Eric Larson # # License : BSD-3-Clause +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np import pytest diff --git a/mne/time_frequency/tests/test_stockwell.py b/mne/time_frequency/tests/test_stockwell.py index dcb202a959a..96b2c064801 100644 --- a/mne/time_frequency/tests/test_stockwell.py +++ b/mne/time_frequency/tests/test_stockwell.py @@ -2,6 +2,8 @@ # Alexandre Gramfort # # License : BSD-3-Clause +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. from pathlib import Path diff --git a/mne/time_frequency/tests/test_tfr.py b/mne/time_frequency/tests/test_tfr.py index 58943b10e48..33e82d5a126 100644 --- a/mne/time_frequency/tests/test_tfr.py +++ b/mne/time_frequency/tests/test_tfr.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import datetime import re from itertools import product diff --git a/mne/time_frequency/tfr.py b/mne/time_frequency/tfr.py index bd3a02865c1..ce547568232 100644 --- a/mne/time_frequency/tfr.py +++ b/mne/time_frequency/tfr.py @@ -8,6 +8,8 @@ # Jean-Remi King # # License : BSD-3-Clause +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. from copy import deepcopy from functools import partial diff --git a/mne/transforms.py b/mne/transforms.py index 88b10a3e11f..f0efd287f40 100644 --- a/mne/transforms.py +++ b/mne/transforms.py @@ -4,6 +4,7 @@ # Christian Brodbeck # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import glob import os diff --git a/mne/utils/__init__.py b/mne/utils/__init__.py index f84944aae4a..bb4c2af7739 100644 --- a/mne/utils/__init__.py +++ b/mne/utils/__init__.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import lazy_loader as lazy (__getattr__, __dir__, __all__) = lazy.attach_stub(__name__, __file__) diff --git a/mne/utils/_bunch.py b/mne/utils/_bunch.py index 9b9af35a7ad..0fdac59139f 100644 --- a/mne/utils/_bunch.py +++ b/mne/utils/_bunch.py @@ -4,6 +4,7 @@ # Joan Massich # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from copy import deepcopy diff --git a/mne/utils/_logging.py b/mne/utils/_logging.py index 84c8bcb2e69..1dcb1a5e8a6 100644 --- a/mne/utils/_logging.py +++ b/mne/utils/_logging.py @@ -2,6 +2,7 @@ # Authors: Alexandre Gramfort # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import contextlib import importlib diff --git a/mne/utils/_testing.py b/mne/utils/_testing.py index 4f1a2eaf9b5..999d6242695 100644 --- a/mne/utils/_testing.py +++ b/mne/utils/_testing.py @@ -2,6 +2,7 @@ # Authors: Alexandre Gramfort # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import inspect import os diff --git a/mne/utils/check.py b/mne/utils/check.py index 2faa364b779..eb8e14de256 100644 --- a/mne/utils/check.py +++ b/mne/utils/check.py @@ -2,6 +2,7 @@ # Authors: Alexandre Gramfort # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numbers import operator diff --git a/mne/utils/config.py b/mne/utils/config.py index 0ac71d7d4a5..2b7d10ab070 100644 --- a/mne/utils/config.py +++ b/mne/utils/config.py @@ -2,6 +2,7 @@ # Authors: Eric Larson # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import atexit import json diff --git a/mne/utils/dataframe.py b/mne/utils/dataframe.py index 2c9cbad78af..599a2f88165 100644 --- a/mne/utils/dataframe.py +++ b/mne/utils/dataframe.py @@ -2,6 +2,7 @@ # Authors: Daniel McCloy # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from inspect import signature diff --git a/mne/utils/docs.py b/mne/utils/docs.py index e68b839055d..d8eb668ae04 100644 --- a/mne/utils/docs.py +++ b/mne/utils/docs.py @@ -2,6 +2,7 @@ # Authors: Eric Larson # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import inspect import os diff --git a/mne/utils/fetching.py b/mne/utils/fetching.py index 626f2043330..2e4d39c59f8 100644 --- a/mne/utils/fetching.py +++ b/mne/utils/fetching.py @@ -2,6 +2,7 @@ # Authors: Alexandre Gramfort # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import os diff --git a/mne/utils/linalg.py b/mne/utils/linalg.py index 342850d4417..9b36f0ae1ed 100644 --- a/mne/utils/linalg.py +++ b/mne/utils/linalg.py @@ -20,6 +20,7 @@ # Authors: Eric Larson # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import functools diff --git a/mne/utils/misc.py b/mne/utils/misc.py index 470caf671f2..05d856c0226 100644 --- a/mne/utils/misc.py +++ b/mne/utils/misc.py @@ -2,6 +2,7 @@ # Authors: Alexandre Gramfort # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import fnmatch import gc diff --git a/mne/utils/mixin.py b/mne/utils/mixin.py index e9f30116454..c90121fdfbb 100644 --- a/mne/utils/mixin.py +++ b/mne/utils/mixin.py @@ -2,6 +2,7 @@ # Authors: Alexandre Gramfort # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import json import logging diff --git a/mne/utils/numerics.py b/mne/utils/numerics.py index fa78f24bb7d..64bc4515f93 100644 --- a/mne/utils/numerics.py +++ b/mne/utils/numerics.py @@ -3,6 +3,7 @@ # Clemens Brunner # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import inspect import numbers diff --git a/mne/utils/progressbar.py b/mne/utils/progressbar.py index 220a103a2b3..94f595dd441 100644 --- a/mne/utils/progressbar.py +++ b/mne/utils/progressbar.py @@ -2,6 +2,7 @@ # Authors: Alexandre Gramfort # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import logging import os diff --git a/mne/utils/spectrum.py b/mne/utils/spectrum.py index 2ea3b058c58..5abcb7e3378 100644 --- a/mne/utils/spectrum.py +++ b/mne/utils/spectrum.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. from inspect import currentframe, getargvalues, signature from ..utils import warn diff --git a/mne/utils/tests/test_bunch.py b/mne/utils/tests/test_bunch.py index 93d87387c55..683d8467238 100644 --- a/mne/utils/tests/test_bunch.py +++ b/mne/utils/tests/test_bunch.py @@ -2,6 +2,7 @@ # Eric Larson # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import pickle diff --git a/mne/utils/tests/test_check.py b/mne/utils/tests/test_check.py index 2cda3188cd8..4f5f6d5416b 100644 --- a/mne/utils/tests/test_check.py +++ b/mne/utils/tests/test_check.py @@ -3,6 +3,7 @@ # Stefan Appelhoff # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import os import sys diff --git a/mne/utils/tests/test_config.py b/mne/utils/tests/test_config.py index fe802734f67..ffae55ad08a 100644 --- a/mne/utils/tests/test_config.py +++ b/mne/utils/tests/test_config.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import os import platform import re diff --git a/mne/utils/tests/test_docs.py b/mne/utils/tests/test_docs.py index 7e744202e95..0fd13aa25a5 100644 --- a/mne/utils/tests/test_docs.py +++ b/mne/utils/tests/test_docs.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import webbrowser import pytest diff --git a/mne/utils/tests/test_linalg.py b/mne/utils/tests/test_linalg.py index 3a470c3073b..03050b29f03 100644 --- a/mne/utils/tests/test_linalg.py +++ b/mne/utils/tests/test_linalg.py @@ -2,6 +2,7 @@ # Authors: Britta Westner # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np import pytest diff --git a/mne/utils/tests/test_logging.py b/mne/utils/tests/test_logging.py index a091bea0f83..81613749aaf 100644 --- a/mne/utils/tests/test_logging.py +++ b/mne/utils/tests/test_logging.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import logging import os import re diff --git a/mne/utils/tests/test_misc.py b/mne/utils/tests/test_misc.py index 6892d561777..06b29964dd1 100644 --- a/mne/utils/tests/test_misc.py +++ b/mne/utils/tests/test_misc.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import os import subprocess import sys diff --git a/mne/utils/tests/test_mixin.py b/mne/utils/tests/test_mixin.py index aa13f705f14..b208335fada 100644 --- a/mne/utils/tests/test_mixin.py +++ b/mne/utils/tests/test_mixin.py @@ -1,6 +1,7 @@ # Author: Eric Larson # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import pytest from numpy.testing import assert_allclose diff --git a/mne/utils/tests/test_numerics.py b/mne/utils/tests/test_numerics.py index 71b1a349cd1..12f366776ea 100644 --- a/mne/utils/tests/test_numerics.py +++ b/mne/utils/tests/test_numerics.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. from copy import deepcopy from datetime import datetime, timezone from io import StringIO diff --git a/mne/utils/tests/test_progressbar.py b/mne/utils/tests/test_progressbar.py index 4d2438bb7d5..92d2d37cfc3 100644 --- a/mne/utils/tests/test_progressbar.py +++ b/mne/utils/tests/test_progressbar.py @@ -1,6 +1,7 @@ # Authors: Eric Larson # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from pathlib import Path diff --git a/mne/utils/tests/test_testing.py b/mne/utils/tests/test_testing.py index 1439cbbce61..02791ffc3d8 100644 --- a/mne/utils/tests/test_testing.py +++ b/mne/utils/tests/test_testing.py @@ -1,6 +1,7 @@ # Authors: Eric Larson # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import os.path as op from pathlib import Path diff --git a/mne/viz/_3d.py b/mne/viz/_3d.py index 41d6f0476b7..5f68fd0a46e 100644 --- a/mne/viz/_3d.py +++ b/mne/viz/_3d.py @@ -7,7 +7,8 @@ # Mainak Jas # Mark Wronkiewicz # -# License: Simplified BSD +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import os import os.path as op diff --git a/mne/viz/_3d_overlay.py b/mne/viz/_3d_overlay.py index 8eb7c7313f7..48baff23d1e 100644 --- a/mne/viz/_3d_overlay.py +++ b/mne/viz/_3d_overlay.py @@ -3,7 +3,8 @@ # Authors: Guillaume Favelier # Eric Larson # -# License: Simplified BSD +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. from collections import OrderedDict diff --git a/mne/viz/__init__.py b/mne/viz/__init__.py index f2f295ccf0c..4fea8fd82c9 100644 --- a/mne/viz/__init__.py +++ b/mne/viz/__init__.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. """Visualization routines.""" import lazy_loader as lazy diff --git a/mne/viz/_brain/__init__.py b/mne/viz/_brain/__init__.py index 21b71973622..b65f65c7757 100644 --- a/mne/viz/_brain/__init__.py +++ b/mne/viz/_brain/__init__.py @@ -7,7 +7,8 @@ # jona-sassenhagen # Joan Massich # -# License: Simplified BSD +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. from ._brain import Brain, _LayeredMesh from ._scraper import _BrainScraper diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 6d5719186fd..4725e8664aa 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -5,7 +5,8 @@ # jona-sassenhagen # Joan Massich # -# License: Simplified BSD +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import copy import os diff --git a/mne/viz/_brain/_linkviewer.py b/mne/viz/_brain/_linkviewer.py index df2edb79bd2..ba7140aa9a6 100644 --- a/mne/viz/_brain/_linkviewer.py +++ b/mne/viz/_brain/_linkviewer.py @@ -2,7 +2,8 @@ # Eric Larson # Guillaume Favelier # -# License: Simplified BSD +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np from ...utils import warn diff --git a/mne/viz/_brain/_scraper.py b/mne/viz/_brain/_scraper.py index 08ad985e7b5..41d79ef79ef 100644 --- a/mne/viz/_brain/_scraper.py +++ b/mne/viz/_brain/_scraper.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import os import os.path as op diff --git a/mne/viz/_brain/colormap.py b/mne/viz/_brain/colormap.py index a1a37327ac1..0567e352252 100644 --- a/mne/viz/_brain/colormap.py +++ b/mne/viz/_brain/colormap.py @@ -3,7 +3,8 @@ # Oleh Kozynets # Guillaume Favelier # -# License: Simplified BSD +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np diff --git a/mne/viz/_brain/surface.py b/mne/viz/_brain/surface.py index 919d6f2aa09..f4625c5f019 100644 --- a/mne/viz/_brain/surface.py +++ b/mne/viz/_brain/surface.py @@ -4,7 +4,8 @@ # Guillaume Favelier # jona-sassenhagen # -# License: Simplified BSD +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. from os import path as path diff --git a/mne/viz/_brain/tests/test_brain.py b/mne/viz/_brain/tests/test_brain.py index 1940138d13e..f233f83389d 100644 --- a/mne/viz/_brain/tests/test_brain.py +++ b/mne/viz/_brain/tests/test_brain.py @@ -5,7 +5,8 @@ # Guillaume Favelier # Oleh Kozynets # -# License: Simplified BSD +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import os import platform diff --git a/mne/viz/_brain/tests/test_notebook.py b/mne/viz/_brain/tests/test_notebook.py index f2da02bc467..0fd253357f8 100644 --- a/mne/viz/_brain/tests/test_notebook.py +++ b/mne/viz/_brain/tests/test_notebook.py @@ -1,5 +1,7 @@ # Authors: Guillaume Favelier # Eric Larson +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. # NOTE: Tests in this directory must be self-contained because they are # executed in a separate IPython kernel. diff --git a/mne/viz/_brain/view.py b/mne/viz/_brain/view.py index 2f20d4c3b8d..dfef3605097 100644 --- a/mne/viz/_brain/view.py +++ b/mne/viz/_brain/view.py @@ -4,7 +4,8 @@ # Guillaume Favelier # jona-sassenhagen # -# License: Simplified BSD +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. ORIGIN = "auto" DIST = "auto" diff --git a/mne/viz/_dipole.py b/mne/viz/_dipole.py index 809a7f45876..c22340aa220 100644 --- a/mne/viz/_dipole.py +++ b/mne/viz/_dipole.py @@ -2,7 +2,8 @@ # Authors: Eric Larson # -# License: Simplified BSD +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import os.path as op diff --git a/mne/viz/_figure.py b/mne/viz/_figure.py index 7f958657876..5ccc486dd84 100644 --- a/mne/viz/_figure.py +++ b/mne/viz/_figure.py @@ -3,7 +3,8 @@ # Authors: Daniel McCloy # Martin Schulz # -# License: Simplified BSD +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import importlib from abc import ABC, abstractmethod from collections import OrderedDict diff --git a/mne/viz/_mpl_figure.py b/mne/viz/_mpl_figure.py index b0a059c97cf..9835afa4e2b 100644 --- a/mne/viz/_mpl_figure.py +++ b/mne/viz/_mpl_figure.py @@ -33,7 +33,8 @@ # Authors: Daniel McCloy # -# License: Simplified BSD +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import datetime import platform diff --git a/mne/viz/_proj.py b/mne/viz/_proj.py index 0493d0ce8fb..011efd4b066 100644 --- a/mne/viz/_proj.py +++ b/mne/viz/_proj.py @@ -2,7 +2,8 @@ # Authors: Eric Larson # -# License: Simplified BSD +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. from copy import deepcopy diff --git a/mne/viz/_scraper.py b/mne/viz/_scraper.py index 78a18203d38..01be6c312f1 100644 --- a/mne/viz/_scraper.py +++ b/mne/viz/_scraper.py @@ -1,6 +1,7 @@ # Authors: Eric Larson # -# License: Simplified BSD +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. from contextlib import contextmanager diff --git a/mne/viz/backends/__init__.py b/mne/viz/backends/__init__.py index 0fc69ca1f6a..5e05db0bc2e 100644 --- a/mne/viz/backends/__init__.py +++ b/mne/viz/backends/__init__.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. """Visualization backend.""" from . import renderer diff --git a/mne/viz/backends/_abstract.py b/mne/viz/backends/_abstract.py index f6520244c3a..23cb65c6c44 100644 --- a/mne/viz/backends/_abstract.py +++ b/mne/viz/backends/_abstract.py @@ -4,7 +4,8 @@ # Eric Larson # Alex Rockhill # -# License: Simplified BSD +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import warnings from abc import ABC, abstractclassmethod, abstractmethod diff --git a/mne/viz/backends/_notebook.py b/mne/viz/backends/_notebook.py index 319d294a8a5..6a9e5a6cf8f 100644 --- a/mne/viz/backends/_notebook.py +++ b/mne/viz/backends/_notebook.py @@ -3,7 +3,8 @@ # Authors: Guillaume Favelier # Alex Rockhill # -# License: Simplified BSD +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import os import os.path as op diff --git a/mne/viz/backends/_pyvista.py b/mne/viz/backends/_pyvista.py index 221672d9915..b8fca4c995a 100644 --- a/mne/viz/backends/_pyvista.py +++ b/mne/viz/backends/_pyvista.py @@ -9,7 +9,8 @@ # Guillaume Favelier # Joan Massich # -# License: Simplified BSD +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import platform import re diff --git a/mne/viz/backends/_qt.py b/mne/viz/backends/_qt.py index 7bb87537d10..3f7f28abc1b 100644 --- a/mne/viz/backends/_qt.py +++ b/mne/viz/backends/_qt.py @@ -4,7 +4,8 @@ # Eric Larson # Alex Rockhill # -# License: Simplified BSD +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import os import platform diff --git a/mne/viz/backends/_utils.py b/mne/viz/backends/_utils.py index fc42538335f..d613a909f67 100644 --- a/mne/viz/backends/_utils.py +++ b/mne/viz/backends/_utils.py @@ -4,7 +4,8 @@ # Joan Massich # Guillaume Favelier # -# License: Simplified BSD +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import collections.abc import functools import os diff --git a/mne/viz/backends/renderer.py b/mne/viz/backends/renderer.py index e0120e5ae70..1bb396d165c 100644 --- a/mne/viz/backends/renderer.py +++ b/mne/viz/backends/renderer.py @@ -5,7 +5,8 @@ # Joan Massich # Guillaume Favelier # -# License: Simplified BSD +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import importlib import time diff --git a/mne/viz/backends/tests/_utils.py b/mne/viz/backends/tests/_utils.py index ca4961e784f..e45ccc5ba1f 100644 --- a/mne/viz/backends/tests/_utils.py +++ b/mne/viz/backends/tests/_utils.py @@ -3,7 +3,8 @@ # Joan Massich # Guillaume Favelier # -# License: Simplified BSD +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import warnings diff --git a/mne/viz/backends/tests/test_abstract.py b/mne/viz/backends/tests/test_abstract.py index 4130611a18a..f12e615d0d7 100644 --- a/mne/viz/backends/tests/test_abstract.py +++ b/mne/viz/backends/tests/test_abstract.py @@ -1,6 +1,7 @@ # Authors: Alex Rockhill # -# License: Simplified BSD +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. from pathlib import Path diff --git a/mne/viz/backends/tests/test_renderer.py b/mne/viz/backends/tests/test_renderer.py index 9de7862a597..ec62a1b6748 100644 --- a/mne/viz/backends/tests/test_renderer.py +++ b/mne/viz/backends/tests/test_renderer.py @@ -3,7 +3,8 @@ # Joan Massich # Guillaume Favelier # -# License: Simplified BSD +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import os import platform diff --git a/mne/viz/backends/tests/test_utils.py b/mne/viz/backends/tests/test_utils.py index b5ff72fc584..196eb030cea 100644 --- a/mne/viz/backends/tests/test_utils.py +++ b/mne/viz/backends/tests/test_utils.py @@ -3,7 +3,8 @@ # Joan Massich # Guillaume Favelier # -# License: Simplified BSD +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import platform from colorsys import rgb_to_hls diff --git a/mne/viz/circle.py b/mne/viz/circle.py index 2877bebe382..7f9d16ecc54 100644 --- a/mne/viz/circle.py +++ b/mne/viz/circle.py @@ -4,7 +4,8 @@ # Denis Engemann # Martin Luessi # -# License: Simplified BSD +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. from functools import partial diff --git a/mne/viz/conftest.py b/mne/viz/conftest.py index 0b46923ffc6..66c7f2be7c8 100644 --- a/mne/viz/conftest.py +++ b/mne/viz/conftest.py @@ -3,6 +3,7 @@ # Alexandre Gramfort # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import os.path as op diff --git a/mne/viz/epochs.py b/mne/viz/epochs.py index 22d686c9b95..20dbeed142c 100644 --- a/mne/viz/epochs.py +++ b/mne/viz/epochs.py @@ -9,7 +9,8 @@ # Stefan Repplinger # Daniel McCloy # -# License: Simplified BSD +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. from collections import Counter from copy import deepcopy diff --git a/mne/viz/evoked.py b/mne/viz/evoked.py index 8e42212e11b..11a229d80d1 100644 --- a/mne/viz/evoked.py +++ b/mne/viz/evoked.py @@ -8,7 +8,8 @@ # Mainak Jas # Daniel McCloy # -# License: Simplified BSD +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. from copy import deepcopy from functools import partial diff --git a/mne/viz/evoked_field.py b/mne/viz/evoked_field.py index 3757d2c00dd..dd691fccf3c 100644 --- a/mne/viz/evoked_field.py +++ b/mne/viz/evoked_field.py @@ -2,6 +2,8 @@ author: Marijn van Vliet """ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. from functools import partial import numpy as np diff --git a/mne/viz/eyetracking/__init__.py b/mne/viz/eyetracking/__init__.py index 7de13fd8900..9dfb9a5ec19 100644 --- a/mne/viz/eyetracking/__init__.py +++ b/mne/viz/eyetracking/__init__.py @@ -1,5 +1,6 @@ """Eye-tracking visualization routines.""" # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. from .heatmap import plot_gaze diff --git a/mne/viz/eyetracking/heatmap.py b/mne/viz/eyetracking/heatmap.py index d3ff4756d8d..8cb44ac4931 100644 --- a/mne/viz/eyetracking/heatmap.py +++ b/mne/viz/eyetracking/heatmap.py @@ -1,6 +1,7 @@ # Authors: Scott Huberty # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np from scipy.ndimage import gaussian_filter diff --git a/mne/viz/eyetracking/tests/__init__.py b/mne/viz/eyetracking/tests/__init__.py index e69de29bb2d..bdfc6e7b46e 100644 --- a/mne/viz/eyetracking/tests/__init__.py +++ b/mne/viz/eyetracking/tests/__init__.py @@ -0,0 +1,2 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. diff --git a/mne/viz/eyetracking/tests/test_heatmap.py b/mne/viz/eyetracking/tests/test_heatmap.py index 99103c552b1..a088c1dc7fe 100644 --- a/mne/viz/eyetracking/tests/test_heatmap.py +++ b/mne/viz/eyetracking/tests/test_heatmap.py @@ -1,6 +1,7 @@ # Authors: Scott Huberty # -# License: Simplified BSD +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import matplotlib.pyplot as plt import numpy as np diff --git a/mne/viz/ica.py b/mne/viz/ica.py index 12002b1cd56..dcd585c37fe 100644 --- a/mne/viz/ica.py +++ b/mne/viz/ica.py @@ -5,7 +5,8 @@ # Teon Brooks # Daniel McCloy # -# License: Simplified BSD +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import warnings from functools import partial diff --git a/mne/viz/misc.py b/mne/viz/misc.py index 3b20c9cb572..3d8a9469620 100644 --- a/mne/viz/misc.py +++ b/mne/viz/misc.py @@ -7,7 +7,8 @@ # Cathy Nangini # Mainak Jas # -# License: Simplified BSD +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import copy import io diff --git a/mne/viz/montage.py b/mne/viz/montage.py index afce1ce8dcb..18ff3e1c2d7 100644 --- a/mne/viz/montage.py +++ b/mne/viz/montage.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. """Functions to plot EEG sensor montages or digitizer montages.""" from copy import deepcopy diff --git a/mne/viz/raw.py b/mne/viz/raw.py index cdefc285c19..65bfb08604e 100644 --- a/mne/viz/raw.py +++ b/mne/viz/raw.py @@ -4,7 +4,8 @@ # Jaakko Leppakangas # Daniel McCloy # -# License: Simplified BSD +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. from collections import OrderedDict diff --git a/mne/viz/tests/__init__.py b/mne/viz/tests/__init__.py index e69de29bb2d..bdfc6e7b46e 100644 --- a/mne/viz/tests/__init__.py +++ b/mne/viz/tests/__init__.py @@ -0,0 +1,2 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. diff --git a/mne/viz/tests/test_3d.py b/mne/viz/tests/test_3d.py index 2c763275805..54ebf0fcd83 100644 --- a/mne/viz/tests/test_3d.py +++ b/mne/viz/tests/test_3d.py @@ -5,7 +5,8 @@ # Mainak Jas # Mark Wronkiewicz # -# License: Simplified BSD +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. from pathlib import Path diff --git a/mne/viz/tests/test_3d_mpl.py b/mne/viz/tests/test_3d_mpl.py index ae4f72d4dd3..2b46a688a13 100644 --- a/mne/viz/tests/test_3d_mpl.py +++ b/mne/viz/tests/test_3d_mpl.py @@ -5,7 +5,8 @@ # Mainak Jas # Mark Wronkiewicz # -# License: Simplified BSD +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import re diff --git a/mne/viz/tests/test_circle.py b/mne/viz/tests/test_circle.py index f95aca2c8a5..cf831768291 100644 --- a/mne/viz/tests/test_circle.py +++ b/mne/viz/tests/test_circle.py @@ -2,7 +2,8 @@ # Denis Engemann # Martin Luessi # -# License: Simplified BSD +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import matplotlib diff --git a/mne/viz/tests/test_epochs.py b/mne/viz/tests/test_epochs.py index d3dd90d224d..cf1c07b7d85 100644 --- a/mne/viz/tests/test_epochs.py +++ b/mne/viz/tests/test_epochs.py @@ -5,7 +5,8 @@ # Jaakko Leppakangas # Daniel McCloy # -# License: Simplified BSD +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import platform diff --git a/mne/viz/tests/test_evoked.py b/mne/viz/tests/test_evoked.py index 51b83f222fa..b44a33385b2 100644 --- a/mne/viz/tests/test_evoked.py +++ b/mne/viz/tests/test_evoked.py @@ -7,7 +7,8 @@ # Jona Sassenhagen # Daniel McCloy # -# License: Simplified BSD +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. from pathlib import Path diff --git a/mne/viz/tests/test_figure.py b/mne/viz/tests/test_figure.py index 19d4e163dd1..6a67c7572e2 100644 --- a/mne/viz/tests/test_figure.py +++ b/mne/viz/tests/test_figure.py @@ -1,6 +1,7 @@ # Authors: Daniel McCloy # -# License: Simplified BSD +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import pytest diff --git a/mne/viz/tests/test_ica.py b/mne/viz/tests/test_ica.py index d01542f3da5..421d844e127 100644 --- a/mne/viz/tests/test_ica.py +++ b/mne/viz/tests/test_ica.py @@ -1,7 +1,8 @@ # Authors: Denis Engemann # Alexandre Gramfort # -# License: Simplified BSD +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import sys from pathlib import Path diff --git a/mne/viz/tests/test_misc.py b/mne/viz/tests/test_misc.py index fcc6d9e1566..49d3e7219bb 100644 --- a/mne/viz/tests/test_misc.py +++ b/mne/viz/tests/test_misc.py @@ -5,7 +5,8 @@ # Cathy Nangini # Mainak Jas # -# License: Simplified BSD +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. from pathlib import Path diff --git a/mne/viz/tests/test_montage.py b/mne/viz/tests/test_montage.py index bed0d212a6b..0a95cdbbb55 100644 --- a/mne/viz/tests/test_montage.py +++ b/mne/viz/tests/test_montage.py @@ -2,7 +2,8 @@ # Alexandre Gramfort # Teon Brooks # -# License: Simplified BSD +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. # Set our plotters to test mode diff --git a/mne/viz/tests/test_proj.py b/mne/viz/tests/test_proj.py index 05f9207a75f..36fe9054363 100644 --- a/mne/viz/tests/test_proj.py +++ b/mne/viz/tests/test_proj.py @@ -1,6 +1,7 @@ # Authors: Eric Larson # -# License: Simplified BSD +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np import pytest diff --git a/mne/viz/tests/test_raw.py b/mne/viz/tests/test_raw.py index a4c73e76075..89619d36e2f 100644 --- a/mne/viz/tests/test_raw.py +++ b/mne/viz/tests/test_raw.py @@ -1,6 +1,7 @@ # Authors: Eric Larson # -# License: Simplified BSD +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import itertools import os diff --git a/mne/viz/tests/test_scraper.py b/mne/viz/tests/test_scraper.py index 7a2c8e734b1..ed95428a097 100644 --- a/mne/viz/tests/test_scraper.py +++ b/mne/viz/tests/test_scraper.py @@ -1,6 +1,7 @@ # Authors: Eric Larson # -# License: Simplified BSD +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import os.path as op diff --git a/mne/viz/tests/test_topo.py b/mne/viz/tests/test_topo.py index da4fe116330..fc421136c94 100644 --- a/mne/viz/tests/test_topo.py +++ b/mne/viz/tests/test_topo.py @@ -4,7 +4,8 @@ # Eric Larson # Robert Luke # -# License: Simplified BSD +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. from collections import namedtuple from pathlib import Path diff --git a/mne/viz/tests/test_topomap.py b/mne/viz/tests/test_topomap.py index 972b07ce83c..33ae3cc645c 100644 --- a/mne/viz/tests/test_topomap.py +++ b/mne/viz/tests/test_topomap.py @@ -4,7 +4,8 @@ # Eric Larson # Robert Luke # -# License: Simplified BSD +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. from functools import partial from pathlib import Path diff --git a/mne/viz/tests/test_ui_events.py b/mne/viz/tests/test_ui_events.py index 5061cfa5261..480ac783f8e 100644 --- a/mne/viz/tests/test_ui_events.py +++ b/mne/viz/tests/test_ui_events.py @@ -1,6 +1,7 @@ # Authors: Marijn van Vliet # -# License: Simplified BSD +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import matplotlib.pyplot as plt import pytest diff --git a/mne/viz/tests/test_utils.py b/mne/viz/tests/test_utils.py index 5450e4d5789..f0679563da3 100644 --- a/mne/viz/tests/test_utils.py +++ b/mne/viz/tests/test_utils.py @@ -1,6 +1,7 @@ # Authors: Alexandre Gramfort # -# License: Simplified BSD +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. from pathlib import Path diff --git a/mne/viz/topo.py b/mne/viz/topo.py index 3cae18c3ed7..9c3f7c5bd75 100644 --- a/mne/viz/topo.py +++ b/mne/viz/topo.py @@ -5,7 +5,8 @@ # Martin Luessi # Eric Larson # -# License: Simplified BSD +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. from copy import deepcopy from functools import partial diff --git a/mne/viz/topomap.py b/mne/viz/topomap.py index 11d8ebce8a0..0c2f6f273b0 100644 --- a/mne/viz/topomap.py +++ b/mne/viz/topomap.py @@ -8,7 +8,8 @@ # Mikołaj Magnuski # Marijn van Vliet # -# License: Simplified BSD +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import copy import itertools diff --git a/mne/viz/ui_events.py b/mne/viz/ui_events.py index 5648c90db9e..231776c9165 100644 --- a/mne/viz/ui_events.py +++ b/mne/viz/ui_events.py @@ -9,6 +9,8 @@ Authors: Marijn van Vliet """ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import contextlib import re import weakref diff --git a/mne/viz/utils.py b/mne/viz/utils.py index cfdf5aa62a6..4223bafad6c 100644 --- a/mne/viz/utils.py +++ b/mne/viz/utils.py @@ -9,7 +9,8 @@ # Clemens Brunner # Daniel McCloy # -# License: Simplified BSD +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import difflib import math import os diff --git a/tools/check_mne_location.py b/tools/check_mne_location.py index 8691cad7d72..8dccf9df091 100755 --- a/tools/check_mne_location.py +++ b/tools/check_mne_location.py @@ -1,4 +1,6 @@ #!/usr/bin/env python +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. from pathlib import Path import mne diff --git a/tools/dev/check_steering_committee.py b/tools/dev/check_steering_committee.py index d6d08d16fae..419e9fd4164 100755 --- a/tools/dev/check_steering_committee.py +++ b/tools/dev/check_steering_committee.py @@ -2,6 +2,8 @@ # with read:org, read:user, and read:project # https://docs.github.com/en/enterprise-server@3.6/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens # https://pygithub.readthedocs.io/ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. from collections import Counter import os diff --git a/tools/dev/ensure_headers.py b/tools/dev/ensure_headers.py new file mode 100644 index 00000000000..d56f67ac32b --- /dev/null +++ b/tools/dev/ensure_headers.py @@ -0,0 +1,122 @@ +"""Ensure license and copyright statements are in source files. + +From https://www.bestpractices.dev/en/projects/7783?criteria_level=2: + + The project MUST include a copyright statement in each source file, identifying the + copyright holder (e.g., the [project name] contributors). [copyright_per_file] + This MAY be done by including the following inside a comment near the beginning of + each file: "Copyright the [project name] contributors.". + +And: + + The project MUST include a license statement in each source file. + +This script ensures that we use consistent license naming in consistent locations +toward the top of each file. +""" +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. + +import re +from pathlib import Path + +import numpy as np +from git import Repo + +repo = Repo(Path(__file__).parents[2]) + +LICENSE_LINE = "# License: BSD-3-Clause" +COPYRIGHT_LINE = "# Copyright the MNE-Python contributors." + + +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 + else: + yield Path(entry.path) # entry.type + + +def _ensure_license(lines, path): + # 1. Keep existing + license_idx = np.where([line.startswith("# License: ") for line in lines])[0] + assert len(license_idx) <= 1, len(license_idx) + if len(license_idx): # If existing, ensure it's correct + lines[license_idx[0]] = LICENSE_LINE + return + # 2. First non-comment line after author line + author_idx = np.where([re.match(r"^# Authors? ?: .*$", line) for line in lines])[0] + assert len(author_idx) <= 1, len(author_idx) + if len(author_idx): + insert = author_idx[0] + for extra in range(1, 100): + if not lines[insert + extra].startswith("#"): + break + else: + raise RuntimeError( + "Failed to find non-comment line within 100 of end of author line" + ) + lines.insert(insert + extra, LICENSE_LINE) + return + # 3. First line after docstring + insert = 0 + max_len = 100 + if lines[0].startswith('"""'): + if lines[0].count('"""') != 2: + for insert in range(1, max_len): + if '"""' in lines[insert]: + # Find next non-blank line: + for extra in range(1, 3): # up to 2 blank lines + if lines[insert + extra].strip(): + break + else: + raise RuntimeError( + "Failed to find non-blank line within 2 of end of " + f"docstring at line {insert + 1}" + ) + insert += extra + break + else: + raise RuntimeError( + f"Failed to find end of file docstring within {max_len} lines" + ) + lines.insert(insert, LICENSE_LINE) + return + # 4. First non-comment line + for insert in range(100): + if not lines[insert].startswith("#"): + lines.insert(insert, LICENSE_LINE) + return + else: + raise RuntimeError("Failed to find non-comment line within 100 lines") + + +def _ensure_copyright(lines, path): + n_expected = { + "mne/preprocessing/_csd.py": 2, + "mne/transforms.py": 2, + } + n_copyright = sum(line.startswith("# Copyright ") for line in lines) + assert n_copyright <= n_expected.get(str(path), 1), n_copyright + insert = lines.index(LICENSE_LINE) + 1 + if lines[insert].startswith("# Copyright"): + lines[insert] = COPYRIGHT_LINE + else: + lines.insert(insert, COPYRIGHT_LINE) + assert lines.count(COPYRIGHT_LINE) == 1, lines.count(COPYRIGHT_LINE) + + +for path in get_paths_from_tree(repo.tree()): + if not path.suffix == ".py": + continue + lines = path.read_text("utf-8").split("\n") + # Remove the UTF-8 file coding stuff + orig_lines = list(lines) + if lines[0] == "# -*- coding: utf-8 -*-": + lines = lines[1:] + _ensure_license(lines, path) + _ensure_copyright(lines, path) + if lines != orig_lines: + print(path) + path.write_text("\n".join(lines), "utf-8") diff --git a/tools/dev/generate_pyi_files.py b/tools/dev/generate_pyi_files.py index f7804e895e8..97deb34f837 100644 --- a/tools/dev/generate_pyi_files.py +++ b/tools/dev/generate_pyi_files.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import os import sys from importlib import import_module diff --git a/tools/generate_codemeta.py b/tools/generate_codemeta.py index 2525b7aa9d9..9e697cecc55 100644 --- a/tools/generate_codemeta.py +++ b/tools/generate_codemeta.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. import subprocess import tomllib from argparse import ArgumentParser diff --git a/tutorials/clinical/20_seeg.py b/tutorials/clinical/20_seeg.py index eeec6fd8f8e..cce5f4a089a 100644 --- a/tutorials/clinical/20_seeg.py +++ b/tutorials/clinical/20_seeg.py @@ -35,6 +35,7 @@ # Alex Rockhill # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/tutorials/clinical/30_ecog.py b/tutorials/clinical/30_ecog.py index f59ce6b213f..2ccc2d6cb91 100644 --- a/tutorials/clinical/30_ecog.py +++ b/tutorials/clinical/30_ecog.py @@ -31,6 +31,7 @@ # Liberty Hamilton # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/tutorials/clinical/60_sleep.py b/tutorials/clinical/60_sleep.py index eefeda02070..020d00bab7e 100644 --- a/tutorials/clinical/60_sleep.py +++ b/tutorials/clinical/60_sleep.py @@ -30,6 +30,7 @@ # Joan Massich # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/tutorials/epochs/10_epochs_overview.py b/tutorials/epochs/10_epochs_overview.py index 7778110b6a5..54a99f9f149 100644 --- a/tutorials/epochs/10_epochs_overview.py +++ b/tutorials/epochs/10_epochs_overview.py @@ -16,6 +16,8 @@ As usual we'll start by importing the modules we need: """ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% import mne diff --git a/tutorials/epochs/15_baseline_regression.py b/tutorials/epochs/15_baseline_regression.py index 4f3c3456760..326e075dd04 100644 --- a/tutorials/epochs/15_baseline_regression.py +++ b/tutorials/epochs/15_baseline_regression.py @@ -36,6 +36,7 @@ # Email: carinaforster0611@gmail.com # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np diff --git a/tutorials/epochs/20_visualize_epochs.py b/tutorials/epochs/20_visualize_epochs.py index 4a1606afed5..5fc1a454700 100644 --- a/tutorials/epochs/20_visualize_epochs.py +++ b/tutorials/epochs/20_visualize_epochs.py @@ -12,6 +12,8 @@ We'll start by importing the modules we need, loading the continuous (raw) sample data, and cropping it to save memory: """ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% import mne diff --git a/tutorials/epochs/30_epochs_metadata.py b/tutorials/epochs/30_epochs_metadata.py index 214178fc275..7d5c06871ad 100644 --- a/tutorials/epochs/30_epochs_metadata.py +++ b/tutorials/epochs/30_epochs_metadata.py @@ -17,6 +17,8 @@ need and loading the data: """ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% import numpy as np diff --git a/tutorials/epochs/40_autogenerate_metadata.py b/tutorials/epochs/40_autogenerate_metadata.py index 025277901d1..8f7f3f5a90e 100644 --- a/tutorials/epochs/40_autogenerate_metadata.py +++ b/tutorials/epochs/40_autogenerate_metadata.py @@ -42,6 +42,8 @@ by calling `mne.events_from_annotations`. """ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% from pathlib import Path diff --git a/tutorials/epochs/50_epochs_to_data_frame.py b/tutorials/epochs/50_epochs_to_data_frame.py index 66c4443e4a9..e86c1111809 100644 --- a/tutorials/epochs/50_epochs_to_data_frame.py +++ b/tutorials/epochs/50_epochs_to_data_frame.py @@ -16,6 +16,8 @@ need and loading the data: """ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% import matplotlib.pyplot as plt diff --git a/tutorials/epochs/60_make_fixed_length_epochs.py b/tutorials/epochs/60_make_fixed_length_epochs.py index 108435c092e..9ca097deab3 100644 --- a/tutorials/epochs/60_make_fixed_length_epochs.py +++ b/tutorials/epochs/60_make_fixed_length_epochs.py @@ -28,6 +28,8 @@ $ pip install mne-connectivity """ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% import matplotlib.pyplot as plt diff --git a/tutorials/evoked/10_evoked_overview.py b/tutorials/evoked/10_evoked_overview.py index b8fff3e71ba..a2513ea1e27 100644 --- a/tutorials/evoked/10_evoked_overview.py +++ b/tutorials/evoked/10_evoked_overview.py @@ -15,6 +15,8 @@ As usual, we start by importing the modules we need: """ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% import mne diff --git a/tutorials/evoked/20_visualize_evoked.py b/tutorials/evoked/20_visualize_evoked.py index 4f7e2ba4967..f19c3863453 100644 --- a/tutorials/evoked/20_visualize_evoked.py +++ b/tutorials/evoked/20_visualize_evoked.py @@ -11,6 +11,8 @@ As usual we'll start by importing the modules we need: """ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% import numpy as np diff --git a/tutorials/evoked/30_eeg_erp.py b/tutorials/evoked/30_eeg_erp.py index c6fcae797af..7c542476b84 100644 --- a/tutorials/evoked/30_eeg_erp.py +++ b/tutorials/evoked/30_eeg_erp.py @@ -17,6 +17,8 @@ we'll crop the raw data from ~4.5 minutes down to 90 seconds. """ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% import matplotlib.pyplot as plt diff --git a/tutorials/evoked/40_whitened.py b/tutorials/evoked/40_whitened.py index 3f63b03350e..84c989541dc 100644 --- a/tutorials/evoked/40_whitened.py +++ b/tutorials/evoked/40_whitened.py @@ -16,6 +16,8 @@ that we'll consider to be noise. """ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% import mne diff --git a/tutorials/forward/10_background_freesurfer.py b/tutorials/forward/10_background_freesurfer.py index 07614c7a11b..900e3c720ee 100644 --- a/tutorials/forward/10_background_freesurfer.py +++ b/tutorials/forward/10_background_freesurfer.py @@ -85,6 +85,8 @@ :footcite:`DestrieuxEtAl2010`). """ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% import mne diff --git a/tutorials/forward/20_source_alignment.py b/tutorials/forward/20_source_alignment.py index 83ad252bfda..c1ff697f9ce 100644 --- a/tutorials/forward/20_source_alignment.py +++ b/tutorials/forward/20_source_alignment.py @@ -13,6 +13,8 @@ Let's start out by loading some data. """ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% import nibabel as nib diff --git a/tutorials/forward/25_automated_coreg.py b/tutorials/forward/25_automated_coreg.py index df8cdc87bc9..b25da7a083a 100644 --- a/tutorials/forward/25_automated_coreg.py +++ b/tutorials/forward/25_automated_coreg.py @@ -20,6 +20,7 @@ # Guillaume Favelier # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import numpy as np diff --git a/tutorials/forward/30_forward.py b/tutorials/forward/30_forward.py index d1fa3430897..b280895e3f1 100644 --- a/tutorials/forward/30_forward.py +++ b/tutorials/forward/30_forward.py @@ -11,6 +11,8 @@ modeling, see :ref:`ch_forward`. """ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% import mne diff --git a/tutorials/forward/35_eeg_no_mri.py b/tutorials/forward/35_eeg_no_mri.py index 504bc3b080b..81a96ed24b3 100644 --- a/tutorials/forward/35_eeg_no_mri.py +++ b/tutorials/forward/35_eeg_no_mri.py @@ -22,6 +22,7 @@ # Eric Larson # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import os.path as op diff --git a/tutorials/forward/50_background_freesurfer_mne.py b/tutorials/forward/50_background_freesurfer_mne.py index 1afd6a5a278..1d71f94606d 100644 --- a/tutorials/forward/50_background_freesurfer_mne.py +++ b/tutorials/forward/50_background_freesurfer_mne.py @@ -16,6 +16,8 @@ readable on top of an MRI image. """ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% import matplotlib.patheffects as path_effects diff --git a/tutorials/forward/80_fix_bem_in_blender.py b/tutorials/forward/80_fix_bem_in_blender.py index 74ea1c69349..948aaa3653b 100644 --- a/tutorials/forward/80_fix_bem_in_blender.py +++ b/tutorials/forward/80_fix_bem_in_blender.py @@ -21,6 +21,7 @@ # Manorama Kadwani # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/tutorials/forward/90_compute_covariance.py b/tutorials/forward/90_compute_covariance.py index 20992900e73..637d02ddbbe 100644 --- a/tutorials/forward/90_compute_covariance.py +++ b/tutorials/forward/90_compute_covariance.py @@ -13,6 +13,8 @@ :ref:`minimum_norm_estimates`. """ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% import mne diff --git a/tutorials/intro/10_overview.py b/tutorials/intro/10_overview.py index e45e64085ba..20dc532f65a 100644 --- a/tutorials/intro/10_overview.py +++ b/tutorials/intro/10_overview.py @@ -14,6 +14,8 @@ We begin by importing the necessary Python modules: """ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% import numpy as np diff --git a/tutorials/intro/15_inplace.py b/tutorials/intro/15_inplace.py index 9d040555909..2c7f6261569 100644 --- a/tutorials/intro/15_inplace.py +++ b/tutorials/intro/15_inplace.py @@ -17,6 +17,8 @@ :ref:`example data `: """ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% import mne diff --git a/tutorials/intro/20_events_from_raw.py b/tutorials/intro/20_events_from_raw.py index e448368a209..cddbe7cf57e 100644 --- a/tutorials/intro/20_events_from_raw.py +++ b/tutorials/intro/20_events_from_raw.py @@ -26,6 +26,8 @@ to just 60 seconds before loading it into RAM: """ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% import numpy as np diff --git a/tutorials/intro/30_info.py b/tutorials/intro/30_info.py index 2df1c17e87b..3f8c0dc624b 100644 --- a/tutorials/intro/30_info.py +++ b/tutorials/intro/30_info.py @@ -14,6 +14,8 @@ `: """ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% import mne diff --git a/tutorials/intro/40_sensor_locations.py b/tutorials/intro/40_sensor_locations.py index 86fefe1bb80..e41ca2cff21 100644 --- a/tutorials/intro/40_sensor_locations.py +++ b/tutorials/intro/40_sensor_locations.py @@ -9,6 +9,8 @@ MNE-Python handles physical locations of sensors. As usual we'll start by importing the modules we need: """ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% from pathlib import Path diff --git a/tutorials/intro/50_configure_mne.py b/tutorials/intro/50_configure_mne.py index 758726bed97..a8fa106986e 100644 --- a/tutorials/intro/50_configure_mne.py +++ b/tutorials/intro/50_configure_mne.py @@ -11,6 +11,8 @@ We begin by importing the necessary Python modules: """ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% import os diff --git a/tutorials/intro/70_report.py b/tutorials/intro/70_report.py index bbbb4ab2abf..926e278838d 100644 --- a/tutorials/intro/70_report.py +++ b/tutorials/intro/70_report.py @@ -20,6 +20,8 @@ building a report. As usual, we will start by importing the modules and data we need: """ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% import tempfile diff --git a/tutorials/inverse/10_stc_class.py b/tutorials/inverse/10_stc_class.py index 30e34f6326d..4330daa41ed 100644 --- a/tutorials/inverse/10_stc_class.py +++ b/tutorials/inverse/10_stc_class.py @@ -52,6 +52,8 @@ is. We first set up the environment and load some data: """ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% from mne import read_source_estimate diff --git a/tutorials/inverse/20_dipole_fit.py b/tutorials/inverse/20_dipole_fit.py index 567c7d9b75e..f12e5968546 100644 --- a/tutorials/inverse/20_dipole_fit.py +++ b/tutorials/inverse/20_dipole_fit.py @@ -11,6 +11,8 @@ `this gist `__. """ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% import matplotlib.pyplot as plt diff --git a/tutorials/inverse/30_mne_dspm_loreta.py b/tutorials/inverse/30_mne_dspm_loreta.py index af3dc2a8e53..1f7af45eec3 100644 --- a/tutorials/inverse/30_mne_dspm_loreta.py +++ b/tutorials/inverse/30_mne_dspm_loreta.py @@ -9,6 +9,8 @@ minimum-norm inverse method on evoked/raw/epochs data. """ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% import matplotlib.pyplot as plt diff --git a/tutorials/inverse/35_dipole_orientations.py b/tutorials/inverse/35_dipole_orientations.py index 26a9b5713c3..6e2260e3fdb 100644 --- a/tutorials/inverse/35_dipole_orientations.py +++ b/tutorials/inverse/35_dipole_orientations.py @@ -15,6 +15,8 @@ See :ref:`inverse_orientation_constraints` for related information. """ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% # Load data # --------- diff --git a/tutorials/inverse/40_mne_fixed_free.py b/tutorials/inverse/40_mne_fixed_free.py index c91815b513d..5a30fe73cdb 100644 --- a/tutorials/inverse/40_mne_fixed_free.py +++ b/tutorials/inverse/40_mne_fixed_free.py @@ -12,6 +12,7 @@ # Author: Eric Larson # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/tutorials/inverse/50_beamformer_lcmv.py b/tutorials/inverse/50_beamformer_lcmv.py index 9015949c250..9c7bc7d73be 100644 --- a/tutorials/inverse/50_beamformer_lcmv.py +++ b/tutorials/inverse/50_beamformer_lcmv.py @@ -12,6 +12,7 @@ # Eric Larson # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/tutorials/inverse/60_visualize_stc.py b/tutorials/inverse/60_visualize_stc.py index 2d06089c846..d9c7cd58134 100644 --- a/tutorials/inverse/60_visualize_stc.py +++ b/tutorials/inverse/60_visualize_stc.py @@ -12,6 +12,8 @@ First, we get the paths for the evoked data and the source time courses (stcs). """ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% import matplotlib.pyplot as plt diff --git a/tutorials/inverse/70_eeg_mri_coords.py b/tutorials/inverse/70_eeg_mri_coords.py index 0586b3084c1..9783435f26c 100644 --- a/tutorials/inverse/70_eeg_mri_coords.py +++ b/tutorials/inverse/70_eeg_mri_coords.py @@ -12,6 +12,7 @@ # Authors: Eric Larson # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/tutorials/inverse/80_brainstorm_phantom_elekta.py b/tutorials/inverse/80_brainstorm_phantom_elekta.py index 40585254aea..8184badeda3 100644 --- a/tutorials/inverse/80_brainstorm_phantom_elekta.py +++ b/tutorials/inverse/80_brainstorm_phantom_elekta.py @@ -15,6 +15,7 @@ # Authors: Eric Larson # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/tutorials/inverse/85_brainstorm_phantom_ctf.py b/tutorials/inverse/85_brainstorm_phantom_ctf.py index 857ff7397fe..8ef188487d6 100644 --- a/tutorials/inverse/85_brainstorm_phantom_ctf.py +++ b/tutorials/inverse/85_brainstorm_phantom_ctf.py @@ -18,6 +18,7 @@ # Authors: Eric Larson # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/tutorials/inverse/90_phantom_4DBTi.py b/tutorials/inverse/90_phantom_4DBTi.py index 1efc932dab5..69cb4a85bd6 100644 --- a/tutorials/inverse/90_phantom_4DBTi.py +++ b/tutorials/inverse/90_phantom_4DBTi.py @@ -15,6 +15,7 @@ # Authors: Alex Gramfort # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/tutorials/inverse/95_phantom_KIT.py b/tutorials/inverse/95_phantom_KIT.py index af259b9ad79..444ae4635fd 100644 --- a/tutorials/inverse/95_phantom_KIT.py +++ b/tutorials/inverse/95_phantom_KIT.py @@ -12,6 +12,7 @@ # Authors: Eric Larson # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% import matplotlib.pyplot as plt diff --git a/tutorials/io/10_reading_meg_data.py b/tutorials/io/10_reading_meg_data.py index 18fd458a45e..14cc31ff8b5 100644 --- a/tutorials/io/10_reading_meg_data.py +++ b/tutorials/io/10_reading_meg_data.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. r""" .. _tut-imorting-meg-data: diff --git a/tutorials/io/20_reading_eeg_data.py b/tutorials/io/20_reading_eeg_data.py index e95794fb35c..94f4f644ccc 100644 --- a/tutorials/io/20_reading_eeg_data.py +++ b/tutorials/io/20_reading_eeg_data.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. r""" .. _tut-imorting-eeg-data: diff --git a/tutorials/io/30_reading_fnirs_data.py b/tutorials/io/30_reading_fnirs_data.py index e87b0efa81b..017664be61e 100644 --- a/tutorials/io/30_reading_fnirs_data.py +++ b/tutorials/io/30_reading_fnirs_data.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. r""" .. _tut-importing-fnirs-data: diff --git a/tutorials/io/60_ctf_bst_auditory.py b/tutorials/io/60_ctf_bst_auditory.py index 20ef8d32534..dd8d9abadf5 100644 --- a/tutorials/io/60_ctf_bst_auditory.py +++ b/tutorials/io/60_ctf_bst_auditory.py @@ -26,6 +26,7 @@ # Jaakko Leppakangas # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/tutorials/io/70_reading_eyetracking_data.py b/tutorials/io/70_reading_eyetracking_data.py index c800b918323..781b89fab9e 100644 --- a/tutorials/io/70_reading_eyetracking_data.py +++ b/tutorials/io/70_reading_eyetracking_data.py @@ -1,4 +1,5 @@ -# -*- coding: utf-8 -*- +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. r""" .. _tut-importing-eyetracking-data: diff --git a/tutorials/machine-learning/30_strf.py b/tutorials/machine-learning/30_strf.py index a1c184d4afb..a838ae0018c 100644 --- a/tutorials/machine-learning/30_strf.py +++ b/tutorials/machine-learning/30_strf.py @@ -16,6 +16,7 @@ # Eric Larson # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/tutorials/machine-learning/50_decoding.py b/tutorials/machine-learning/50_decoding.py index c99ad50cb83..06d34bd49c8 100644 --- a/tutorials/machine-learning/50_decoding.py +++ b/tutorials/machine-learning/50_decoding.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. r""" .. _tut-mvpa: diff --git a/tutorials/preprocessing/10_preprocessing_overview.py b/tutorials/preprocessing/10_preprocessing_overview.py index ce2c77e3d84..483ac653767 100644 --- a/tutorials/preprocessing/10_preprocessing_overview.py +++ b/tutorials/preprocessing/10_preprocessing_overview.py @@ -12,6 +12,8 @@ :ref:`example data `: """ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% import os diff --git a/tutorials/preprocessing/15_handling_bad_channels.py b/tutorials/preprocessing/15_handling_bad_channels.py index 7385506bf34..daac97976a5 100644 --- a/tutorials/preprocessing/15_handling_bad_channels.py +++ b/tutorials/preprocessing/15_handling_bad_channels.py @@ -12,6 +12,8 @@ data: """ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% import os diff --git a/tutorials/preprocessing/20_rejecting_bad_data.py b/tutorials/preprocessing/20_rejecting_bad_data.py index 99228eb37d7..d478255b048 100644 --- a/tutorials/preprocessing/20_rejecting_bad_data.py +++ b/tutorials/preprocessing/20_rejecting_bad_data.py @@ -17,6 +17,8 @@ array to use when converting the continuous data to epochs: """ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% import os diff --git a/tutorials/preprocessing/25_background_filtering.py b/tutorials/preprocessing/25_background_filtering.py index aac46559850..948ab43d76f 100644 --- a/tutorials/preprocessing/25_background_filtering.py +++ b/tutorials/preprocessing/25_background_filtering.py @@ -1,3 +1,5 @@ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. r""" .. _disc-filtering: diff --git a/tutorials/preprocessing/30_filtering_resampling.py b/tutorials/preprocessing/30_filtering_resampling.py index 13d5f199c6f..530b92741f6 100644 --- a/tutorials/preprocessing/30_filtering_resampling.py +++ b/tutorials/preprocessing/30_filtering_resampling.py @@ -13,6 +13,8 @@ (to save memory on the documentation server): """ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% import os diff --git a/tutorials/preprocessing/35_artifact_correction_regression.py b/tutorials/preprocessing/35_artifact_correction_regression.py index e328648a33c..30416ec0f41 100644 --- a/tutorials/preprocessing/35_artifact_correction_regression.py +++ b/tutorials/preprocessing/35_artifact_correction_regression.py @@ -38,6 +38,8 @@ blink artifacts, especially during the presentation of visual stimuli. """ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% import numpy as np diff --git a/tutorials/preprocessing/40_artifact_correction_ica.py b/tutorials/preprocessing/40_artifact_correction_ica.py index 72e61a69454..6f21840fa30 100644 --- a/tutorials/preprocessing/40_artifact_correction_ica.py +++ b/tutorials/preprocessing/40_artifact_correction_ica.py @@ -18,6 +18,8 @@ and classes from that submodule: """ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% import os diff --git a/tutorials/preprocessing/45_projectors_background.py b/tutorials/preprocessing/45_projectors_background.py index 5c25fc798f3..0b11d168db4 100644 --- a/tutorials/preprocessing/45_projectors_background.py +++ b/tutorials/preprocessing/45_projectors_background.py @@ -14,6 +14,8 @@ function to make it easier to make several plots that look similar: """ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% import os diff --git a/tutorials/preprocessing/50_artifact_correction_ssp.py b/tutorials/preprocessing/50_artifact_correction_ssp.py index 2d18367efe8..2f5af536a3d 100644 --- a/tutorials/preprocessing/50_artifact_correction_ssp.py +++ b/tutorials/preprocessing/50_artifact_correction_ssp.py @@ -15,6 +15,8 @@ functions from that submodule: """ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% import os diff --git a/tutorials/preprocessing/55_setting_eeg_reference.py b/tutorials/preprocessing/55_setting_eeg_reference.py index 36254647700..049e8f31a8b 100644 --- a/tutorials/preprocessing/55_setting_eeg_reference.py +++ b/tutorials/preprocessing/55_setting_eeg_reference.py @@ -13,6 +13,8 @@ just a few EEG channels so the plots are easier to see: """ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% import os diff --git a/tutorials/preprocessing/59_head_positions.py b/tutorials/preprocessing/59_head_positions.py index 28c54fe3875..cd1a454fd7b 100644 --- a/tutorials/preprocessing/59_head_positions.py +++ b/tutorials/preprocessing/59_head_positions.py @@ -26,6 +26,7 @@ # Daniel McCloy # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/tutorials/preprocessing/60_maxwell_filtering_sss.py b/tutorials/preprocessing/60_maxwell_filtering_sss.py index f07caa46257..e0062d3bbe8 100644 --- a/tutorials/preprocessing/60_maxwell_filtering_sss.py +++ b/tutorials/preprocessing/60_maxwell_filtering_sss.py @@ -12,6 +12,8 @@ :ref:`example data `, and cropping it to save on memory: """ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% import os diff --git a/tutorials/preprocessing/70_fnirs_processing.py b/tutorials/preprocessing/70_fnirs_processing.py index 2e0f4f9fa0f..8b59c6a31ff 100644 --- a/tutorials/preprocessing/70_fnirs_processing.py +++ b/tutorials/preprocessing/70_fnirs_processing.py @@ -12,6 +12,8 @@ Here we will work with the :ref:`fNIRS motor data `. """ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% from itertools import compress diff --git a/tutorials/preprocessing/80_opm_processing.py b/tutorials/preprocessing/80_opm_processing.py index a8d30c12abd..49a8159d748 100644 --- a/tutorials/preprocessing/80_opm_processing.py +++ b/tutorials/preprocessing/80_opm_processing.py @@ -21,6 +21,8 @@ :footcite:`SeymourEtAl2022` """ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% import matplotlib.pyplot as plt diff --git a/tutorials/preprocessing/90_eyetracking_data.py b/tutorials/preprocessing/90_eyetracking_data.py index a788988bae0..6c44fb29fdf 100644 --- a/tutorials/preprocessing/90_eyetracking_data.py +++ b/tutorials/preprocessing/90_eyetracking_data.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ .. _tut-eyetrack: @@ -16,6 +15,7 @@ # # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% # Data loading diff --git a/tutorials/raw/10_raw_overview.py b/tutorials/raw/10_raw_overview.py index b059cba86dd..31dfbf12325 100644 --- a/tutorials/raw/10_raw_overview.py +++ b/tutorials/raw/10_raw_overview.py @@ -16,6 +16,8 @@ As usual we'll start by importing the modules we need: """ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% import os diff --git a/tutorials/raw/20_event_arrays.py b/tutorials/raw/20_event_arrays.py index 4390634dfd8..e66db1003b7 100644 --- a/tutorials/raw/20_event_arrays.py +++ b/tutorials/raw/20_event_arrays.py @@ -13,6 +13,8 @@ object to just 60 seconds before loading it into RAM to save memory: """ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% import os diff --git a/tutorials/raw/30_annotate_raw.py b/tutorials/raw/30_annotate_raw.py index 90387f0a5e8..8a2a43d4188 100644 --- a/tutorials/raw/30_annotate_raw.py +++ b/tutorials/raw/30_annotate_raw.py @@ -14,6 +14,8 @@ seconds before loading it into RAM to save memory: """ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% import os diff --git a/tutorials/raw/40_visualize_raw.py b/tutorials/raw/40_visualize_raw.py index 28fbfa8fa9e..0056d90e413 100644 --- a/tutorials/raw/40_visualize_raw.py +++ b/tutorials/raw/40_visualize_raw.py @@ -13,6 +13,8 @@ :ref:`example data `, and cropping the `~mne.io.Raw` object to just 60 seconds before loading it into RAM to save memory: """ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% import os diff --git a/tutorials/simulation/10_array_objs.py b/tutorials/simulation/10_array_objs.py index a7fc8c88985..a2e94ab1c7a 100644 --- a/tutorials/simulation/10_array_objs.py +++ b/tutorials/simulation/10_array_objs.py @@ -11,6 +11,8 @@ We begin by importing the necessary Python modules: """ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% import numpy as np diff --git a/tutorials/simulation/70_point_spread.py b/tutorials/simulation/70_point_spread.py index 777c8996d3c..74913e98ec1 100644 --- a/tutorials/simulation/70_point_spread.py +++ b/tutorials/simulation/70_point_spread.py @@ -10,6 +10,8 @@ signal with point-spread by applying a forward and inverse solution. """ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% import numpy as np diff --git a/tutorials/simulation/80_dics.py b/tutorials/simulation/80_dics.py index 61e6fedcc44..98533a02a9c 100644 --- a/tutorials/simulation/80_dics.py +++ b/tutorials/simulation/80_dics.py @@ -16,6 +16,7 @@ # Author: Marijn van Vliet # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% # Setup diff --git a/tutorials/stats-sensor-space/10_background_stats.py b/tutorials/stats-sensor-space/10_background_stats.py index 1d0c88149a6..9d6912a20cb 100644 --- a/tutorials/stats-sensor-space/10_background_stats.py +++ b/tutorials/stats-sensor-space/10_background_stats.py @@ -11,6 +11,7 @@ # Authors: Eric Larson # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/tutorials/stats-sensor-space/20_erp_stats.py b/tutorials/stats-sensor-space/20_erp_stats.py index cba1d3bbf0f..2f0efa387db 100644 --- a/tutorials/stats-sensor-space/20_erp_stats.py +++ b/tutorials/stats-sensor-space/20_erp_stats.py @@ -15,6 +15,8 @@ short words. TFCE is described in :footcite:`SmithNichols2009`. """ +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% import matplotlib.pyplot as plt diff --git a/tutorials/stats-sensor-space/40_cluster_1samp_time_freq.py b/tutorials/stats-sensor-space/40_cluster_1samp_time_freq.py index e6ced1c8903..c32af4bcd97 100644 --- a/tutorials/stats-sensor-space/40_cluster_1samp_time_freq.py +++ b/tutorials/stats-sensor-space/40_cluster_1samp_time_freq.py @@ -29,6 +29,7 @@ # Stefan Appelhoff # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/tutorials/stats-sensor-space/50_cluster_between_time_freq.py b/tutorials/stats-sensor-space/50_cluster_between_time_freq.py index 790de36a42c..3ced6a82463 100644 --- a/tutorials/stats-sensor-space/50_cluster_between_time_freq.py +++ b/tutorials/stats-sensor-space/50_cluster_between_time_freq.py @@ -22,6 +22,7 @@ # Authors: Alexandre Gramfort # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/tutorials/stats-sensor-space/70_cluster_rmANOVA_time_freq.py b/tutorials/stats-sensor-space/70_cluster_rmANOVA_time_freq.py index f7b274d3ce4..202c660575a 100644 --- a/tutorials/stats-sensor-space/70_cluster_rmANOVA_time_freq.py +++ b/tutorials/stats-sensor-space/70_cluster_rmANOVA_time_freq.py @@ -26,6 +26,7 @@ # Alex Rockhill # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/tutorials/stats-sensor-space/75_cluster_ftest_spatiotemporal.py b/tutorials/stats-sensor-space/75_cluster_ftest_spatiotemporal.py index dda2ab29255..c7fdbdb1fc2 100644 --- a/tutorials/stats-sensor-space/75_cluster_ftest_spatiotemporal.py +++ b/tutorials/stats-sensor-space/75_cluster_ftest_spatiotemporal.py @@ -28,6 +28,7 @@ # Stefan Appelhoff # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/tutorials/stats-source-space/20_cluster_1samp_spatiotemporal.py b/tutorials/stats-source-space/20_cluster_1samp_spatiotemporal.py index d2210f56842..53e90f78d01 100644 --- a/tutorials/stats-source-space/20_cluster_1samp_spatiotemporal.py +++ b/tutorials/stats-source-space/20_cluster_1samp_spatiotemporal.py @@ -16,6 +16,7 @@ # Stefan Appelhoff # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/tutorials/stats-source-space/30_cluster_ftest_spatiotemporal.py b/tutorials/stats-source-space/30_cluster_ftest_spatiotemporal.py index 118011c32f6..59d7cd2430f 100644 --- a/tutorials/stats-source-space/30_cluster_ftest_spatiotemporal.py +++ b/tutorials/stats-source-space/30_cluster_ftest_spatiotemporal.py @@ -13,6 +13,7 @@ # Authors: Alexandre Gramfort # Eric Larson # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/tutorials/stats-source-space/60_cluster_rmANOVA_spatiotemporal.py b/tutorials/stats-source-space/60_cluster_rmANOVA_spatiotemporal.py index dd9d405dd13..0951280e6d6 100644 --- a/tutorials/stats-source-space/60_cluster_rmANOVA_spatiotemporal.py +++ b/tutorials/stats-source-space/60_cluster_rmANOVA_spatiotemporal.py @@ -20,6 +20,7 @@ # Denis Engemannn # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/tutorials/time-freq/10_spectrum_class.py b/tutorials/time-freq/10_spectrum_class.py index d646581517c..c5f8f4fd639 100644 --- a/tutorials/time-freq/10_spectrum_class.py +++ b/tutorials/time-freq/10_spectrum_class.py @@ -1,4 +1,6 @@ # noqa: E501 +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. """ .. _tut-spectrum-class: diff --git a/tutorials/time-freq/20_sensors_time_frequency.py b/tutorials/time-freq/20_sensors_time_frequency.py index 07a31e99db5..c4981b2b1e0 100644 --- a/tutorials/time-freq/20_sensors_time_frequency.py +++ b/tutorials/time-freq/20_sensors_time_frequency.py @@ -16,6 +16,7 @@ # Richard Höchenberger # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% import matplotlib.pyplot as plt diff --git a/tutorials/time-freq/50_ssvep.py b/tutorials/time-freq/50_ssvep.py index 611d6b19fe9..323e8a4fe54 100644 --- a/tutorials/time-freq/50_ssvep.py +++ b/tutorials/time-freq/50_ssvep.py @@ -39,6 +39,7 @@ # Evgenii Kalenkovich # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% diff --git a/tutorials/visualization/10_publication_figure.py b/tutorials/visualization/10_publication_figure.py index cc6d83b29f8..69edf301eb5 100644 --- a/tutorials/visualization/10_publication_figure.py +++ b/tutorials/visualization/10_publication_figure.py @@ -14,6 +14,7 @@ # Stefan Appelhoff # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. # %% # Imports diff --git a/tutorials/visualization/20_ui_events.py b/tutorials/visualization/20_ui_events.py index 014a8d2a057..ce268e1d8a5 100644 --- a/tutorials/visualization/20_ui_events.py +++ b/tutorials/visualization/20_ui_events.py @@ -19,6 +19,7 @@ # Author: Marijn van Vliet # # License: BSD-3-Clause +# Copyright the MNE-Python contributors. import matplotlib.pyplot as plt import mne From d8448e3940790eb3f03d3fbed0286ae75956f0c3 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Fri, 17 Nov 2023 17:29:25 -0500 Subject: [PATCH 074/405] DOC: Update docs (#12223) --- .circleci/config.yml | 8 +++++++- mne/_fiff/meas_info.py | 2 +- mne/commands/mne_browse_raw.py | 3 ++- mne/preprocessing/artifact_detection.py | 4 ++-- mne/preprocessing/maxwell.py | 2 +- mne/utils/config.py | 2 +- 6 files changed, 14 insertions(+), 7 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index bd94268c1ad..6618188621b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -296,8 +296,14 @@ jobs: # Build docs - run: name: make html + command: | # we have -o pipefail in #BASH_ENV so we should be okay + set -x + PATTERN=$(cat pattern.txt) make -C doc $(cat build.txt) 2>&1 | tee sphinx_log.txt + - run: + name: Check sphinx log for warnings (which are treated as errors) + when: always command: | - PATTERN=$(cat pattern.txt) make -C doc $(cat build.txt); + ! grep "^.* WARNING: .*$" sphinx_log.txt - run: name: Show profiling output when: always diff --git a/mne/_fiff/meas_info.py b/mne/_fiff/meas_info.py index 8630053e78e..483ddc34b52 100644 --- a/mne/_fiff/meas_info.py +++ b/mne/_fiff/meas_info.py @@ -1380,7 +1380,7 @@ class Info(dict, SetChannelsMixin, MontageMixin, ContainsMixin): The distance limit. accept : int Whether or not the fit was accepted. - coord_trans : instance of Transformation + coord_trans : instance of Transform The resulting MEG<->head transformation. * ``hpi_subsystem`` dict: diff --git a/mne/commands/mne_browse_raw.py b/mne/commands/mne_browse_raw.py index 17a8f8bcb88..0c3d81a16e9 100644 --- a/mne/commands/mne_browse_raw.py +++ b/mne/commands/mne_browse_raw.py @@ -125,7 +125,8 @@ def run(): parser.add_option( "--clipping", dest="clipping", - help="Enable trace clipping mode, either 'clamp' or " "'transparent'", + help="Enable trace clipping mode. Can be 'clamp', 'transparent', a float, " + "or 'none'.", default=_RAW_CLIP_DEF, ) parser.add_option( diff --git a/mne/preprocessing/artifact_detection.py b/mne/preprocessing/artifact_detection.py index eadd54a260e..d5bcfccb730 100644 --- a/mne/preprocessing/artifact_detection.py +++ b/mne/preprocessing/artifact_detection.py @@ -304,8 +304,8 @@ def compute_average_dev_head_t(raw, pos): Returns ------- - dev_head_t : array of shape (4, 4) - New trans matrix using the averaged good head positions. + dev_head_t : instance of Transform + New ``dev_head_t`` transformation using the averaged good head positions. """ sfreq = raw.info["sfreq"] seg_good = np.ones(len(raw.times)) diff --git a/mne/preprocessing/maxwell.py b/mne/preprocessing/maxwell.py index 8ee465ddb8e..25430db6f9e 100644 --- a/mne/preprocessing/maxwell.py +++ b/mne/preprocessing/maxwell.py @@ -1973,7 +1973,7 @@ def _update_sss_info( The moments that were used. st_only : bool Whether tSSS only was performed. - recon_trans : instance of Transformation + recon_trans : instance of Transform The reconstruction trans. extended_proj : ndarray Extended external bases. diff --git a/mne/utils/config.py b/mne/utils/config.py index 2b7d10ab070..fe4bc7079a4 100644 --- a/mne/utils/config.py +++ b/mne/utils/config.py @@ -805,7 +805,7 @@ def _get_latest_version(timeout): try: with urlopen(url, timeout=timeout) as f: # nosec response = json.load(f) - except URLError as err: + except (URLError, TimeoutError) as err: # Triage error type if "SSL" in str(err): return "SSL error" From 9f339d149a401cc1383e07aea91ab635e1bcbd01 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Mon, 20 Nov 2023 13:23:44 -0500 Subject: [PATCH 075/405] MAINT: Updates for release --- .mailmap | 2 ++ doc/_static/style.css | 1 + doc/changes/devel.rst | 65 +++++++++++++++++++++++++++++-------------- pyproject.toml | 2 +- 4 files changed, 48 insertions(+), 22 deletions(-) diff --git a/.mailmap b/.mailmap index fcfc7edea01..e6d5377c402 100644 --- a/.mailmap +++ b/.mailmap @@ -240,6 +240,7 @@ Olaf Hauk olafhauk Omer Shubi Omer S Paul Pasler ppasler Paul Roujansky Paul ROUJANSKY +Paul Roujansky paulroujansky Pedro Silva pbnsilva Phillip Alday Phillip Alday Phillip Alday Phillip Alday @@ -254,6 +255,7 @@ Praveen Sripad prav Proloy Das pdas6 Ram Pari Ram Ramonapariciog Apariciogarcia ramonapariciog +Rasmus Aagaard roraa Reza Nasri Reza Reza Nasri RezaNasri Roan LaPlante aestrivex diff --git a/doc/_static/style.css b/doc/_static/style.css index b10216ddcb3..61eea678830 100644 --- a/doc/_static/style.css +++ b/doc/_static/style.css @@ -59,6 +59,7 @@ html[data-theme="dark"] { --pst-color-border: #333; --pst-color-background: #000; --pst-color-link: #66b0ff; + --pst-color-on-background: #1e1e1e; /* sphinx-gallery overrides */ --sg-download-a-background-color: var(--pst-color-primary); --sg-download-a-background-image: unset; diff --git a/doc/changes/devel.rst b/doc/changes/devel.rst index 9558f8fe0ea..ee58c7a527f 100644 --- a/doc/changes/devel.rst +++ b/doc/changes/devel.rst @@ -1,25 +1,7 @@ -.. NOTE: we use cross-references to highlight new functions and classes. - Please follow the examples below like :func:`mne.stats.f_mway_rm`, so the - whats_new page will have a link to the function/class documentation. +.. _changes_1_6_0: -.. NOTE: there are 3 separate sections for changes, based on type: - - "Enhancements" for new features - - "Bugs" for bug fixes - - "API changes" for backward-incompatible changes - -.. NOTE: changes from first-time contributors should be added to the TOP of - the relevant section (Enhancements / Bugs / API changes), and should look - like this (where xxxx is the pull request number): - - - description of enhancement/bugfix/API change (:gh:`xxxx` by - :newcontrib:`Firstname Lastname`) - - Also add a corresponding entry for yourself in doc/changes/names.inc - -.. _current: - -Version 1.6.dev0 (development) ------------------------------- +Version 1.6.0 (2022-11-20) +-------------------------- Enhancements ~~~~~~~~~~~~ @@ -107,3 +89,44 @@ API changes - Replace legacy ``inst.pick_channels`` and ``inst.pick_types`` with ``inst.pick`` (where ``inst`` is an instance of :class:`~mne.io.Raw`, :class:`~mne.Epochs`, or :class:`~mne.Evoked`) wherever possible (:gh:`11907` by `Clemens Brunner`_) - The ``reset_camera`` parameter has been removed in favor of ``distance="auto"`` in :func:`mne.viz.set_3d_view`, :meth:`mne.viz.Brain.show_view`, and related functions (:gh:`12000` by `Eric Larson`_) - Several unused parameters from :func:`mne.gui.coregistration` are now deprecated: tabbed, split, scrollable, head_inside, guess_mri_subject, scale, and ``advanced_rendering``. All arguments are also now keyword-only. (:gh:`12147` by `Eric Larson`_) + +Authors +~~~~~~~ +* Adam Li +* Alex Rockhill +* Alexandre Gramfort +* Ana Radanovic +* Andy Gilbert+ +* Clemens Brunner +* Daniel McCloy +* Denis A. Engemann +* Dimitri Papadopoulos Orfanos +* Dmitrii Altukhov +* Dominik Welke +* Eric Larson +* Erica Peterson +* Gonzalo Reina +* Hamza Abdelhedi +* Ivan Skelin+ +* Ivan Zubarev+ +* Jack Zhang +* Jacob Woessner +* Johann Benerradi +* John Veillette +* Judy D Zhu +* Kristijan Armeni+ +* Mainak Jas +* Maksym Balatsko+ +* Marijn van Vliet +* Martin Schulz +* Mathieu Scheltienne +* Nikolai Chapochnikov +* Pablo Mainar+ +* Paul Roujansky +* Qian Chu+ +* Rasmus Aagaard+ +* Richard Höchenberger +* Rob Luke +* Santeri Ruuskanen +* Scott Huberty +* Stefan Appelhoff diff --git a/pyproject.toml b/pyproject.toml index 8c172fcac70..3db07d5d3c5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -133,7 +133,7 @@ test_extra = [ doc = [ "sphinx>=6", "numpydoc", - "pydata_sphinx_theme==0.13.3", + "pydata_sphinx_theme==0.14.3", "sphinx-gallery", "sphinxcontrib-bibtex>=2.5", "memory_profiler", From b93021bb9cc1d243164b3f8defe2bde020484561 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Mon, 20 Nov 2023 13:24:04 -0500 Subject: [PATCH 076/405] MAINT: Move --- doc/changes/{devel.rst => v1.6.rst} | 0 doc/development/whats_new.rst | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename doc/changes/{devel.rst => v1.6.rst} (100%) diff --git a/doc/changes/devel.rst b/doc/changes/v1.6.rst similarity index 100% rename from doc/changes/devel.rst rename to doc/changes/v1.6.rst diff --git a/doc/development/whats_new.rst b/doc/development/whats_new.rst index f9cb6e1c4b0..0e8c96ebe4d 100644 --- a/doc/development/whats_new.rst +++ b/doc/development/whats_new.rst @@ -8,7 +8,7 @@ Changes for each version of MNE-Python are listed below. .. toctree:: :maxdepth: 1 - ../changes/devel.rst + ../changes/v1.6.rst ../changes/v1.5.rst ../changes/v1.4.rst ../changes/v1.3.rst From bb0cdf049570df15df22278300867ab2ad62ca2c Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Mon, 20 Nov 2023 13:27:19 -0500 Subject: [PATCH 077/405] MAINT: Security --- SECURITY.md | 8 ++++---- doc/_static/versions.json | 9 +++++++-- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/SECURITY.md b/SECURITY.md index c61f3bfae87..e627242d244 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -10,9 +10,9 @@ without a proper 6-month deprecation cycle. | Version | Supported | | ------- | ------------------------ | -| 1.6.x | :heavy_check_mark: (dev) | -| 1.5.x | :heavy_check_mark: | -| < 1.5 | :x: | +| 1.7.x | :heavy_check_mark: (dev) | +| 1.6.x | :heavy_check_mark: | +| < 1.6 | :x: | ## Reporting a Vulnerability @@ -21,7 +21,7 @@ recorded with a variety of devices/modalities (EEG, MEG, ECoG, fNIRS, etc). It is not expected that using MNE-Python will lead to security vulnerabilities under normal use cases (i.e., running without administrator privileges). However, if you think you have found a security vulnerability -in MNE-Python, **please do not report it as a GitHub issue**, in order to +in MNE-Python, **please do not report it as a GitHub issue**, in order to keep the vulnerability confidential. Instead, please report it to mne-core-dev-team@groups.io and include a description and proof-of-concept that is [short and self-contained](http://www.sscce.org/). diff --git a/doc/_static/versions.json b/doc/_static/versions.json index 9845f298fb6..8141440bd16 100644 --- a/doc/_static/versions.json +++ b/doc/_static/versions.json @@ -1,14 +1,19 @@ [ { - "name": "1.6 (devel)", + "name": "1.7 (devel)", "version": "dev", "url": "https://mne.tools/dev/" }, { - "name": "1.5 (stable)", + "name": "1.6 (stable)", "version": "stable", "url": "https://mne.tools/stable/" }, + { + "name": "1.5", + "version": "1.5", + "url": "https://mne.tools/1.5/" + }, { "name": "1.4", "version": "1.4", From c65c9c490499286344d731f4c8bded2b265218ec Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Mon, 20 Nov 2023 13:27:58 -0500 Subject: [PATCH 078/405] MAINT: Codemeta --- CITATION.cff | 92 +++++++++++------- codemeta.json | 263 ++++++++++++++++++++++++++++++++------------------ 2 files changed, 226 insertions(+), 129 deletions(-) diff --git a/CITATION.cff b/CITATION.cff index edda3668c4c..c1850a2f55b 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -1,9 +1,9 @@ cff-version: 1.2.0 title: "MNE-Python" message: "If you use this software, please cite both the software itself, and the paper listed in the preferred-citation field." -version: 1.5.0 -date-released: "2023-08-15" -commit: 5645d375f52727eb563bd46a45422fc1a55e9f1a +version: 1.6.0 +date-released: "2023-11-20" +commit: 498cf789685ede0b29e712a1e7220c69443e8744 doi: 10.5281/zenodo.592483 keywords: - MEG @@ -61,14 +61,14 @@ authors: given-names: Joan - family-names: Bekhti given-names: Yousra + - family-names: Scheltienne + given-names: Mathieu - family-names: Appelhoff given-names: Stefan - family-names: Leggitt given-names: Alan - family-names: Dykstra given-names: Andrew - - family-names: Scheltienne - given-names: Mathieu - family-names: Luke given-names: Rob - family-names: Trachel @@ -121,10 +121,14 @@ authors: given-names: Cathy - family-names: García Alanis given-names: José C + - family-names: Huberty + given-names: Scott - family-names: Hauk given-names: Olaf - family-names: Maddox given-names: Ross + - family-names: Orfanos + given-names: Dimitri Papadopoulos - family-names: LaPlante given-names: Roan - family-names: Drew @@ -133,16 +137,12 @@ authors: given-names: Christoph - family-names: Dumas given-names: Guillaume - - family-names: Huberty - given-names: Scott + - family-names: Benerradi + given-names: Johann - family-names: Hartmann given-names: Thomas - - family-names: Orfanos - given-names: Dimitri Papadopoulos - family-names: Ort given-names: Eduard - - family-names: Benerradi - given-names: Johann - family-names: Pasler given-names: Paul - family-names: Repplinger @@ -185,6 +185,8 @@ authors: given-names: Tanay - family-names: Nunes given-names: Adonay + - family-names: Gramfort + given-names: Alexandre - family-names: Gütlin given-names: Dirk - name: kjs @@ -196,6 +198,10 @@ authors: given-names: Catalina María - family-names: Moënne-Loccoz given-names: Cristóbal + - family-names: Altukhov + given-names: Dmitrii + - family-names: Peterson + given-names: Erica - family-names: Heinila given-names: Erkka - family-names: Hanna @@ -206,6 +212,8 @@ authors: given-names: Michiru - family-names: Klein given-names: Natalie + - family-names: Roujansky + given-names: Paul - family-names: Kern given-names: Simon - family-names: Rantala @@ -214,24 +222,26 @@ authors: given-names: Burkhard - family-names: O'Reilly given-names: Christian - - family-names: Peterson - given-names: Erica - family-names: Kolkhorst given-names: Henrich - family-names: Banville given-names: Hubert + - family-names: Zhang + given-names: Jack + - family-names: Woessner + given-names: Jacob - family-names: Maksymenko given-names: Kostiantyn - family-names: Clarke given-names: Maggie - family-names: Anelli given-names: Matteo + - family-names: Chapochnikov + given-names: Nikolai - family-names: Bannier given-names: Pierre-Antoine - family-names: Choudhary given-names: Saket - - family-names: Gramfort - given-names: Alexandre - family-names: Forster given-names: Carina - family-names: Kim @@ -242,8 +252,6 @@ authors: given-names: Fu-Te - family-names: Kojcic given-names: Ivana - - family-names: Zhang - given-names: Jack - family-names: Nielsen given-names: Jesper Duemose - family-names: Lankinen @@ -258,6 +266,10 @@ authors: given-names: Nathalie - family-names: Ward given-names: Nick + - family-names: Ruuskanen + given-names: Santeri + - family-names: Radanovic + given-names: Ana - family-names: Quinn given-names: Andrew - family-names: Gauthier @@ -266,6 +278,8 @@ authors: given-names: Basile - family-names: Welke given-names: Dominik + - family-names: Welke + given-names: Dominik - family-names: Stephen given-names: Emily - family-names: Hornberger @@ -326,12 +340,8 @@ authors: given-names: Chetan - family-names: Zhao given-names: Christina - - family-names: Altukhov - given-names: Dmitrii - family-names: Krzemiński given-names: Dominik - - family-names: Welke - given-names: Dominik - family-names: Makowski given-names: Dominique - family-names: Mikulan @@ -340,16 +350,22 @@ authors: given-names: Gennadiy - family-names: O'Neill given-names: George - - family-names: Woessner - given-names: Jacob + - family-names: Abdelhedi + given-names: Hamza - family-names: Schiratti given-names: Jean-Baptiste - family-names: Evans given-names: Jen + - family-names: Veillette + given-names: John - family-names: Drew given-names: Jordan - family-names: Teves given-names: Joshua + - family-names: Zhu + given-names: Judy D + - family-names: Armeni + given-names: Kristijan - family-names: Mathewson given-names: Kyle - family-names: Gwilliams @@ -375,8 +391,6 @@ authors: given-names: Naveen - family-names: Wilming given-names: Niklas - - family-names: Chapochnikov - given-names: Nikolai - family-names: Kozynets given-names: Oleh - family-names: Ablin @@ -427,6 +441,8 @@ authors: given-names: Adina - family-names: Ciok given-names: Alex + - family-names: Gilbert + given-names: Andy - family-names: Pradhan given-names: Aniket - family-names: Padee @@ -510,10 +526,10 @@ authors: - family-names: O'Neill given-names: George - name: Giulio + - family-names: Reina + given-names: Gonzalo - family-names: Maymandi given-names: Hamid - - family-names: Abdelhedi - given-names: Hamza - family-names: Sonntag given-names: Hermann - family-names: Ye @@ -524,6 +540,10 @@ authors: given-names: Hüseyin Orkun - family-names: Machairas given-names: Ilias + - family-names: Skelin + given-names: Ivan + - family-names: Zubarev + given-names: Ivan - family-names: Kaczmarzyk given-names: Jakub - family-names: Zerfowski @@ -536,14 +556,10 @@ authors: given-names: Johan - family-names: Niediek given-names: Johannes - - family-names: Veillette - given-names: John - family-names: Koen given-names: Josh - family-names: Bear given-names: Joshua J - - family-names: Zhu - given-names: Judy D - family-names: Dammers given-names: Juergen - family-names: Galán @@ -566,6 +582,8 @@ authors: given-names: Lorenzo - family-names: Hejtmánek given-names: Lukáš + - family-names: Balatsko + given-names: Maksym - family-names: Kitzbichler given-names: Manfred - family-names: Kumar @@ -578,6 +596,8 @@ authors: given-names: Marcin - family-names: Henney given-names: Mark Alexander + - family-names: Schulz + given-names: Martin - family-names: van Harmelen given-names: Martin - name: MartinBaBer @@ -613,16 +633,18 @@ authors: given-names: Nikolas - family-names: Shubi given-names: Omer + - family-names: Mainar + given-names: Pablo - family-names: Sundaram given-names: Padma - - family-names: Roujansky - given-names: Paul - family-names: Silva given-names: Pedro - family-names: Molfese given-names: Peter J - family-names: Das given-names: Proloy + - family-names: Chu + given-names: Qian - family-names: Li given-names: Quanliang - family-names: Barthélemy @@ -633,6 +655,8 @@ authors: given-names: Ramiro - family-names: Apariciogarcia given-names: Ramonapariciog + - family-names: Aagaard + given-names: Rasmus - family-names: Nasri given-names: Reza - family-names: Koehler @@ -653,8 +677,6 @@ authors: given-names: Sam - family-names: Louviot given-names: Samuel - - family-names: Ruuskanen - given-names: Santeri - family-names: Saha given-names: Sawradip - family-names: Mathot diff --git a/codemeta.json b/codemeta.json index 7e8b2cca70a..b2922b2194d 100644 --- a/codemeta.json +++ b/codemeta.json @@ -5,11 +5,11 @@ "codeRepository": "git+https://github.com/mne-tools/mne-python.git", "dateCreated": "2010-12-26", "datePublished": "2014-08-04", - "dateModified": "2023-08-15", - "downloadUrl": "https://github.com/mne-tools/mne-python/archive/v1.5.0.zip", + "dateModified": "2023-11-20", + "downloadUrl": "https://github.com/mne-tools/mne-python/archive/v1.6.0.zip", "issueTracker": "https://github.com/mne-tools/mne-python/issues", "name": "MNE-Python", - "version": "1.5.0", + "version": "1.6.0", "description": "MNE-Python is an open-source Python package for exploring, visualizing, and analyzing human neurophysiological data. It provides methods for data input/output, preprocessing, visualization, source estimation, time-frequency analysis, connectivity analysis, machine learning, and statistics.", "applicationCategory": "Neuroscience", "developmentStatus": "active", @@ -38,8 +38,17 @@ ], "softwareRequirements": [ "python>=3.8", - "numpy>=1.15.4", - "scipy>=1.6.3" + "numpy>=1.21.2", + "scipy>=1.7.1", + "matplotlib>=3.5.0", + "tqdm", + "pooch>=1.5", + "decorator", + "packaging", + "jinja2", + "importlib_resources>=5.10.2; python_version<'3.9'", + "lazy_loader>=0.3", + "defusedxml" ], "author": [ { @@ -168,6 +177,12 @@ "givenName":"Yousra", "familyName": "Bekhti" }, + { + "@type":"Person", + "email":"mathieu.scheltienne@gmail.com", + "givenName":"Mathieu", + "familyName": "Scheltienne" + }, { "@type":"Person", "email":"stefan.appelhoff@mailbox.org", @@ -186,12 +201,6 @@ "givenName":"Andrew", "familyName": "Dykstra" }, - { - "@type":"Person", - "email":"mathieu.scheltienne@gmail.com", - "givenName":"Mathieu", - "familyName": "Scheltienne" - }, { "@type":"Person", "email":"code@robertluke.net", @@ -348,6 +357,12 @@ "givenName":"José C", "familyName": "García Alanis" }, + { + "@type":"Person", + "email":"", + "givenName":"Scott", + "familyName": "Huberty" + }, { "@type":"Person", "email":"olaf.hauk@mrc-cbu.cam.ac.uk", @@ -360,6 +375,12 @@ "givenName":"Ross", "familyName": "Maddox" }, + { + "@type":"Person", + "email":"", + "givenName":"Dimitri Papadopoulos", + "familyName": "Orfanos" + }, { "@type":"Person", "email":"aestrivex@gmail.com", @@ -386,9 +407,9 @@ }, { "@type":"Person", - "email":"", - "givenName":"Scott", - "familyName": "Huberty" + "email":"johann.benerradi@gmail.com", + "givenName":"Johann", + "familyName": "Benerradi" }, { "@type":"Person", @@ -396,24 +417,12 @@ "givenName":"Thomas", "familyName": "Hartmann" }, - { - "@type":"Person", - "email":"", - "givenName":"Dimitri Papadopoulos", - "familyName": "Orfanos" - }, { "@type":"Person", "email":"eduardxort@gmail.com", "givenName":"Eduard", "familyName": "Ort" }, - { - "@type":"Person", - "email":"johann.benerradi@gmail.com", - "givenName":"Johann", - "familyName": "Benerradi" - }, { "@type":"Person", "email":"paul@ppasler.de", @@ -540,6 +549,12 @@ "givenName":"Adonay", "familyName": "Nunes" }, + { + "@type":"Person", + "email":"agramfort@fb.com", + "givenName":"Alexandre", + "familyName": "Gramfort" + }, { "@type":"Person", "email":"", @@ -576,6 +591,18 @@ "givenName":"Cristóbal", "familyName": "Moënne-Loccoz" }, + { + "@type":"Person", + "email":"dm.altukhov@ya.ru", + "givenName":"Dmitrii", + "familyName": "Altukhov" + }, + { + "@type":"Person", + "email":"nordme@uw.edu", + "givenName":"Erica", + "familyName": "Peterson" + }, { "@type":"Person", "email":"erkkahe@gmail.com", @@ -606,6 +633,12 @@ "givenName":"Natalie", "familyName": "Klein" }, + { + "@type":"Person", + "email":"paul@roujansky.eu", + "givenName":"Paul", + "familyName": "Roujansky" + }, { "@type":"Person", "email":"simon.kern@online.de", @@ -630,12 +663,6 @@ "givenName":"Christian", "familyName": "O'Reilly" }, - { - "@type":"Person", - "email":"nordme@uw.edu", - "givenName":"Erica", - "familyName": "Peterson" - }, { "@type":"Person", "email":"", @@ -648,6 +675,18 @@ "givenName":"Hubert", "familyName": "Banville" }, + { + "@type":"Person", + "email":"zhangmengyu10@gmail.com", + "givenName":"Jack", + "familyName": "Zhang" + }, + { + "@type":"Person", + "email":"Woessner.jacob@gmail.com", + "givenName":"Jacob", + "familyName": "Woessner" + }, { "@type":"Person", "email":"makkostya@ukr.net", @@ -666,6 +705,12 @@ "givenName":"Matteo", "familyName": "Anelli" }, + { + "@type":"Person", + "email":"", + "givenName":"Nikolai", + "familyName": "Chapochnikov" + }, { "@type":"Person", "email":"pierreantoine.bannier@gmail.com", @@ -678,12 +723,6 @@ "givenName":"Saket", "familyName": "Choudhary" }, - { - "@type":"Person", - "email":"agramfort@fb.com", - "givenName":"Alexandre", - "familyName": "Gramfort" - }, { "@type":"Person", "email":"carinaforster0611@gmail.com", @@ -714,12 +753,6 @@ "givenName":"Ivana", "familyName": "Kojcic" }, - { - "@type":"Person", - "email":"zhangmengyu10@gmail.com", - "givenName":"Jack", - "familyName": "Zhang" - }, { "@type":"Person", "email":"jdue@dtu.dk", @@ -762,6 +795,18 @@ "givenName":"Nick", "familyName": "Ward" }, + { + "@type":"Person", + "email":"", + "givenName":"Santeri", + "familyName": "Ruuskanen" + }, + { + "@type":"Person", + "email":"", + "givenName":"Ana", + "familyName": "Radanovic" + }, { "@type":"Person", "email":"", @@ -780,6 +825,12 @@ "givenName":"Basile", "familyName": "Pinsard" }, + { + "@type":"Person", + "email":"dominik.welke@ae.mpg.de", + "givenName":"Dominik", + "familyName": "Welke" + }, { "@type":"Person", "email":"dominik.welke@web.de", @@ -966,24 +1017,12 @@ "givenName":"Christina", "familyName": "Zhao" }, - { - "@type":"Person", - "email":"dm.altukhov@ya.ru", - "givenName":"Dmitrii", - "familyName": "Altukhov" - }, { "@type":"Person", "email":"raymon92@gmail.com", "givenName":"Dominik", "familyName": "Krzemiński" }, - { - "@type":"Person", - "email":"dominik.welke@ae.mpg.de", - "givenName":"Dominik", - "familyName": "Welke" - }, { "@type":"Person", "email":"dom.mak19@gmail.com", @@ -1010,9 +1049,9 @@ }, { "@type":"Person", - "email":"Woessner.jacob@gmail.com", - "givenName":"Jacob", - "familyName": "Woessner" + "email":"hamza.abdelhedii@gmail.com", + "givenName":"Hamza", + "familyName": "Abdelhedi" }, { "@type":"Person", @@ -1026,6 +1065,12 @@ "givenName":"Jen", "familyName": "Evans" }, + { + "@type":"Person", + "email":"johnv@uchicago.edu", + "givenName":"John", + "familyName": "Veillette" + }, { "@type":"Person", "email":"", @@ -1038,6 +1083,18 @@ "givenName":"Joshua", "familyName": "Teves" }, + { + "@type":"Person", + "email":"", + "givenName":"Judy D", + "familyName": "Zhu" + }, + { + "@type":"Person", + "email":"kristijan.armeni@gmail.com", + "givenName":"Kristijan", + "familyName": "Armeni" + }, { "@type":"Person", "email":"kylemath@gmail.com", @@ -1116,12 +1173,6 @@ "givenName":"Niklas", "familyName": "Wilming" }, - { - "@type":"Person", - "email":"", - "givenName":"Nikolai", - "familyName": "Chapochnikov" - }, { "@type":"Person", "email":"", @@ -1278,6 +1329,12 @@ "givenName":"Alex", "familyName": "Ciok" }, + { + "@type":"Person", + "email":"7andy121@gmail.com", + "givenName":"Andy", + "familyName": "Gilbert" + }, { "@type":"Person", "email":"aniket17133@iiitd.ac.in", @@ -1533,14 +1590,14 @@ { "@type":"Person", "email":"", - "givenName":"Hamid", - "familyName": "Maymandi" + "givenName":"Gonzalo", + "familyName": "Reina" }, { "@type":"Person", - "email":"hamza.abdelhedii@gmail.com", - "givenName":"Hamza", - "familyName": "Abdelhedi" + "email":"", + "givenName":"Hamid", + "familyName": "Maymandi" }, { "@type":"Person", @@ -1572,6 +1629,18 @@ "givenName":"Ilias", "familyName": "Machairas" }, + { + "@type":"Person", + "email":"", + "givenName":"Ivan", + "familyName": "Skelin" + }, + { + "@type":"Person", + "email":"ivan.zubarev@aalto.fi", + "givenName":"Ivan", + "familyName": "Zubarev" + }, { "@type":"Person", "email":"", @@ -1608,12 +1677,6 @@ "givenName":"Johannes", "familyName": "Niediek" }, - { - "@type":"Person", - "email":"johnv@uchicago.edu", - "givenName":"John", - "familyName": "Veillette" - }, { "@type":"Person", "email":"koen.joshua@gmail.com", @@ -1626,12 +1689,6 @@ "givenName":"Joshua J", "familyName": "Bear" }, - { - "@type":"Person", - "email":"", - "givenName":"Judy D", - "familyName": "Zhu" - }, { "@type":"Person", "email":"j.dammers@fz-juelich.de", @@ -1698,6 +1755,12 @@ "givenName":"Lukáš", "familyName": "Hejtmánek" }, + { + "@type":"Person", + "email":"mbalatsko@gmail.com", + "givenName":"Maksym", + "familyName": "Balatsko" + }, { "@type":"Person", "email":"manfredg@nmr.mgh.harvard.edu", @@ -1734,6 +1797,12 @@ "givenName":"Mark Alexander", "familyName": "Henney" }, + { + "@type":"Person", + "email":"dev@mgschulz.de", + "givenName":"Martin", + "familyName": "Schulz" + }, { "@type":"Person", "email":"", @@ -1844,15 +1913,15 @@ }, { "@type":"Person", - "email":"tottochan@gmail.com", - "givenName":"Padma", - "familyName": "Sundaram" + "email":"pablomainar.pm@gmail.com", + "givenName":"Pablo", + "familyName": "Mainar" }, { "@type":"Person", - "email":"paul@roujansky.eu", - "givenName":"Paul", - "familyName": "Roujansky" + "email":"tottochan@gmail.com", + "givenName":"Padma", + "familyName": "Sundaram" }, { "@type":"Person", @@ -1872,6 +1941,12 @@ "givenName":"Proloy", "familyName": "Das" }, + { + "@type":"Person", + "email":"", + "givenName":"Qian", + "familyName": "Chu" + }, { "@type":"Person", "email":"glia@dtu.dk", @@ -1902,6 +1977,12 @@ "givenName":"Ramonapariciog", "familyName": "Apariciogarcia" }, + { + "@type":"Person", + "email":"raagaard97@gmail.com", + "givenName":"Rasmus", + "familyName": "Aagaard" + }, { "@type":"Person", "email":"reza@ddpo.ir", @@ -1962,12 +2043,6 @@ "givenName":"Samuel", "familyName": "Louviot" }, - { - "@type":"Person", - "email":"", - "givenName":"Santeri", - "familyName": "Ruuskanen" - }, { "@type":"Person", "email":"", From e4838892bf8dcd220e623be66edfcef683f0e638 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Mon, 20 Nov 2023 14:15:05 -0500 Subject: [PATCH 079/405] FIX: NumPy 2.0 breaks everything --- pyproject.toml | 2 +- tools/azure_dependencies.sh | 8 +++++--- tools/github_actions_dependencies.sh | 8 +++++--- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 3db07d5d3c5..8c172fcac70 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -133,7 +133,7 @@ test_extra = [ doc = [ "sphinx>=6", "numpydoc", - "pydata_sphinx_theme==0.14.3", + "pydata_sphinx_theme==0.13.3", "sphinx-gallery", "sphinxcontrib-bibtex>=2.5", "memory_profiler", diff --git a/tools/azure_dependencies.sh b/tools/azure_dependencies.sh index 371a61b60e3..d27c10d8224 100755 --- a/tools/azure_dependencies.sh +++ b/tools/azure_dependencies.sh @@ -8,15 +8,17 @@ elif [ "${TEST_MODE}" == "pip-pre" ]; then STD_ARGS="$STD_ARGS --pre" python -m pip install $STD_ARGS --only-binary ":all:" --extra-index-url "https://www.riverbankcomputing.com/pypi/simple" PyQt6 PyQt6-sip PyQt6-Qt6 echo "Numpy etc." - # As of 2023/10/25 no pandas (or statsmodels) because they pin to NumPy < 2 - python -m pip install $STD_ARGS --only-binary ":all:" --extra-index-url "https://pypi.anaconda.org/scientific-python-nightly-wheels/simple" "numpy>=2.0.0.dev0" "scipy>=1.12.0.dev0" scikit-learn matplotlib + # See github_actions_dependencies.sh for comments + python -m pip install $STD_ARGS --only-binary "numpy" numpy + python -m pip install $STD_ARGS --only-binary ":all:" --extra-index-url "https://pypi.anaconda.org/scientific-python-nightly-wheels/simple" "scipy>=1.12.0.dev0" scikit-learn matplotlib pandas statsmodels echo "dipy" python -m pip install $STD_ARGS --only-binary ":all:" --extra-index-url "https://pypi.anaconda.org/scipy-wheels-nightly/simple" dipy echo "h5py" python -m pip install $STD_ARGS --only-binary ":all:" -f "https://7933911d6844c6c53a7d-47bd50c35cd79bd838daf386af554a83.ssl.cf2.rackcdn.com" h5py echo "vtk" python -m pip install $STD_ARGS --only-binary ":all:" --extra-index-url "https://wheels.vtk.org" vtk - echo "openmeeg" + echo "nilearn and openmeeg" + python -m pip install $STD_ARGS git+https://github.com/nilearn/nilearn python -m pip install $STD_ARGS --only-binary ":all:" --extra-index-url "https://test.pypi.org/simple" openmeeg echo "pyvista/pyvistaqt" python -m pip install --progress-bar off git+https://github.com/pyvista/pyvista diff --git a/tools/github_actions_dependencies.sh b/tools/github_actions_dependencies.sh index 768165635a4..2a4b90bb910 100755 --- a/tools/github_actions_dependencies.sh +++ b/tools/github_actions_dependencies.sh @@ -24,8 +24,10 @@ else echo "PyQt6" pip install $STD_ARGS --only-binary ":all:" --default-timeout=60 --extra-index-url https://www.riverbankcomputing.com/pypi/simple PyQt6 echo "NumPy/SciPy/pandas etc." - # As of 2023/10/25 no pandas (or statsmodels, nilearn) because they pin to NumPy < 2 - pip install $STD_ARGS --only-binary ":all:" --default-timeout=60 --extra-index-url "https://pypi.anaconda.org/scientific-python-nightly-wheels/simple" "numpy>=2.0.0.dev0" scipy scikit-learn matplotlib pillow + # As of 2023/11/20 no NumPy 2.0 because it requires everything using its ABI to + # compile against 2.0, and h5py isn't (and probably not VTK either) + pip install $STD_ARGS --only-binary "numpy" --default-timeout=60 numpy + pip install $STD_ARGS --only-binary ":all:" --default-timeout=60 --extra-index-url "https://pypi.anaconda.org/scientific-python-nightly-wheels/simple" scipy scikit-learn matplotlib pillow pandas statsmodels echo "dipy" pip install $STD_ARGS --only-binary ":all:" --default-timeout=60 --extra-index-url "https://pypi.anaconda.org/scipy-wheels-nightly/simple" dipy echo "H5py" @@ -34,7 +36,7 @@ else pip install $STD_ARGS --only-binary ":all:" --extra-index-url "https://test.pypi.org/simple" openmeeg # No Numba because it forces an old NumPy version echo "nilearn and openmeeg" - # pip install $STD_ARGS git+https://github.com/nilearn/nilearn + pip install $STD_ARGS git+https://github.com/nilearn/nilearn pip install $STD_ARGS openmeeg echo "VTK" pip install $STD_ARGS --only-binary ":all:" --extra-index-url "https://wheels.vtk.org" vtk From d7612a115d7a3ff86b4bb55a19dfd4a62dab5b56 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Mon, 20 Nov 2023 16:52:39 -0500 Subject: [PATCH 080/405] MAINT: Update changelog post-release (#12226) --- doc/changes/devel.rst | 34 ++++++++++++++++++++++++++++++++++ doc/development/whats_new.rst | 1 + doc/install/installers.rst | 12 ++++++------ 3 files changed, 41 insertions(+), 6 deletions(-) create mode 100644 doc/changes/devel.rst diff --git a/doc/changes/devel.rst b/doc/changes/devel.rst new file mode 100644 index 00000000000..b30353407d4 --- /dev/null +++ b/doc/changes/devel.rst @@ -0,0 +1,34 @@ +.. NOTE: we use cross-references to highlight new functions and classes. + Please follow the examples below like :func:`mne.stats.f_mway_rm`, so the + whats_new page will have a link to the function/class documentation. + +.. NOTE: there are 3 separate sections for changes, based on type: + - "Enhancements" for new features + - "Bugs" for bug fixes + - "API changes" for backward-incompatible changes + +.. NOTE: changes from first-time contributors should be added to the TOP of + the relevant section (Enhancements / Bugs / API changes), and should look + like this (where xxxx is the pull request number): + + - description of enhancement/bugfix/API change (:gh:`xxxx` by + :newcontrib:`Firstname Lastname`) + + Also add a corresponding entry for yourself in doc/changes/names.inc + +.. _current: + +Version 1.7.dev0 (development) +------------------------------ + +Enhancements +~~~~~~~~~~~~ +- None yet + +Bugs +~~~~ +- None yet + +API changes +~~~~~~~~~~~ +- None yet diff --git a/doc/development/whats_new.rst b/doc/development/whats_new.rst index 0e8c96ebe4d..61c14a876f9 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.6.rst ../changes/v1.5.rst ../changes/v1.4.rst diff --git a/doc/install/installers.rst b/doc/install/installers.rst index 2d1d75323b8..39583ac9135 100644 --- a/doc/install/installers.rst +++ b/doc/install/installers.rst @@ -15,7 +15,7 @@ Got any questions? Let us know on the `MNE Forum`_! :class-content: text-center :name: linux-installers - .. button-link:: https://github.com/mne-tools/mne-installers/releases/download/v1.5.1/MNE-Python-1.5.1_0-Linux.sh + .. button-link:: https://github.com/mne-tools/mne-installers/releases/download/v1.6.0/MNE-Python-1.6.0_0-Linux.sh :ref-type: ref :color: primary :shadow: @@ -29,14 +29,14 @@ Got any questions? Let us know on the `MNE Forum`_! .. code-block:: console - $ sh ./MNE-Python-1.5.1_0-Linux.sh + $ sh ./MNE-Python-1.6.0_0-Linux.sh .. tab-item:: macOS (Intel) :class-content: text-center :name: macos-intel-installers - .. button-link:: https://github.com/mne-tools/mne-installers/releases/download/v1.5.1/MNE-Python-1.5.1_0-macOS_Intel.pkg + .. button-link:: https://github.com/mne-tools/mne-installers/releases/download/v1.6.0/MNE-Python-1.6.0_0-macOS_Intel.pkg :ref-type: ref :color: primary :shadow: @@ -52,7 +52,7 @@ Got any questions? Let us know on the `MNE Forum`_! :class-content: text-center :name: macos-apple-installers - .. button-link:: https://github.com/mne-tools/mne-installers/releases/download/v1.5.1/MNE-Python-1.5.1_0-macOS_M1.pkg + .. button-link:: https://github.com/mne-tools/mne-installers/releases/download/v1.6.0/MNE-Python-1.6.0_0-macOS_M1.pkg :ref-type: ref :color: primary :shadow: @@ -68,7 +68,7 @@ Got any questions? Let us know on the `MNE Forum`_! :class-content: text-center :name: windows-installers - .. button-link:: https://github.com/mne-tools/mne-installers/releases/download/v1.5.1/MNE-Python-1.5.1_0-Windows.exe + .. button-link:: https://github.com/mne-tools/mne-installers/releases/download/v1.6.0/MNE-Python-1.6.0_0-Windows.exe :ref-type: ref :color: primary :shadow: @@ -120,7 +120,7 @@ information, including a line that will read something like: .. code-block:: - Using Python: /some/directory/mne-python_1.5.1_0/bin/python + Using Python: /some/directory/mne-python_1.6.0_0/bin/python This path is what you need to enter in VS Code when selecting the Python interpreter. From d030961250650d0636de71d81b00fa69f90d30d8 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 21 Nov 2023 09:37:13 +0100 Subject: [PATCH 081/405] [pre-commit.ci] pre-commit autoupdate (#12225) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 436fbbb80a7..0eb61e67b73 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -7,7 +7,7 @@ repos: # Ruff mne - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.5 + rev: v0.1.6 hooks: - id: ruff name: ruff mne @@ -16,7 +16,7 @@ repos: # Ruff tutorials and examples - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.5 + rev: v0.1.6 hooks: - id: ruff name: ruff tutorials and examples From 1c86d86206610567091ba3fd2dcb63f919c768dc Mon Sep 17 00:00:00 2001 From: Tristan Stenner Date: Tue, 21 Nov 2023 15:13:24 +0100 Subject: [PATCH 082/405] Fix 1.6 release date (#12227) --- doc/changes/v1.6.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/changes/v1.6.rst b/doc/changes/v1.6.rst index ee58c7a527f..b5bcd3a5f11 100644 --- a/doc/changes/v1.6.rst +++ b/doc/changes/v1.6.rst @@ -1,6 +1,6 @@ .. _changes_1_6_0: -Version 1.6.0 (2022-11-20) +Version 1.6.0 (2023-11-20) -------------------------- Enhancements From a7d479568b3f427f65d87452e5400235e6a73dc9 Mon Sep 17 00:00:00 2001 From: Stefan Appelhoff Date: Tue, 21 Nov 2023 22:13:53 +0100 Subject: [PATCH 083/405] FIX: download link in pyproject.toml (#12231) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 8c172fcac70..826c43d2c9c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -157,7 +157,7 @@ dev = ["mne[test,doc]"] [project.urls] Homepage = "https://mne.tools/" -Download = "https://pypi.org/project/scikit-learn/#files" +Download = "https://pypi.org/project/mne/#files" "Bug Tracker" = "https://github.com/mne-tools/mne-python/issues/" Documentation = "https://mne.tools/" Forum = "https://mne.discourse.group/" From 44c787fd4cffa3453ffbc7b6735a5d09f47eed44 Mon Sep 17 00:00:00 2001 From: Clemens Brunner Date: Tue, 21 Nov 2023 23:02:19 +0100 Subject: [PATCH 084/405] Remove Python 3.8 (#12229) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Eric Larson --- .github/workflows/tests.yml | 6 +++--- .pre-commit-config.yaml | 3 +++ README.rst | 4 ++-- doc/development/contributing.rst | 2 +- environment.yml | 2 +- mne/utils/config.py | 11 ----------- pyproject.toml | 2 +- 7 files changed, 11 insertions(+), 19 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 09555ac5eb9..d09ed2529d1 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -63,16 +63,16 @@ jobs: python: '3.11' kind: pip-pre - os: macos-latest - python: '3.8' + python: '3.9' kind: mamba - os: windows-latest python: '3.10' kind: mamba - os: ubuntu-latest - python: '3.8' + python: '3.9' kind: minimal - os: ubuntu-20.04 - python: '3.8' + python: '3.9' kind: old steps: - uses: actions/checkout@v4 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0eb61e67b73..25d15b2157d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -50,3 +50,6 @@ repos: additional_dependencies: - tomli files: ^doc/.*\.(rst|inc)$ + +ci: + autofix_prs: false diff --git a/README.rst b/README.rst index e8690281bcb..ca4e08becba 100644 --- a/README.rst +++ b/README.rst @@ -43,7 +43,7 @@ only, use pip_ in a terminal: $ pip install --upgrade mne -The current MNE-Python release requires Python 3.8 or higher. MNE-Python 0.17 +The current MNE-Python release requires Python 3.9 or higher. MNE-Python 0.17 was the last release to support Python 2.7. For more complete instructions, including our standalone installers and more @@ -73,7 +73,7 @@ Dependencies The minimum required dependencies to run MNE-Python are: -- `Python `__ ≥ 3.8 +- `Python `__ ≥ 3.9 - `NumPy `__ ≥ 1.21.2 - `SciPy `__ ≥ 1.7.1 - `Matplotlib `__ ≥ 3.5.0 diff --git a/doc/development/contributing.rst b/doc/development/contributing.rst index 4a9e7f52d0e..d4c90028e77 100644 --- a/doc/development/contributing.rst +++ b/doc/development/contributing.rst @@ -243,7 +243,7 @@ Creating the virtual environment These instructions will set up a Python environment that is separated from your system-level Python and any other managed Python environments on your computer. This lets you switch between different versions of Python (MNE-Python requires -version 3.8 or higher) and also switch between the stable and development +version 3.9 or higher) and also switch between the stable and development versions of MNE-Python (so you can, for example, use the same computer to analyze your data with the stable release, and also work with the latest development version to fix bugs or add new features). Even if you've already diff --git a/environment.yml b/environment.yml index 9f0971b2fb3..3491974ffb3 100644 --- a/environment.yml +++ b/environment.yml @@ -2,7 +2,7 @@ name: mne channels: - conda-forge dependencies: - - python>=3.8 + - python>=3.9 - pip - numpy - scipy diff --git a/mne/utils/config.py b/mne/utils/config.py index fe4bc7079a4..77b94508114 100644 --- a/mne/utils/config.py +++ b/mne/utils/config.py @@ -10,7 +10,6 @@ import os import os.path as op import platform -import re import shutil import subprocess import sys @@ -626,16 +625,6 @@ def sys_info( _validate_type(check_version, (bool, "numeric"), "check_version") ljust = 24 if dependencies == "developer" else 21 platform_str = platform.platform() - if platform.system() == "Darwin" and sys.version_info[:2] < (3, 8): - # platform.platform() in Python < 3.8 doesn't call - # platform.mac_ver() if we're on Darwin, so we don't get a nice macOS - # version number. Therefore, let's do this manually here. - macos_ver = platform.mac_ver()[0] - macos_architecture = re.findall("Darwin-.*?-(.*)", platform_str) - if macos_architecture: - macos_architecture = macos_architecture[0] - platform_str = f"macOS-{macos_ver}-{macos_architecture}" - del macos_ver, macos_architecture out = partial(print, end="", file=fid) out("Platform".ljust(ljust) + platform_str + "\n") diff --git a/pyproject.toml b/pyproject.toml index 826c43d2c9c..1ff5d93139c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ authors = [ maintainers = [{ name = "Dan McCloy", email = "dan@mccloy.info" }] license = { text = "BSD-3-Clause" } readme = { file = "README.rst", content-type = "text/x-rst" } -requires-python = ">=3.8" +requires-python = ">=3.9" keywords = [ "neuroscience", "neuroimaging", From 6c6e6ec6dfea7feebc488fa7d39d3f295f4e105b Mon Sep 17 00:00:00 2001 From: Florian Hofer Date: Wed, 22 Nov 2023 11:13:29 +0100 Subject: [PATCH 085/405] Speed up .edf export with `edfio` (#12218) --- doc/changes/devel.rst | 3 +- doc/changes/names.inc | 2 + environment.yml | 2 +- mne/export/_edf.py | 339 ++++++++++++-------------------- mne/export/tests/test_export.py | 273 ++++++++++++------------- mne/utils/__init__.pyi | 4 +- mne/utils/check.py | 19 +- mne/utils/docs.py | 2 +- pyproject.toml | 2 +- 9 files changed, 254 insertions(+), 392 deletions(-) diff --git a/doc/changes/devel.rst b/doc/changes/devel.rst index b30353407d4..e3738f86b68 100644 --- a/doc/changes/devel.rst +++ b/doc/changes/devel.rst @@ -23,7 +23,8 @@ Version 1.7.dev0 (development) Enhancements ~~~~~~~~~~~~ -- None yet +- Speed up export to .edf in :func:`mne.export.export_raw` by using ``edfio`` instead of ``EDFlib-Python`` (:gh:`12218` by :newcontrib:`Florian Hofer`) + Bugs ~~~~ diff --git a/doc/changes/names.inc b/doc/changes/names.inc index da884792c4f..2ec8f2268be 100644 --- a/doc/changes/names.inc +++ b/doc/changes/names.inc @@ -172,6 +172,8 @@ .. _Felix Raimundo: https://github.com/gamazeps +.. _Florian Hofer: https://github.com/hofaflo + .. _Florin Pop: https://github.com/florin-pop .. _Frederik Weber: https://github.com/Frederik-D-Weber diff --git a/environment.yml b/environment.yml index 3491974ffb3..75c57d69346 100644 --- a/environment.yml +++ b/environment.yml @@ -56,7 +56,7 @@ dependencies: - mne-qt-browser - pymatreader - eeglabio - - edflib-python + - edfio>=0.2.1 - pybv - mamba - lazy_loader diff --git a/mne/export/_edf.py b/mne/export/_edf.py index 7097f7bd85d..04590f042da 100644 --- a/mne/export/_edf.py +++ b/mne/export/_edf.py @@ -3,46 +3,15 @@ # License: BSD-3-Clause # Copyright the MNE-Python contributors. -from contextlib import contextmanager +import datetime as dt import numpy as np -from ..utils import _check_edflib_installed, warn +from ..utils import _check_edfio_installed, warn -_check_edflib_installed() -from EDFlib.edfwriter import EDFwriter # noqa: E402 - - -def _try_to_set_value(header, key, value, channel_index=None): - """Set key/value pairs in EDF header.""" - # all EDFLib set functions are set - # for example "setPatientName()" - func_name = f"set{key}" - func = getattr(header, func_name) - - # some setter functions are indexed by channels - if channel_index is None: - return_val = func(value) - else: - return_val = func(channel_index, value) - - # a nonzero return value indicates an error - if return_val != 0: - raise RuntimeError( - f"Setting {key} with {value} " f"returned an error value " f"{return_val}." - ) - - -@contextmanager -def _auto_close(fid): - # try to close the handle no matter what - try: - yield fid - finally: - try: - fid.close() - except Exception: - pass # we did our best +_check_edfio_installed() +from edfio import Edf, EdfAnnotation, EdfSignal, Patient, Recording # noqa: E402 +from edfio._utils import round_float_to_8_characters # noqa: E402 def _export_raw(fname, raw, physical_range, add_ch_type): @@ -51,9 +20,6 @@ def _export_raw(fname, raw, physical_range, add_ch_type): TODO: if in future the Info object supports transducer or technician information, allow writing those here. """ - # scale to save data in EDF - phys_dims = "uV" - # get EEG-related data in uV units = dict( eeg="uV", ecog="uV", seeg="uV", eog="uV", ecg="uV", emg="uV", bio="uV", dbs="uV" @@ -61,7 +27,6 @@ def _export_raw(fname, raw, physical_range, add_ch_type): digital_min = -32767 digital_max = 32767 - file_type = EDFwriter.EDFLIB_FILETYPE_EDFPLUS # load data first raw.load_data() @@ -73,6 +38,7 @@ def _export_raw(fname, raw, physical_range, add_ch_type): stim_index = np.argwhere(np.array(orig_ch_types) == "stim") stim_index = np.atleast_1d(stim_index.squeeze()).tolist() drop_chs.extend([raw.ch_names[idx] for idx in stim_index]) + warn(f"Exporting STIM channels is not supported, dropping indices {stim_index}") # Add warning if any channel types are not voltage based. # Users are expected to only export data that is voltage based, @@ -97,9 +63,11 @@ def _export_raw(fname, raw, physical_range, add_ch_type): ch_names = [ch for ch in raw.ch_names if ch not in drop_chs] ch_types = np.array(raw.get_channel_types(picks=ch_names)) - n_channels = len(ch_names) n_times = raw.n_times + # get the entire dataset in uV + data = raw.get_data(units=units, picks=ch_names) + # Sampling frequency in EDF only supports integers, so to allow for # float sampling rates from Raw, we adjust the output sampling rate # for all channels and the data record duration. @@ -107,10 +75,20 @@ def _export_raw(fname, raw, physical_range, add_ch_type): if float(sfreq).is_integer(): out_sfreq = int(sfreq) data_record_duration = None + # make non-integer second durations work + if (pad_width := int(np.ceil(n_times / sfreq) * sfreq - n_times)) > 0: + warn( + f"EDF format requires equal-length data blocks, " + f"so {pad_width / sfreq} seconds of " + "zeros were appended to all channels when writing the " + "final block." + ) + data = np.pad(data, (0, int(pad_width))) else: - out_sfreq = np.floor(sfreq).astype(int) - data_record_duration = int(np.around(out_sfreq / sfreq, decimals=6) * 1e6) - + data_record_duration = round_float_to_8_characters( + np.floor(sfreq) / sfreq, round + ) + out_sfreq = np.floor(sfreq) / data_record_duration warn( f"Data has a non-integer sampling rate of {sfreq}; writing to " "EDF format may cause a small change to sample times." @@ -122,16 +100,13 @@ def _export_raw(fname, raw, physical_range, add_ch_type): linefreq = raw.info["line_freq"] filter_str_info = f"HP:{highpass}Hz LP:{lowpass}Hz N:{linefreq}Hz" - # get the entire dataset in uV - data = raw.get_data(units=units, picks=ch_names) - if physical_range == "auto": # get max and min for each channel type data ch_types_phys_max = dict() ch_types_phys_min = dict() for _type in np.unique(ch_types): - _picks = np.nonzero(ch_types == _type)[0] + _picks = [n for n, t in zip(ch_names, ch_types) if t == _type] _data = raw.get_data(units=units, picks=_picks) ch_types_phys_max[_type] = _data.max() ch_types_phys_min[_type] = _data.min() @@ -157,178 +132,106 @@ def _export_raw(fname, raw, physical_range, add_ch_type): f"The minimum μV of the data {data.min()} is " f"less than the physical min passed in {pmin}.", ) + data = np.clip(data, pmin, pmax) + signals = [] + for idx, ch in enumerate(ch_names): + ch_type = ch_types[idx] + signal_label = f"{ch_type.upper()} {ch}" if add_ch_type else ch + if len(signal_label) > 16: + raise RuntimeError( + f"Signal label for {ch} ({ch_type}) is " + f"longer than 16 characters, which is not " + f"supported in EDF. Please shorten the " + f"channel name before exporting to EDF." + ) - # create instance of EDF Writer - with _auto_close(EDFwriter(fname, file_type, n_channels)) as hdl: - # set channel data - for idx, ch in enumerate(ch_names): - ch_type = ch_types[idx] - signal_label = f"{ch_type.upper()} {ch}" if add_ch_type else ch - if len(signal_label) > 16: - raise RuntimeError( - f"Signal label for {ch} ({ch_type}) is " - f"longer than 16 characters, which is not " - f"supported in EDF. Please shorten the " - f"channel name before exporting to EDF." - ) - - if physical_range == "auto": - # take the channel type minimum and maximum - pmin = ch_types_phys_min[ch_type] - pmax = ch_types_phys_max[ch_type] - for key, val in [ - ("PhysicalMaximum", pmax), - ("PhysicalMinimum", pmin), - ("DigitalMaximum", digital_max), - ("DigitalMinimum", digital_min), - ("PhysicalDimension", phys_dims), - ("SampleFrequency", out_sfreq), - ("SignalLabel", signal_label), - ("PreFilter", filter_str_info), - ]: - _try_to_set_value(hdl, key, val, channel_index=idx) - - # set patient info - subj_info = raw.info.get("subject_info") - if subj_info is not None: - # get the full name of subject if available - first_name = subj_info.get("first_name", "") - middle_name = subj_info.get("middle_name", "") - last_name = subj_info.get("last_name", "") - name = " ".join(filter(None, [first_name, middle_name, last_name])) - - birthday = subj_info.get("birthday") - hand = subj_info.get("hand") - weight = subj_info.get("weight") - height = subj_info.get("height") - sex = subj_info.get("sex") - - additional_patient_info = [] - for key, value in [("height", height), ("weight", weight), ("hand", hand)]: - if value: - additional_patient_info.append(f"{key}={value}") - if len(additional_patient_info) == 0: - additional_patient_info = None - else: - additional_patient_info = " ".join(additional_patient_info) - - if birthday is not None: - if hdl.setPatientBirthDate(birthday[0], birthday[1], birthday[2]) != 0: - raise RuntimeError( - f"Setting patient birth date to {birthday} " - f"returned an error" - ) - for key, val in [ - ("PatientCode", subj_info.get("his_id", "")), - ("PatientName", name), - ("PatientGender", sex), - ("AdditionalPatientInfo", additional_patient_info), - ]: - # EDFwriter compares integer encodings of sex and will - # raise a TypeError if value is None as returned by - # subj_info.get(key) if key is missing. - if val is not None: - _try_to_set_value(hdl, key, val) - - # set measurement date - meas_date = raw.info["meas_date"] - if meas_date: - subsecond = int(meas_date.microsecond / 100) - if ( - hdl.setStartDateTime( - year=meas_date.year, - month=meas_date.month, - day=meas_date.day, - hour=meas_date.hour, - minute=meas_date.minute, - second=meas_date.second, - subsecond=subsecond, - ) - != 0 - ): - raise RuntimeError( - f"Setting start date time {meas_date} " f"returned an error" - ) - - device_info = raw.info.get("device_info") - if device_info is not None: - device_type = device_info.get("type") - _try_to_set_value(hdl, "Equipment", device_type) - - # set data record duration - if data_record_duration is not None: - _try_to_set_value(hdl, "DataRecordDuration", data_record_duration) - - # compute number of data records to loop over - n_blocks = np.ceil(n_times / out_sfreq).astype(int) - - # increase the number of annotation signals if necessary - annots = raw.annotations - if annots is not None: - n_annotations = len(raw.annotations) - n_annot_chans = int(n_annotations / n_blocks) + 1 - if n_annot_chans > 1: - hdl.setNumberOfAnnotationSignals(n_annot_chans) - - # Write each data record sequentially - for idx in range(n_blocks): - end_samp = (idx + 1) * out_sfreq - if end_samp > n_times: - end_samp = n_times - start_samp = idx * out_sfreq - - # then for each datarecord write each channel - for jdx in range(n_channels): - # create a buffer with sampling rate - buf = np.zeros(out_sfreq, np.float64, "C") + if physical_range == "auto": + # take the channel type minimum and maximum + pmin = ch_types_phys_min[ch_type] + pmax = ch_types_phys_max[ch_type] + + signals.append( + EdfSignal( + data[idx], + out_sfreq, + label=signal_label, + transducer_type="", + physical_dimension="uV", + physical_range=(pmin, pmax), + digital_range=(digital_min, digital_max), + prefiltering=filter_str_info, + ) + ) - # get channel data for this block - ch_data = data[jdx, start_samp:end_samp] + # set patient info + subj_info = raw.info.get("subject_info") + if subj_info is not None: + # get the full name of subject if available + first_name = subj_info.get("first_name", "") + middle_name = subj_info.get("middle_name", "") + last_name = subj_info.get("last_name", "") + name = "_".join(filter(None, [first_name, middle_name, last_name])) + + birthday = subj_info.get("birthday") + if birthday is not None: + birthday = dt.date(*birthday) + hand = subj_info.get("hand") + weight = subj_info.get("weight") + height = subj_info.get("height") + sex = subj_info.get("sex") + + additional_patient_info = [] + for key, value in [("height", height), ("weight", weight), ("hand", hand)]: + if value: + additional_patient_info.append(f"{key}={value}") + + patient = Patient( + code=subj_info.get("his_id") or "X", + sex={0: "X", 1: "M", 2: "F", None: "X"}[sex], + birthdate=birthday, + name=name or "X", + additional=additional_patient_info, + ) + else: + patient = None - # assign channel data to the buffer and write to EDF - buf[: len(ch_data)] = ch_data - err = hdl.writeSamples(buf) - if err != 0: - raise RuntimeError( - f"writeSamples() for channel{ch_names[jdx]} " - f"returned error: {err}" - ) + # set measurement date + if (meas_date := raw.info["meas_date"]) is not None: + startdate = dt.date(meas_date.year, meas_date.month, meas_date.day) + starttime = dt.time( + meas_date.hour, meas_date.minute, meas_date.second, meas_date.microsecond + ) + else: + startdate = None + starttime = None - # there was an incomplete datarecord - if len(ch_data) != len(buf): - warn( - f"EDF format requires equal-length data blocks, " - f"so {(len(buf) - len(ch_data)) / sfreq} seconds of " - "zeros were appended to all channels when writing the " - "final block." + device_info = raw.info.get("device_info") + if device_info is not None: + device_type = device_info.get("type") or "X" + recording = Recording(startdate=startdate, equipment_code=device_type) + else: + recording = Recording(startdate=startdate) + + annotations = [] + for desc, onset, duration, ch_names in zip( + raw.annotations.description, + raw.annotations.onset, + raw.annotations.duration, + raw.annotations.ch_names, + ): + if ch_names: + for ch_name in ch_names: + annotations.append( + EdfAnnotation(onset, duration, desc + f"@@{ch_name}") ) - - # write annotations - if annots is not None: - for desc, onset, duration, ch_names in zip( - raw.annotations.description, - raw.annotations.onset, - raw.annotations.duration, - raw.annotations.ch_names, - ): - # annotations are written in terms of 100 microseconds - onset = onset * 10000 - duration = duration * 10000 - if ch_names: - for ch_name in ch_names: - if ( - hdl.writeAnnotation(onset, duration, desc + f"@@{ch_name}") - != 0 - ): - raise RuntimeError( - f"writeAnnotation() returned an error " - f"trying to write {desc}@@{ch_name} at {onset} " - f"for {duration} seconds." - ) - else: - if hdl.writeAnnotation(onset, duration, desc) != 0: - raise RuntimeError( - f"writeAnnotation() returned an error " - f"trying to write {desc} at {onset} " - f"for {duration} seconds." - ) + else: + annotations.append(EdfAnnotation(onset, duration, desc)) + + Edf( + signals=signals, + patient=patient, + recording=recording, + starttime=starttime, + data_record_duration=data_record_duration, + annotations=annotations, + ).write(fname) diff --git a/mne/export/tests/test_export.py b/mne/export/tests/test_export.py index 67bd417bb50..4e86c3bb6d3 100644 --- a/mne/export/tests/test_export.py +++ b/mne/export/tests/test_export.py @@ -6,7 +6,6 @@ from contextlib import nullcontext from datetime import datetime, timezone -from os import remove from pathlib import Path import numpy as np @@ -33,7 +32,7 @@ ) from mne.tests.test_epochs import _get_data from mne.utils import ( - _check_edflib_installed, + _check_edfio_installed, _record_warnings, _resource_path, object_diff, @@ -120,17 +119,11 @@ def test_export_raw_eeglab(tmp_path): raw.export(temp_fname, overwrite=True) -@pytest.mark.skipif( - not _check_edflib_installed(strict=False), reason="edflib-python not installed" -) -def test_double_export_edf(tmp_path): - """Test exporting an EDF file multiple times.""" - rng = np.random.RandomState(123456) - format = "edf" +def _create_raw_for_edf_tests(stim_channel_index=None): + rng = np.random.RandomState(12345) ch_types = [ "eeg", "eeg", - "stim", "ecog", "ecog", "seeg", @@ -140,12 +133,27 @@ def test_double_export_edf(tmp_path): "dbs", "bio", ] - info = create_info(len(ch_types), sfreq=1000, ch_types=ch_types) - info = info.set_meas_date("2023-09-04 14:53:09.000") - data = rng.random(size=(len(ch_types), 1000)) * 1e-5 + if stim_channel_index is not None: + ch_types.insert(stim_channel_index, "stim") + ch_names = np.arange(len(ch_types)).astype(str).tolist() + info = create_info(ch_names, sfreq=1000, ch_types=ch_types) + data = rng.random(size=(len(ch_names), 2000)) * 1e-5 + return RawArray(data, info) + + +edfio_mark = pytest.mark.skipif( + not _check_edfio_installed(strict=False), reason="edfio not installed" +) + + +@edfio_mark() +def test_double_export_edf(tmp_path): + """Test exporting an EDF file multiple times.""" + raw = _create_raw_for_edf_tests(stim_channel_index=2) + raw.info.set_meas_date("2023-09-04 14:53:09.000") # include subject info and measurement date - info["subject_info"] = dict( + raw.info["subject_info"] = dict( his_id="12345", first_name="mne", last_name="python", @@ -155,15 +163,14 @@ def test_double_export_edf(tmp_path): height=1.75, hand=3, ) - raw = RawArray(data, info) # export once - temp_fname = tmp_path / f"test.{format}" - raw.export(temp_fname, add_ch_type=True) + temp_fname = tmp_path / "test.edf" + with pytest.warns(RuntimeWarning, match="Exporting STIM channels"): + raw.export(temp_fname, add_ch_type=True) raw_read = read_raw_edf(temp_fname, infer_types=True, preload=True) # export again - raw_read.load_data() raw_read.export(temp_fname, add_ch_type=True, overwrite=True) raw_read = read_raw_edf(temp_fname, infer_types=True, preload=True) @@ -171,53 +178,22 @@ def test_double_export_edf(tmp_path): raw.drop_channels("2") assert raw.ch_names == raw_read.ch_names - # only compare the original length, since extra zeros are appended - orig_raw_len = len(raw) - assert_array_almost_equal( - raw.get_data(), raw_read.get_data()[:, :orig_raw_len], decimal=4 - ) - assert_allclose(raw.times, raw_read.times[:orig_raw_len], rtol=0, atol=1e-5) + assert_array_almost_equal(raw.get_data(), raw_read.get_data(), decimal=10) + assert_array_equal(raw.times, raw_read.times) # check info for key in set(raw.info) - {"chs"}: assert raw.info[key] == raw_read.info[key] - # check channel types except for 'bio', which loses its type orig_ch_types = raw.get_channel_types() read_ch_types = raw_read.get_channel_types() assert_array_equal(orig_ch_types, read_ch_types) - # check handling of missing subject metadata - del info["subject_info"]["sex"] - raw_2 = RawArray(data, info) - raw_2.export(temp_fname, add_ch_type=True, overwrite=True) - -@pytest.mark.skipif( - not _check_edflib_installed(strict=False), reason="edflib-python not installed" -) +@edfio_mark() def test_export_edf_annotations(tmp_path): """Test that exporting EDF preserves annotations.""" - rng = np.random.RandomState(123456) - format = "edf" - ch_types = [ - "eeg", - "eeg", - "stim", - "ecog", - "ecog", - "seeg", - "eog", - "ecg", - "emg", - "dbs", - "bio", - ] - ch_names = np.arange(len(ch_types)).astype(str).tolist() - info = create_info(ch_names, sfreq=1000, ch_types=ch_types) - data = rng.random(size=(len(ch_names), 2000)) * 1.0e-5 - raw = RawArray(data, info) - + raw = _create_raw_for_edf_tests() annotations = Annotations( onset=[0.01, 0.05, 0.90, 1.05], duration=[0, 1, 0, 0], @@ -227,7 +203,7 @@ def test_export_edf_annotations(tmp_path): raw.set_annotations(annotations) # export - temp_fname = tmp_path / f"test.{format}" + temp_fname = tmp_path / "test.edf" raw.export(temp_fname) # read in the file @@ -238,24 +214,19 @@ def test_export_edf_annotations(tmp_path): assert_array_equal(raw.annotations.ch_names, raw_read.annotations.ch_names) -@pytest.mark.skipif( - not _check_edflib_installed(strict=False), reason="edflib-python not installed" -) +@edfio_mark() def test_rawarray_edf(tmp_path): """Test saving a Raw array with integer sfreq to EDF.""" - rng = np.random.RandomState(12345) - format = "edf" - ch_types = ["eeg", "eeg", "stim", "ecog", "seeg", "eog", "ecg", "emg", "dbs", "bio"] - ch_names = np.arange(len(ch_types)).astype(str).tolist() - info = create_info(ch_names, sfreq=1000, ch_types=ch_types) - data = rng.random(size=(len(ch_names), 1000)) * 1e-5 + raw = _create_raw_for_edf_tests() # include subject info and measurement date - subject_info = dict( - first_name="mne", last_name="python", birthday=(1992, 1, 20), sex=1, hand=3 + raw.info["subject_info"] = dict( + first_name="mne", + last_name="python", + birthday=(1992, 1, 20), + sex=1, + hand=3, ) - info["subject_info"] = subject_info - raw = RawArray(data, info) time_now = datetime.now() meas_date = datetime( year=time_now.year, @@ -267,125 +238,104 @@ def test_rawarray_edf(tmp_path): tzinfo=timezone.utc, ) raw.set_meas_date(meas_date) - temp_fname = tmp_path / f"test.{format}" + temp_fname = tmp_path / "test.edf" raw.export(temp_fname, add_ch_type=True) raw_read = read_raw_edf(temp_fname, infer_types=True, preload=True) - # stim channel should be dropped - raw.drop_channels("2") - assert raw.ch_names == raw_read.ch_names - # only compare the original length, since extra zeros are appended - orig_raw_len = len(raw) - assert_array_almost_equal( - raw.get_data(), raw_read.get_data()[:, :orig_raw_len], decimal=4 - ) - assert_allclose(raw.times, raw_read.times[:orig_raw_len], rtol=0, atol=1e-5) + assert_array_almost_equal(raw.get_data(), raw_read.get_data(), decimal=10) + assert_array_equal(raw.times, raw_read.times) - # check channel types except for 'bio', which loses its type orig_ch_types = raw.get_channel_types() read_ch_types = raw_read.get_channel_types() assert_array_equal(orig_ch_types, read_ch_types) assert raw.info["meas_date"] == raw_read.info["meas_date"] - # channel name can't be longer than 16 characters with the type added - raw_bad = raw.copy() - raw_bad.rename_channels({"1": "abcdefghijklmnopqrstuvwxyz"}) - with pytest.raises(RuntimeError, match="Signal label"), pytest.warns( - RuntimeWarning, match="Data has a non-integer" - ): - raw_bad.export(temp_fname, overwrite=True) - # include bad birthday that is non-EDF compliant - bad_info = info.copy() - bad_info["subject_info"]["birthday"] = (1700, 1, 20) - raw = RawArray(data, bad_info) - with pytest.raises(RuntimeError, match="Setting patient birth date"): - raw.export(temp_fname, overwrite=True) +@edfio_mark() +def test_edf_export_warns_on_non_voltage_channels(tmp_path): + """Test saving a Raw array containing a non-voltage channel.""" + temp_fname = tmp_path / "test.edf" - # include bad measurement date that is non-EDF compliant - raw = RawArray(data, info) - meas_date = datetime(year=1984, month=1, day=1, tzinfo=timezone.utc) - raw.set_meas_date(meas_date) - with pytest.raises(RuntimeError, match="Setting start date time"): - raw.export(temp_fname, overwrite=True) - - # test that warning is raised if there are non-voltage based channels - raw = RawArray(data, info) + raw = _create_raw_for_edf_tests() raw.set_channel_types({"9": "hbr"}, on_unit_change="ignore") with pytest.warns(RuntimeWarning, match="Non-voltage channels"): raw.export(temp_fname, overwrite=True) # data should match up to the non-accepted channel raw_read = read_raw_edf(temp_fname, preload=True) - orig_raw_len = len(raw) - assert_array_almost_equal( - raw.get_data()[:-1, :], raw_read.get_data()[:, :orig_raw_len], decimal=4 - ) - assert_allclose(raw.times, raw_read.times[:orig_raw_len], rtol=0, atol=1e-5) - - # the data should still match though - raw_read = read_raw_edf(temp_fname, preload=True) - raw.drop_channels("2") assert raw.ch_names == raw_read.ch_names - orig_raw_len = len(raw) - assert_array_almost_equal( - raw.get_data(), raw_read.get_data()[:, :orig_raw_len], decimal=4 - ) - assert_allclose(raw.times, raw_read.times[:orig_raw_len], rtol=0, atol=1e-5) + assert_array_almost_equal(raw.get_data()[:-1], raw_read.get_data()[:-1], decimal=10) + assert_array_equal(raw.times, raw_read.times) + + +@edfio_mark() +def test_channel_label_too_long_for_edf_raises_error(tmp_path): + """Test trying to save an EDF where a channel label is longer than 16 characters.""" + raw = _create_raw_for_edf_tests() + raw.rename_channels({"1": "abcdefghijklmnopqrstuvwxyz"}) + with pytest.raises(RuntimeError, match="Signal label"): + raw.export(tmp_path / "test.edf") -@pytest.mark.skipif( - not _check_edflib_installed(strict=False), reason="edflib-python not installed" +@edfio_mark() +def test_measurement_date_outside_range_valid_for_edf(tmp_path): + """Test trying to save an EDF with a measurement date before 1985-01-01.""" + raw = _create_raw_for_edf_tests() + raw.set_meas_date(datetime(year=1984, month=1, day=1, tzinfo=timezone.utc)) + with pytest.raises(ValueError, match="EDF only allows dates from 1985 to 2084"): + raw.export(tmp_path / "test.edf", overwrite=True) + + +@pytest.mark.parametrize( + ("physical_range", "exceeded_bound"), + [ + ((-1e6, 0), "maximum"), + ((0, 1e6), "minimum"), + ], ) +@edfio_mark() +def test_export_edf_signal_clipping(tmp_path, physical_range, exceeded_bound): + """Test if exporting data exceeding physical min/max clips and emits a warning.""" + raw = read_raw_fif(fname_raw) + raw.pick(picks=["eeg", "ecog", "seeg"]).load_data() + temp_fname = tmp_path / "test.edf" + with pytest.warns(RuntimeWarning, match=f"The {exceeded_bound}"): + raw.export(temp_fname, physical_range=physical_range) + raw_read = read_raw_edf(temp_fname, preload=True) + assert raw_read.get_data().min() >= physical_range[0] + assert raw_read.get_data().max() <= physical_range[1] + + +@edfio_mark() @pytest.mark.parametrize( - ["dataset", "format"], + ("input_path", "warning_msg"), [ - ["test", "edf"], - pytest.param("misc", "edf", marks=[pytest.mark.slowtest, misc._pytest_mark()]), + (fname_raw, "Data has a non-integer"), + pytest.param( + misc_path / "ecog" / "sample_ecog_ieeg.fif", + "EDF format requires", + marks=[pytest.mark.slowtest, misc._pytest_mark()], + ), ], ) -def test_export_raw_edf(tmp_path, dataset, format): +def test_export_raw_edf(tmp_path, input_path, warning_msg): """Test saving a Raw instance to EDF format.""" - if dataset == "test": - raw = read_raw_fif(fname_raw) - elif dataset == "misc": - fname = misc_path / "ecog" / "sample_ecog_ieeg.fif" - raw = read_raw_fif(fname) + raw = read_raw_fif(input_path) # only test with EEG channels raw.pick(picks=["eeg", "ecog", "seeg"]).load_data() - orig_ch_names = raw.ch_names - temp_fname = tmp_path / f"test.{format}" - - # test runtime errors - with pytest.warns() as record: - raw.export(temp_fname, physical_range=(-1e6, 0)) - if dataset == "test": - assert any("Data has a non-integer" in str(rec.message) for rec in record) - assert any("The maximum" in str(rec.message) for rec in record) - remove(temp_fname) - - with pytest.warns() as record: - raw.export(temp_fname, physical_range=(0, 1e6)) - if dataset == "test": - assert any("Data has a non-integer" in str(rec.message) for rec in record) - assert any("The minimum" in str(rec.message) for rec in record) - remove(temp_fname) - - if dataset == "test": - with pytest.warns(RuntimeWarning, match="Data has a non-integer"): - raw.export(temp_fname) - elif dataset == "misc": - with pytest.warns(RuntimeWarning, match="EDF format requires"): - raw.export(temp_fname) + temp_fname = tmp_path / "test.edf" + + with pytest.warns(RuntimeWarning, match=warning_msg): + raw.export(temp_fname) if "epoc" in raw.ch_names: raw.drop_channels(["epoc"]) raw_read = read_raw_edf(temp_fname, preload=True) - assert orig_ch_names == raw_read.ch_names + assert raw.ch_names == raw_read.ch_names # only compare the original length, since extra zeros are appended orig_raw_len = len(raw) @@ -395,7 +345,7 @@ def test_export_raw_edf(tmp_path, dataset, format): # will result in a resolution of 0.09 uV. This resolution # though is acceptable for most EEG manufacturers. assert_array_almost_equal( - raw.get_data(), raw_read.get_data()[:, :orig_raw_len], decimal=4 + raw.get_data(), raw_read.get_data()[:, :orig_raw_len], decimal=8 ) # Due to the data record duration limitations of EDF files, one @@ -407,6 +357,27 @@ def test_export_raw_edf(tmp_path, dataset, format): assert_allclose(raw.times, raw_read.times[:orig_raw_len], rtol=0, atol=1e-5) +@edfio_mark() +def test_export_raw_edf_does_not_fail_on_empty_header_fields(tmp_path): + """Test writing a Raw instance with empty header fields to EDF.""" + rng = np.random.RandomState(123456) + + ch_types = ["eeg"] + info = create_info(len(ch_types), sfreq=1000, ch_types=ch_types) + info["subject_info"] = { + "his_id": "", + "first_name": "", + "middle_name": "", + "last_name": "", + } + info["device_info"] = {"type": "123"} + + data = rng.random(size=(len(ch_types), 1000)) * 1e-5 + raw = RawArray(data, info) + + raw.export(tmp_path / "test.edf", add_ch_type=True) + + @pytest.mark.xfail(reason="eeglabio (usage?) bugs that should be fixed") @pytest.mark.parametrize("preload", (True, False)) def test_export_epochs_eeglab(tmp_path, preload): diff --git a/mne/utils/__init__.pyi b/mne/utils/__init__.pyi index 42694921f00..3e4d1292ee2 100644 --- a/mne/utils/__init__.pyi +++ b/mne/utils/__init__.pyi @@ -32,7 +32,7 @@ __all__ = [ "_check_depth", "_check_dict_keys", "_check_dt", - "_check_edflib_installed", + "_check_edfio_installed", "_check_eeglabio_installed", "_check_event_id", "_check_fname", @@ -230,7 +230,7 @@ from .check import ( _check_compensation_grade, _check_depth, _check_dict_keys, - _check_edflib_installed, + _check_edfio_installed, _check_eeglabio_installed, _check_event_id, _check_fname, diff --git a/mne/utils/check.py b/mne/utils/check.py index eb8e14de256..8c2bc5f919d 100644 --- a/mne/utils/check.py +++ b/mne/utils/check.py @@ -11,11 +11,9 @@ from builtins import input # no-op here but facilitates testing from difflib import get_close_matches from importlib import import_module -from importlib.metadata import version from pathlib import Path import numpy as np -from packaging.version import parse from ..defaults import HEAD_SIZE_DEFAULT, _handle_default from ..fixes import _compare_version, _median_complex @@ -368,7 +366,6 @@ def indent(x): # Mapping import namespaces to their pypi package name pip_name = dict( sklearn="scikit-learn", - EDFlib="EDFlib-Python", mne_bids="mne-bids", mne_nirs="mne-nirs", mne_features="mne-features", @@ -411,21 +408,9 @@ def _check_eeglabio_installed(strict=True): return _soft_import("eeglabio", "exporting to EEGLab", strict=strict) -def _check_edflib_installed(strict=True): +def _check_edfio_installed(strict=True): """Aux function.""" - out = _soft_import("EDFlib", "exporting to EDF", strict=strict) - if out: - # EDFlib-Python 1.0.7 is not compatible with NumPy 2.0 - # https://gitlab.com/Teuniz/EDFlib-Python/-/issues/10 - ver = version("EDFlib-Python") - if parse(ver) <= parse("1.0.7") and parse(np.__version__).major >= 2: - if strict: # pragma: no cover - raise RuntimeError( - f"EDFlib version={ver} is not compatible with NumPy 2.0, consider " - "upgrading EDFlib-Python" - ) - out = False - return out + return _soft_import("edfio", "exporting to EDF", strict=strict) def _check_pybv_installed(strict=True): diff --git a/mne/utils/docs.py b/mne/utils/docs.py index d8eb668ae04..015a5ff7d28 100644 --- a/mne/utils/docs.py +++ b/mne/utils/docs.py @@ -1463,7 +1463,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): Supported formats: - BrainVision (``.vhdr``, ``.vmrk``, ``.eeg``, uses `pybv `_) - EEGLAB (``.set``, uses :mod:`eeglabio`) - - EDF (``.edf``, uses `EDFlib-Python `_) + - EDF (``.edf``, uses `edfio `_) """ # noqa: E501 docdict[ diff --git a/pyproject.toml b/pyproject.toml index 1ff5d93139c..9c21ef711bc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -122,7 +122,7 @@ test_extra = [ "nbclient", "sphinx-gallery", "eeglabio", - "EDFlib-Python", + "edfio>=0.2.1", "pybv", "imageio>=2.6.1", "imageio-ffmpeg>=0.4.1", From 0a9c61bbbd0b70193e9bb88ddaeea3a69c6805f4 Mon Sep 17 00:00:00 2001 From: Scott Huberty <52462026+scott-huberty@users.noreply.github.com> Date: Fri, 24 Nov 2023 19:31:18 -0500 Subject: [PATCH 086/405] FIX: Allow eyetrack channels to be used with plot_compare_evoked (#12190) --- doc/changes/devel.rst | 2 +- mne/viz/evoked.py | 10 +++++++++- mne/viz/tests/test_evoked.py | 11 +++++++++++ 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/doc/changes/devel.rst b/doc/changes/devel.rst index e3738f86b68..17fe2d56680 100644 --- a/doc/changes/devel.rst +++ b/doc/changes/devel.rst @@ -28,7 +28,7 @@ Enhancements Bugs ~~~~ -- None yet +- Allow :func:`mne.viz.plot_compare_evokeds` to plot eyetracking channels, and improve error handling (:gh:`12190` by `Scott Huberty`_) API changes ~~~~~~~~~~~ diff --git a/mne/viz/evoked.py b/mne/viz/evoked.py index 11a229d80d1..ccbe48eabd4 100644 --- a/mne/viz/evoked.py +++ b/mne/viz/evoked.py @@ -2900,6 +2900,8 @@ def plot_compare_evokeds( "misc", # from ICA "emg", "ref_meg", + "eyegaze", + "pupil", ) ch_types = [ t for t in info.get_channel_types(picks=picks, unique=True) if t in all_types @@ -3002,7 +3004,13 @@ def plot_compare_evokeds( colorbar_ticks, ) = _handle_styles_pce(styles, linestyles, colors, cmap, conditions) # From now on there is only 1 channel type - assert len(ch_types) == 1 + if not len(ch_types): + got_idx = _picks_to_idx(info, picks=orig_picks) + got = np.unique(np.array(info.get_channel_types())[got_idx]).tolist() + raise RuntimeError( + f"No valid channel type(s) provided. Got {got}. Valid channel types are:" + f"\n{all_types}." + ) ch_type = ch_types[0] # some things that depend on ch_type: units = _handle_default("units")[ch_type] diff --git a/mne/viz/tests/test_evoked.py b/mne/viz/tests/test_evoked.py index b44a33385b2..b22db0bfa15 100644 --- a/mne/viz/tests/test_evoked.py +++ b/mne/viz/tests/test_evoked.py @@ -438,6 +438,17 @@ def test_plot_compare_evokeds(): yvals = line.get_ydata() assert (yvals < ylim[1]).all() assert (yvals > ylim[0]).all() + # test plotting eyetracking data + plt.close("all") # close the previous figures as to avoid a too many figs warning + info_tmp = mne.create_info(["pupil_left"], evoked.info["sfreq"], ["pupil"]) + evoked_et = mne.EvokedArray(np.ones_like(evoked.times).reshape(1, -1), info_tmp) + figs = plot_compare_evokeds(evoked_et, show_sensors=False) + assert len(figs) == 1 + # test plotting only invalid channel types + info_tmp = mne.create_info(["ias"], evoked.info["sfreq"], ["ias"]) + ev_invalid = mne.EvokedArray(np.ones_like(evoked.times).reshape(1, -1), info_tmp) + with pytest.raises(RuntimeError, match="No valid"): + plot_compare_evokeds(ev_invalid, picks="all") plt.close("all") # test other CI args From ac0d42c997f3698922736721ba272ab5e9a4b791 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20H=C3=B6chenberger?= Date: Sat, 25 Nov 2023 15:27:45 +0100 Subject: [PATCH 087/405] Fix pyproject.toml setuptools configuration (#12240) --- pyproject.toml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 9c21ef711bc..074784fa011 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -176,9 +176,8 @@ namespaces = false write_to = "mne/_version.py" version_scheme = "release-branch-semver" -[options] -zip_safe = false # the package can run out of an .egg file -include_package_data = true +[tool.setuptools] +include-package-data = true [tool.setuptools.package-data] "mne" = [ From 1334bfcdbdd2c3fa70d4e0cff8d24cdb64ce9ff7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20H=C3=B6chenberger?= Date: Mon, 27 Nov 2023 16:53:33 +0100 Subject: [PATCH 088/405] Add some omitted dependencies to "full" and "doc" variants (#12235) --- pyproject.toml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 074784fa011..79b3d29ceb1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -95,6 +95,11 @@ full = [ "darkdetect", "qdarkstyle", "threadpoolctl", + # duplicated in test_extra: + "eeglabio", + "edfio>=0.2.1", + "pybv", + "snirf", ] # Dependencies for running the test infrastructure @@ -153,7 +158,7 @@ doc = [ "ipython!=8.7.0", "selenium", ] -dev = ["mne[test,doc]"] +dev = ["mne[test,doc]", "rcssmin"] [project.urls] Homepage = "https://mne.tools/" From 476e50dd0b67474d98b96b06bf3752c0ec0e46cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20H=C3=B6chenberger?= Date: Mon, 27 Nov 2023 18:14:57 +0100 Subject: [PATCH 089/405] Add missing types to 3D viz docstrings (#12242) --- mne/viz/backends/_abstract.py | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/mne/viz/backends/_abstract.py b/mne/viz/backends/_abstract.py index 23cb65c6c44..a8e902aa33b 100644 --- a/mne/viz/backends/_abstract.py +++ b/mne/viz/backends/_abstract.py @@ -166,11 +166,11 @@ def mesh( The scalar valued associated to the vertices. vmin : float | None vmin is used to scale the colormap. - If None, the min of the data will be used + If None, the min of the data will be used. vmax : float | None vmax is used to scale the colormap. - If None, the max of the data will be used - colormap : + If None, the max of the data will be used. + colormap : str | np.ndarray | matplotlib.colors.Colormap | None The colormap to use. interpolate_before_map : Enabling makes for a smoother scalars display. Default is True. @@ -225,17 +225,17 @@ def contour( The opacity of the contour. vmin : float | None vmin is used to scale the colormap. - If None, the min of the data will be used + If None, the min of the data will be used. vmax : float | None vmax is used to scale the colormap. - If None, the max of the data will be used - colormap : + If None, the max of the data will be used. + colormap : str | np.ndarray | matplotlib.colors.Colormap | None The colormap to use. normalized_colormap : bool Specify if the values of the colormap are between 0 and 1. kind : 'line' | 'tube' The type of the primitives to use to display the contours. - color : + color : tuple | str The color of the mesh as a tuple (red, green, blue) of float values between 0 and 1 or a valid color name (i.e. 'white' or 'w'). @@ -270,11 +270,11 @@ def surface( The opacity of the surface. vmin : float | None vmin is used to scale the colormap. - If None, the min of the data will be used + If None, the min of the data will be used. vmax : float | None vmax is used to scale the colormap. - If None, the max of the data will be used - colormap : + If None, the max of the data will be used. + colormap : str | np.ndarray | matplotlib.colors.Colormap | None The colormap to use. scalars : ndarray, shape (n_vertices,) The scalar valued associated to the vertices. @@ -354,11 +354,11 @@ def tube( The optional scalar data to use. vmin : float | None vmin is used to scale the colormap. - If None, the min of the data will be used + If None, the min of the data will be used. vmax : float | None vmax is used to scale the colormap. - If None, the max of the data will be used - colormap : + If None, the max of the data will be used. + colormap : str | np.ndarray | matplotlib.colors.Colormap | None The colormap to use. opacity : float The opacity of the tube(s). @@ -446,7 +446,7 @@ def quiver3d( The optional scalar data to use. backface_culling : bool If True, enable backface culling on the quiver. - colormap : + colormap : str | np.ndarray | matplotlib.colors.Colormap | None The colormap to use. vmin : float | None vmin is used to scale the colormap. @@ -518,15 +518,15 @@ def scalarbar(self, source, color="white", title=None, n_labels=4, bgcolor=None) Parameters ---------- - source : + source The object of the scene used for the colormap. - color : + color : tuple | str The color of the label text. title : str | None The title of the scalar bar. n_labels : int | None The number of labels to display on the scalar bar. - bgcolor : + bgcolor : tuple | str The color of the background when there is transparency. """ pass From 133d589bace71074e9cfddc57b641796235aaaeb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20H=C3=B6chenberger?= Date: Mon, 27 Nov 2023 19:48:28 +0100 Subject: [PATCH 090/405] Fix incorrect type hint in Neuralynx I/O module (#12236) --- doc/api/reading_raw_data.rst | 1 + doc/changes/devel.rst | 1 + doc/changes/v1.6.rst | 2 +- doc/conf.py | 1 + mne/io/neuralynx/neuralynx.py | 14 ++++++++++++-- 5 files changed, 16 insertions(+), 3 deletions(-) diff --git a/doc/api/reading_raw_data.rst b/doc/api/reading_raw_data.rst index 1b8ebae2abf..50f524ce7c8 100644 --- a/doc/api/reading_raw_data.rst +++ b/doc/api/reading_raw_data.rst @@ -40,6 +40,7 @@ Reading raw data read_raw_nihon read_raw_fil read_raw_nsx + read_raw_neuralynx Base class: diff --git a/doc/changes/devel.rst b/doc/changes/devel.rst index 17fe2d56680..bd7cd302524 100644 --- a/doc/changes/devel.rst +++ b/doc/changes/devel.rst @@ -29,6 +29,7 @@ Enhancements Bugs ~~~~ - Allow :func:`mne.viz.plot_compare_evokeds` to plot eyetracking channels, and improve error handling (:gh:`12190` by `Scott Huberty`_) +- Fix bug with type hints in :func:`mne.io.read_raw_neuralynx` (:gh:`12236` by `Richard Höchenberger`_) API changes ~~~~~~~~~~~ diff --git a/doc/changes/v1.6.rst b/doc/changes/v1.6.rst index b5bcd3a5f11..f770b5046d2 100644 --- a/doc/changes/v1.6.rst +++ b/doc/changes/v1.6.rst @@ -5,7 +5,7 @@ Version 1.6.0 (2023-11-20) Enhancements ~~~~~~~~~~~~ -- Add support for Neuralynx data files with ``mne.io.read_raw_neuralynx`` (:gh:`11969` by :newcontrib:`Kristijan Armeni` and :newcontrib:`Ivan Skelin`) +- Add support for Neuralynx data files with :func:`mne.io.read_raw_neuralynx` (:gh:`11969` by :newcontrib:`Kristijan Armeni` and :newcontrib:`Ivan Skelin`) - Improve tests for saving splits with :class:`mne.Epochs` (:gh:`11884` by `Dmitrii Altukhov`_) - Added functionality for linking interactive figures together, such that changing one figure will affect another, see :ref:`tut-ui-events` and :mod:`mne.viz.ui_events`. Current figures implementing UI events are :func:`mne.viz.plot_topomap` and :func:`mne.viz.plot_source_estimates` (:gh:`11685` :gh:`11891` by `Marijn van Vliet`_) - HTML anchors for :class:`mne.Report` now reflect the ``section-title`` of the report items rather than using a global incrementor ``global-N`` (:gh:`11890` by `Eric Larson`_) diff --git a/doc/conf.py b/doc/conf.py index 2267fcb1026..585274426fe 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -382,6 +382,7 @@ "RawBrainVision", "RawCurry", "RawNIRX", + "RawNeuralynx", "RawGDF", "RawSNIRF", "RawBOXY", diff --git a/mne/io/neuralynx/neuralynx.py b/mne/io/neuralynx/neuralynx.py index 4bfad0fea2c..06d5000fcb6 100644 --- a/mne/io/neuralynx/neuralynx.py +++ b/mne/io/neuralynx/neuralynx.py @@ -38,7 +38,12 @@ def read_raw_neuralynx( -------- mne.io.Raw : Documentation of attributes and methods of RawNeuralynx. """ - return RawNeuralynx(fname, preload, verbose, exclude_fname_patterns) + return RawNeuralynx( + fname, + preload=preload, + exclude_fname_patterns=exclude_fname_patterns, + verbose=verbose, + ) @fill_doc @@ -47,7 +52,12 @@ class RawNeuralynx(BaseRaw): @verbose def __init__( - self, fname, preload=False, verbose=None, exclude_fname_patterns: list = None + self, + fname, + *, + preload=False, + exclude_fname_patterns=None, + verbose=None, ): _soft_import("neo", "Reading NeuralynxIO files", strict=True) from neo.io import NeuralynxIO From 5ae31b6ca7275a03332eef0c23b5d2cee68c3c42 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Tue, 28 Nov 2023 11:29:48 -0600 Subject: [PATCH 091/405] MAINT: Restore NumPy 2.0 testing (#12244) --- environment.yml | 2 +- mne/gui/tests/test_coreg.py | 3 +++ mne/report/tests/test_report.py | 2 ++ pyproject.toml | 2 +- tools/azure_dependencies.sh | 21 +++++++++-------- tools/github_actions_dependencies.sh | 34 +++++++++++++--------------- 6 files changed, 34 insertions(+), 30 deletions(-) diff --git a/environment.yml b/environment.yml index 75c57d69346..8978dfc64e8 100644 --- a/environment.yml +++ b/environment.yml @@ -33,7 +33,7 @@ dependencies: - traitlets - pyvista>=0.32,!=0.35.2,!=0.38.0,!=0.38.1,!=0.38.2,!=0.38.3,!=0.38.4,!=0.38.5,!=0.38.6,!=0.42.0 - pyvistaqt>=0.4 - - qdarkstyle + - qdarkstyle!=3.2.2 - darkdetect - dipy - nibabel diff --git a/mne/gui/tests/test_coreg.py b/mne/gui/tests/test_coreg.py index aea6fba08ff..f2372f4f3d6 100644 --- a/mne/gui/tests/test_coreg.py +++ b/mne/gui/tests/test_coreg.py @@ -93,6 +93,9 @@ def test_coreg_gui_pyvista_file_support( """Test reading supported files.""" from mne.gui import coregistration + if Path(inst_path).suffix == ".snirf": + pytest.importorskip("snirf") + if inst_path == "gen_montage": # generate a montage fig to use as inst. tmp_info = read_info(raw_path) diff --git a/mne/report/tests/test_report.py b/mne/report/tests/test_report.py index 4f307367b6a..7a2f3f27ee4 100644 --- a/mne/report/tests/test_report.py +++ b/mne/report/tests/test_report.py @@ -103,6 +103,8 @@ def _make_invisible(fig, **kwargs): @testing.requires_testing_data def test_render_report(renderer_pyvistaqt, tmp_path, invisible_fig): """Test rendering *.fif files for mne report.""" + pytest.importorskip("pymatreader") + raw_fname_new = tmp_path / "temp_raw.fif" raw_fname_new_bids = tmp_path / "temp_meg.fif" ms_fname_new = tmp_path / "temp_ms_raw.fif" diff --git a/pyproject.toml b/pyproject.toml index 79b3d29ceb1..660ea919a0a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -93,7 +93,7 @@ full = [ "trame-vuetify", "mne-qt-browser", "darkdetect", - "qdarkstyle", + "qdarkstyle!=3.2.2", "threadpoolctl", # duplicated in test_extra: "eeglabio", diff --git a/tools/azure_dependencies.sh b/tools/azure_dependencies.sh index d27c10d8224..3feb0f2df35 100755 --- a/tools/azure_dependencies.sh +++ b/tools/azure_dependencies.sh @@ -10,29 +10,30 @@ elif [ "${TEST_MODE}" == "pip-pre" ]; then echo "Numpy etc." # See github_actions_dependencies.sh for comments python -m pip install $STD_ARGS --only-binary "numpy" numpy - python -m pip install $STD_ARGS --only-binary ":all:" --extra-index-url "https://pypi.anaconda.org/scientific-python-nightly-wheels/simple" "scipy>=1.12.0.dev0" scikit-learn matplotlib pandas statsmodels - echo "dipy" - python -m pip install $STD_ARGS --only-binary ":all:" --extra-index-url "https://pypi.anaconda.org/scipy-wheels-nightly/simple" dipy - echo "h5py" - python -m pip install $STD_ARGS --only-binary ":all:" -f "https://7933911d6844c6c53a7d-47bd50c35cd79bd838daf386af554a83.ssl.cf2.rackcdn.com" h5py + python -m pip install $STD_ARGS --only-binary ":all:" --extra-index-url "https://pypi.anaconda.org/scientific-python-nightly-wheels/simple" "numpy>=2.0.0.dev0" "scipy>=1.12.0.dev0" scikit-learn matplotlib pandas statsmodels + # echo "dipy" + # python -m pip install $STD_ARGS --only-binary ":all:" --extra-index-url "https://pypi.anaconda.org/scipy-wheels-nightly/simple" dipy + # echo "h5py" + # python -m pip install $STD_ARGS --only-binary ":all:" -f "https://7933911d6844c6c53a7d-47bd50c35cd79bd838daf386af554a83.ssl.cf2.rackcdn.com" h5py + # echo "OpenMEEG" + # pip install $STD_ARGS --only-binary ":all:" --extra-index-url "https://test.pypi.org/simple" openmeeg echo "vtk" python -m pip install $STD_ARGS --only-binary ":all:" --extra-index-url "https://wheels.vtk.org" vtk - echo "nilearn and openmeeg" + echo "nilearn" python -m pip install $STD_ARGS git+https://github.com/nilearn/nilearn - python -m pip install $STD_ARGS --only-binary ":all:" --extra-index-url "https://test.pypi.org/simple" openmeeg echo "pyvista/pyvistaqt" python -m pip install --progress-bar off git+https://github.com/pyvista/pyvista python -m pip install --progress-bar off git+https://github.com/pyvista/pyvistaqt echo "misc" - python -m pip install $STD_ARGS imageio-ffmpeg xlrd mffpy python-picard pillow traitlets pybv eeglabio + python -m pip install $STD_ARGS imageio-ffmpeg xlrd mffpy pillow traitlets pybv eeglabio echo "nibabel with workaround" python -m pip install --progress-bar off git+https://github.com/nipy/nibabel.git echo "joblib" python -m pip install --progress-bar off git+https://github.com/joblib/joblib@master echo "EDFlib-Python" - python -m pip install $STD_ARGS git+https://gitlab.com/Teuniz/EDFlib-Python@master + python -m pip install $STD_ARGS git+https://github.com/the-siesta-group/edfio ./tools/check_qt_import.sh PyQt6 - python -m pip install $STD_ARGS -e .[hdf5,test] + python -m pip install $STD_ARGS -e .[test] else echo "Unknown run type ${TEST_MODE}" exit 1 diff --git a/tools/github_actions_dependencies.sh b/tools/github_actions_dependencies.sh index 2a4b90bb910..0c0069cc1eb 100755 --- a/tools/github_actions_dependencies.sh +++ b/tools/github_actions_dependencies.sh @@ -24,20 +24,18 @@ else echo "PyQt6" pip install $STD_ARGS --only-binary ":all:" --default-timeout=60 --extra-index-url https://www.riverbankcomputing.com/pypi/simple PyQt6 echo "NumPy/SciPy/pandas etc." - # As of 2023/11/20 no NumPy 2.0 because it requires everything using its ABI to - # compile against 2.0, and h5py isn't (and probably not VTK either) - pip install $STD_ARGS --only-binary "numpy" --default-timeout=60 numpy - pip install $STD_ARGS --only-binary ":all:" --default-timeout=60 --extra-index-url "https://pypi.anaconda.org/scientific-python-nightly-wheels/simple" scipy scikit-learn matplotlib pillow pandas statsmodels - echo "dipy" - pip install $STD_ARGS --only-binary ":all:" --default-timeout=60 --extra-index-url "https://pypi.anaconda.org/scipy-wheels-nightly/simple" dipy - echo "H5py" - pip install $STD_ARGS --only-binary ":all:" -f "https://7933911d6844c6c53a7d-47bd50c35cd79bd838daf386af554a83.ssl.cf2.rackcdn.com" h5py - echo "OpenMEEG" - pip install $STD_ARGS --only-binary ":all:" --extra-index-url "https://test.pypi.org/simple" openmeeg + pip install $STD_ARGS --only-binary ":all:" --default-timeout=60 --extra-index-url "https://pypi.anaconda.org/scientific-python-nightly-wheels/simple" "numpy>=2.0.0.dev0" "scipy>=1.12.0.dev0" scikit-learn matplotlib pillow pandas statsmodels + # No dipy, h5py, openmeeg, python-picard (needs numexpr) until they update to NumPy 2.0 compat + INSTALL_KIND="test_extra" + # echo "dipy" + # pip install $STD_ARGS --only-binary ":all:" --default-timeout=60 --extra-index-url "https://pypi.anaconda.org/scipy-wheels-nightly/simple" dipy + # echo "H5py" + # pip install $STD_ARGS --only-binary ":all:" -f "https://7933911d6844c6c53a7d-47bd50c35cd79bd838daf386af554a83.ssl.cf2.rackcdn.com" h5py + # echo "OpenMEEG" + # pip install $STD_ARGS --only-binary ":all:" --extra-index-url "https://test.pypi.org/simple" openmeeg # No Numba because it forces an old NumPy version - echo "nilearn and openmeeg" + echo "nilearn" pip install $STD_ARGS git+https://github.com/nilearn/nilearn - pip install $STD_ARGS openmeeg echo "VTK" pip install $STD_ARGS --only-binary ":all:" --extra-index-url "https://wheels.vtk.org" vtk python -c "import vtk" @@ -45,18 +43,18 @@ else pip install $STD_ARGS git+https://github.com/pyvista/pyvista echo "pyvistaqt" pip install $STD_ARGS git+https://github.com/pyvista/pyvistaqt - echo "imageio-ffmpeg, xlrd, mffpy, python-picard" - pip install $STD_ARGS imageio-ffmpeg xlrd mffpy python-picard patsy traitlets pybv eeglabio + echo "imageio-ffmpeg, xlrd, mffpy" + pip install $STD_ARGS imageio-ffmpeg xlrd mffpy patsy traitlets pybv eeglabio echo "mne-qt-browser" pip install $STD_ARGS git+https://github.com/mne-tools/mne-qt-browser echo "nibabel with workaround" pip install $STD_ARGS git+https://github.com/nipy/nibabel.git echo "joblib" pip install $STD_ARGS git+https://github.com/joblib/joblib@master - echo "EDFlib-Python" - pip install $STD_ARGS git+https://gitlab.com/Teuniz/EDFlib-Python@master - # Until Pandas is fixed, make sure we didn't install it - ! python -c "import pandas" + echo "edfio" + pip install $STD_ARGS git+https://github.com/the-siesta-group/edfio + # Make sure we're on a NumPy 2.0 variant + python -c "import numpy as np; assert np.__version__[0] == '2', np.__version__" fi echo "" From 88fb4a612ed7f109c3bac1910c38e32ccea49ebb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20H=C3=B6chenberger?= Date: Wed, 29 Nov 2023 16:17:37 +0100 Subject: [PATCH 092/405] MRG: Remove `_version.py` file; and small doc cleanups (#12245) --- .git_archival.txt | 4 ++++ .gitattributes | 1 + Makefile | 2 +- doc/development/contributing.rst | 4 ++-- doc/install/manual_install.rst | 2 +- doc/install/updating.rst | 4 ++-- mne/__init__.py | 7 +++---- pyproject.toml | 1 - 8 files changed, 14 insertions(+), 11 deletions(-) create mode 100644 .git_archival.txt create mode 100644 .gitattributes diff --git a/.git_archival.txt b/.git_archival.txt new file mode 100644 index 00000000000..8fb235d7045 --- /dev/null +++ b/.git_archival.txt @@ -0,0 +1,4 @@ +node: $Format:%H$ +node-date: $Format:%cI$ +describe-name: $Format:%(describe:tags=true,match=*[0-9]*)$ +ref-names: $Format:%D$ diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000000..00a7b00c94e --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +.git_archival.txt export-subst diff --git a/Makefile b/Makefile index 2843e0193b5..7d5488258d8 100644 --- a/Makefile +++ b/Makefile @@ -55,7 +55,7 @@ codespell: # running manually @codespell --builtin clear,rare,informal,names,usage -w -i 3 -q 3 -S $(CODESPELL_SKIPS) --ignore-words=ignore_words.txt --uri-ignore-words-list=bu $(CODESPELL_DIRS) check-manifest: - check-manifest -q --ignore .circleci/config.yml,doc,logo,mne/io/*/tests/data*,mne/io/tests/data,mne/preprocessing/tests/data,.DS_Store,mne/_version.py + check-manifest -q --ignore .circleci/config.yml,doc,logo,mne/io/*/tests/data*,mne/io/tests/data,mne/preprocessing/tests/data,.DS_Store,.git_archival.txt check-readme: clean wheel twine check dist/* diff --git a/doc/development/contributing.rst b/doc/development/contributing.rst index d4c90028e77..2957b434751 100644 --- a/doc/development/contributing.rst +++ b/doc/development/contributing.rst @@ -304,11 +304,11 @@ be reflected the next time you open a Python interpreter and ``import mne`` Finally, we'll add a few dependencies that are not needed for running MNE-Python, but are needed for locally running our test suite:: - $ pip install -e .[test] + $ pip install -e ".[test]" And for building our documentation:: - $ pip install -e .[doc] + $ pip install -e ".[doc]" $ conda install graphviz .. note:: diff --git a/doc/install/manual_install.rst b/doc/install/manual_install.rst index 57932648bf6..c95db0ae2d6 100644 --- a/doc/install/manual_install.rst +++ b/doc/install/manual_install.rst @@ -67,7 +67,7 @@ others), you should run via :code:`pip`: .. code-block:: console - $ pip install mne[hdf5] + $ pip install "mne[hdf5]" or via :code:`conda`: diff --git a/doc/install/updating.rst b/doc/install/updating.rst index 0737ee7c6a0..c946d5e496e 100644 --- a/doc/install/updating.rst +++ b/doc/install/updating.rst @@ -78,8 +78,8 @@ Sometimes, new features or bugfixes become available that are important to your research and you just can't wait for the next official release of MNE-Python to start taking advantage of them. In such cases, you can use ``pip`` to install the *development version* of MNE-Python. Ensure to activate the MNE conda -environment first by running ``conda activate name_of_environment``. +environment first by running ``conda activate mne``. .. code-block:: console - $ pip install -U --no-deps git+https://github.com/mne-tools/mne-python@main + $ pip install -U --no-deps https://github.com/mne-tools/mne-python/archive/refs/heads/main.zip diff --git a/mne/__init__.py b/mne/__init__.py index 594eddefdd2..10ff0c23738 100644 --- a/mne/__init__.py +++ b/mne/__init__.py @@ -23,11 +23,10 @@ __version__ = version("mne") except Exception: - try: - from ._version import __version__ - except ImportError: - __version__ = "0.0.0" + __version__ = "0.0.0" + (__getattr__, __dir__, __all__) = lazy.attach_stub(__name__, __file__) + # initialize logging from .utils import set_log_level, set_log_file diff --git a/pyproject.toml b/pyproject.toml index 660ea919a0a..b6d4fdcd5fd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -178,7 +178,6 @@ include = ["mne*"] namespaces = false [tool.setuptools_scm] -write_to = "mne/_version.py" version_scheme = "release-branch-semver" [tool.setuptools] From 2a1f7e4929b03c243375551755f5e99618acb5ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20H=C3=B6chenberger?= Date: Wed, 29 Nov 2023 16:20:36 +0100 Subject: [PATCH 093/405] MRG: Remove `importlib_resources` requirement (only needed for dropped Python <3.9 support) (#12246) --- mne/datasets/eegbci/eegbci.py | 8 +------- mne/utils/misc.py | 7 +------ pyproject.toml | 1 - 3 files changed, 2 insertions(+), 14 deletions(-) diff --git a/mne/datasets/eegbci/eegbci.py b/mne/datasets/eegbci/eegbci.py index 3af5661e5f7..93c6c731932 100644 --- a/mne/datasets/eegbci/eegbci.py +++ b/mne/datasets/eegbci/eegbci.py @@ -7,19 +7,13 @@ import os import re import time +from importlib.resources import files from os import path as op from pathlib import Path from ...utils import _url_to_local_path, logger, verbose from ..utils import _do_path_update, _downloader_params, _get_path, _log_time_size -# TODO: remove try/except when our min version is py 3.9 -try: - from importlib.resources import files -except ImportError: - from importlib_resources import files - - EEGMI_URL = "https://physionet.org/files/eegmmidb/1.0.0/" diff --git a/mne/utils/misc.py b/mne/utils/misc.py index 05d856c0226..3dbff7b2bc5 100644 --- a/mne/utils/misc.py +++ b/mne/utils/misc.py @@ -14,6 +14,7 @@ import traceback import weakref from contextlib import ExitStack, contextmanager +from importlib.resources import files from math import log from queue import Empty, Queue from string import Formatter @@ -26,12 +27,6 @@ from ._logging import logger, verbose, warn from .check import _check_option, _validate_type -# TODO: remove try/except when our min version is py 3.9 -try: - from importlib.resources import files -except ImportError: - from importlib_resources import files - # TODO: no longer needed when py3.9 is minimum supported version def _empty_hash(kind="md5"): diff --git a/pyproject.toml b/pyproject.toml index b6d4fdcd5fd..bd1b3f2ac32 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,7 +41,6 @@ dependencies = [ "decorator", "packaging", "jinja2", - "importlib_resources>=5.10.2; python_version<'3.9'", "lazy_loader>=0.3", "defusedxml", ] From b107d92ecc0526bd8cd90b0a06c13caa9a9ec87c Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Wed, 29 Nov 2023 11:15:19 -0600 Subject: [PATCH 094/405] BUG: Fix bug with last item access (#12248) --- doc/changes/devel.rst | 1 + mne/io/base.py | 3 +++ mne/io/tests/test_raw.py | 7 +++++++ 3 files changed, 11 insertions(+) diff --git a/doc/changes/devel.rst b/doc/changes/devel.rst index bd7cd302524..3f3e8036419 100644 --- a/doc/changes/devel.rst +++ b/doc/changes/devel.rst @@ -29,6 +29,7 @@ Enhancements Bugs ~~~~ - Allow :func:`mne.viz.plot_compare_evokeds` to plot eyetracking channels, and improve error handling (:gh:`12190` by `Scott Huberty`_) +- Fix bug with accessing the last data sample using ``raw[:, -1]`` where an empty array was returned (:gh:`12248` by `Eric Larson`_) - Fix bug with type hints in :func:`mne.io.read_raw_neuralynx` (:gh:`12236` by `Richard Höchenberger`_) API changes diff --git a/mne/io/base.py b/mne/io/base.py index de6f3aa589d..fd8dde30258 100644 --- a/mne/io/base.py +++ b/mne/io/base.py @@ -797,6 +797,9 @@ def _parse_get_set_params(self, item): item1 = int(item1) if isinstance(item1, (int, np.integer)): start, stop, step = item1, item1 + 1, 1 + # Need to special case -1, because -1:0 will be empty + if start == -1: + stop = None else: raise ValueError("Must pass int or slice to __getitem__") diff --git a/mne/io/tests/test_raw.py b/mne/io/tests/test_raw.py index bac32f83f65..ce5d111bcbf 100644 --- a/mne/io/tests/test_raw.py +++ b/mne/io/tests/test_raw.py @@ -1022,3 +1022,10 @@ def test_concatenate_raw_dev_head_t(): raw.info["dev_head_t"]["trans"][0, 0] = np.nan raw2 = raw.copy() concatenate_raws([raw, raw2]) + + +def test_last_samp(): + """Test that getting the last sample works.""" + raw = read_raw_fif(raw_fname).crop(0, 0.1).load_data() + last_data = raw._data[:, [-1]] + assert_array_equal(raw[:, -1][0], last_data) From a8422368e91d678c297429f19e496666a2a5e19f Mon Sep 17 00:00:00 2001 From: Nikolai Kapralov <4dvlup@gmail.com> Date: Thu, 30 Nov 2023 21:44:16 +0100 Subject: [PATCH 095/405] DOC: fix the description of extract_label_time_course modes (#12239) --- mne/utils/docs.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mne/utils/docs.py b/mne/utils/docs.py index 015a5ff7d28..6665e7550e2 100644 --- a/mne/utils/docs.py +++ b/mne/utils/docs.py @@ -1193,7 +1193,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): Valid values for ``mode`` are: - ``'max'`` - Maximum value across vertices at each time point within each label. + Maximum absolute value across vertices at each time point within each label. - ``'mean'`` Average across vertices at each time point within each label. Ignores orientation of sources for standard source estimates, which varies @@ -1203,7 +1203,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): - ``'mean_flip'`` Finds the dominant direction of source space normal vector orientations within each label, applies a sign-flip to time series at vertices whose - orientation is more than 180° different from the dominant direction, and + orientation is more than 90° different from the dominant direction, and then averages across vertices at each time point within each label. - ``'pca_flip'`` Applies singular value decomposition to the time courses within each label, From 20174f448e5f70623c3c8eda048ae43cb0b39a05 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Thu, 30 Nov 2023 16:08:34 -0500 Subject: [PATCH 096/405] MAINT: Avoid problematic sip (#12251) --- azure-pipelines.yml | 15 +++++++-------- pyproject.toml | 1 + tools/azure_dependencies.sh | 2 +- tools/github_actions_dependencies.sh | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 5cee5568623..5e70fe270ea 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -108,7 +108,7 @@ stages: - bash: | set -e python -m pip install --progress-bar off --upgrade pip - python -m pip install --progress-bar off "mne-qt-browser[opengl] @ git+https://github.com/mne-tools/mne-qt-browser.git@main" pyvista scikit-learn pytest-error-for-skips python-picard "PyQt6!=6.5.1" qtpy nibabel sphinx-gallery + python -m pip install --progress-bar off "mne-qt-browser[opengl] @ git+https://github.com/mne-tools/mne-qt-browser.git@main" pyvista scikit-learn pytest-error-for-skips python-picard "PyQt6!=6.5.1" "PyQt6-Qt6!=6.6.1" qtpy nibabel sphinx-gallery python -m pip uninstall -yq mne python -m pip install --progress-bar off --upgrade -e .[test] displayName: 'Install dependencies with pip' @@ -117,10 +117,9 @@ stages: mne sys_info -pd mne sys_info -pd | grep "qtpy .*(PyQt6=.*)$" displayName: Print config - # Uncomment if "xcb not found" Qt errors/segfaults come up again - # - bash: | - # set -e - # LD_DEBUG=libs python -c "from PyQt6.QtWidgets import QApplication, QWidget; app = QApplication([]); import matplotlib; matplotlib.use('QtAgg'); import matplotlib.pyplot as plt; plt.figure()" + - bash: | + set -e + LD_DEBUG=libs python -c "from PyQt6.QtWidgets import QApplication, QWidget; app = QApplication([]); import matplotlib; matplotlib.use('QtAgg'); import matplotlib.pyplot as plt; plt.figure()" - bash: source tools/get_testing_version.sh displayName: 'Get testing version' - task: Cache@2 @@ -188,9 +187,9 @@ stages: displayName: 'Get test data' - bash: | set -e - python -m pip install PyQt6 - # Uncomment if "xcb not found" Qt errors/segfaults come up again - # LD_DEBUG=libs python -c "from PyQt6.QtWidgets import QApplication, QWidget; app = QApplication([]); import matplotlib; matplotlib.use('QtAgg'); import matplotlib.pyplot as plt; plt.figure()" + python -m pip install PyQt6 "PyQt6-Qt6!=6.6.1" + LD_DEBUG=libs python -c "from PyQt6.QtWidgets import QApplication, QWidget; app = QApplication([]); import matplotlib; matplotlib.use('QtAgg'); import matplotlib.pyplot as plt; plt.figure()" + - bash: | mne sys_info -pd mne sys_info -pd | grep "qtpy .* (PyQt6=.*)$" PYTEST_QT_API=PyQt6 pytest -m "not slowtest" ${TEST_OPTIONS} diff --git a/pyproject.toml b/pyproject.toml index bd1b3f2ac32..daba4d874fe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -60,6 +60,7 @@ full = [ "mne[hdf5]", "qtpy", "PyQt6", + "PyQt6-Qt6!=6.6.1", "pyobjc-framework-Cocoa>=5.2.0; platform_system=='Darwin'", "sip", "scikit-learn", diff --git a/tools/azure_dependencies.sh b/tools/azure_dependencies.sh index 3feb0f2df35..32b51e631f7 100755 --- a/tools/azure_dependencies.sh +++ b/tools/azure_dependencies.sh @@ -6,7 +6,7 @@ if [ "${TEST_MODE}" == "pip" ]; then python -m pip install --only-binary="numba,llvmlite,numpy,scipy,vtk" -e .[test,full] elif [ "${TEST_MODE}" == "pip-pre" ]; then STD_ARGS="$STD_ARGS --pre" - python -m pip install $STD_ARGS --only-binary ":all:" --extra-index-url "https://www.riverbankcomputing.com/pypi/simple" PyQt6 PyQt6-sip PyQt6-Qt6 + python -m pip install $STD_ARGS --only-binary ":all:" --extra-index-url "https://www.riverbankcomputing.com/pypi/simple" PyQt6 PyQt6-sip PyQt6-Qt6 "PyQt6-Qt6!=6.6.1" echo "Numpy etc." # See github_actions_dependencies.sh for comments python -m pip install $STD_ARGS --only-binary "numpy" numpy diff --git a/tools/github_actions_dependencies.sh b/tools/github_actions_dependencies.sh index 0c0069cc1eb..0c4185d6a04 100755 --- a/tools/github_actions_dependencies.sh +++ b/tools/github_actions_dependencies.sh @@ -22,7 +22,7 @@ else echo "Numpy" pip uninstall -yq numpy echo "PyQt6" - pip install $STD_ARGS --only-binary ":all:" --default-timeout=60 --extra-index-url https://www.riverbankcomputing.com/pypi/simple PyQt6 + pip install $STD_ARGS --only-binary ":all:" --default-timeout=60 --extra-index-url https://www.riverbankcomputing.com/pypi/simple PyQt6 "PyQt6-Qt6!=6.6.1" echo "NumPy/SciPy/pandas etc." pip install $STD_ARGS --only-binary ":all:" --default-timeout=60 --extra-index-url "https://pypi.anaconda.org/scientific-python-nightly-wheels/simple" "numpy>=2.0.0.dev0" "scipy>=1.12.0.dev0" scikit-learn matplotlib pillow pandas statsmodels # No dipy, h5py, openmeeg, python-picard (needs numexpr) until they update to NumPy 2.0 compat From 45eba5a688b555d3d31ea89d9d2e0aa2fae33ff8 Mon Sep 17 00:00:00 2001 From: Alex Rockhill Date: Fri, 1 Dec 2023 10:15:56 -0800 Subject: [PATCH 097/405] [BUG, MRG] Account for clipping when finding voxel neighbors (#12252) --- mne/surface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mne/surface.py b/mne/surface.py index 285d6ab0be1..d9c6af696d7 100644 --- a/mne/surface.py +++ b/mne/surface.py @@ -2175,7 +2175,7 @@ def _get_neighbors(loc, image, voxels, thresh, dist_params): next_loc = tuple(next_loc) if ( image[next_loc] > thresh - and image[next_loc] < image[loc] + and image[next_loc] <= image[loc] and next_loc not in voxels ): neighbors.add(next_loc) From 080a2a12554415f0a4d6b6d89b63cb80534fa2aa Mon Sep 17 00:00:00 2001 From: Daniel McCloy Date: Fri, 1 Dec 2023 12:40:06 -0600 Subject: [PATCH 098/405] fix broken links (#12254) --- doc/changes/names.inc | 6 +++--- doc/links.inc | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/changes/names.inc b/doc/changes/names.inc index 2ec8f2268be..1085716a697 100644 --- a/doc/changes/names.inc +++ b/doc/changes/names.inc @@ -26,7 +26,7 @@ .. _Alexandre Barachant: https://alexandre.barachant.org -.. _Andrea Brovelli: https://andrea-brovelli.net +.. _Andrea Brovelli: https://brovelli.github.io/ .. _Andreas Hojlund: https://github.com/ahoejlund @@ -340,7 +340,7 @@ .. _Mark Alexander Henney: https://github.com/henneysq -.. _Mark Wronkiewicz: https://ml.jpl.nasa.gov/people/wronkiewicz/wronkiewicz.html +.. _Mark Wronkiewicz: https://github.com/wronk .. _Marmaduke Woodman: https://github.com/maedoc @@ -544,7 +544,7 @@ .. _Tal Linzen: https://tallinzen.net/ -.. _Teon Brooks: https://teonbrooks.com +.. _Teon Brooks: https://github.com/teonbrooks .. _Théodore Papadopoulo: https://github.com/papadop diff --git a/doc/links.inc b/doc/links.inc index 52dfec9b068..c3f265ec3a6 100644 --- a/doc/links.inc +++ b/doc/links.inc @@ -107,7 +107,7 @@ .. _anaconda: https://www.anaconda.com/products/individual .. _miniconda: https://conda.io/en/latest/miniconda.html .. _miniforge: https://github.com/conda-forge/miniforge -.. _mambaforge: https://mamba.readthedocs.io/en/latest/mamba-installation.html#mamba-install +.. _mambaforge: https://mamba.readthedocs.io/en/latest/installation/mamba-installation.html .. _installation instructions for Anaconda: http://docs.continuum.io/anaconda/install .. _installation instructions for Miniconda: https://conda.io/projects/conda/en/latest/user-guide/install/index.html .. _Anaconda troubleshooting guide: http://conda.pydata.org/docs/troubleshooting.html From 82d195dd10c3b0756b5615b6e28dbc481040825f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Dec 2023 22:25:59 +0000 Subject: [PATCH 099/405] Bump conda-incubator/setup-miniconda from 2 to 3 (#12260) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Eric Larson --- .github/workflows/tests.yml | 2 +- .gitignore | 2 ++ azure-pipelines.yml | 4 ++-- doc/api/realtime.rst | 3 ++- doc/changes/v0.23.rst | 2 +- doc/changes/v0.24.rst | 2 +- doc/conf.py | 8 ++++---- doc/install/mne_tools_suite.rst | 1 - doc/links.inc | 1 + mne/bem.py | 4 ++-- mne/datasets/_fetch.py | 6 +++--- mne/decoding/transformer.py | 7 +++---- mne/forward/_make_forward.py | 2 +- mne/utils/docs.py | 2 +- mne/viz/ica.py | 4 ++-- pyproject.toml | 2 +- tools/azure_dependencies.sh | 4 ++-- tools/github_actions_dependencies.sh | 6 +++--- 18 files changed, 32 insertions(+), 30 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index d09ed2529d1..14b4ba53f19 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -90,7 +90,7 @@ jobs: python-version: ${{ matrix.python }} if: startswith(matrix.kind, 'pip') # Python (if conda) - - uses: conda-incubator/setup-miniconda@v2 + - uses: conda-incubator/setup-miniconda@v3 with: python-version: ${{ env.PYTHON_VERSION }} environment-file: ${{ env.CONDA_ENV }} diff --git a/.gitignore b/.gitignore index be502ec189a..564599c864a 100644 --- a/.gitignore +++ b/.gitignore @@ -71,6 +71,8 @@ doc/*.dat doc/fil-result doc/optipng.exe sg_execution_times.rst +sg_api_usage.rst +sg_api_unused.dot cover *.html diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 5e70fe270ea..2bfce3b4378 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -108,7 +108,7 @@ stages: - bash: | set -e python -m pip install --progress-bar off --upgrade pip - python -m pip install --progress-bar off "mne-qt-browser[opengl] @ git+https://github.com/mne-tools/mne-qt-browser.git@main" pyvista scikit-learn pytest-error-for-skips python-picard "PyQt6!=6.5.1" "PyQt6-Qt6!=6.6.1" qtpy nibabel sphinx-gallery + python -m pip install --progress-bar off "mne-qt-browser[opengl] @ git+https://github.com/mne-tools/mne-qt-browser.git@main" pyvista scikit-learn pytest-error-for-skips python-picard "PyQt6!=6.5.1,!=6.6.1" "PyQt6-Qt6!=6.6.1" qtpy nibabel sphinx-gallery python -m pip uninstall -yq mne python -m pip install --progress-bar off --upgrade -e .[test] displayName: 'Install dependencies with pip' @@ -187,7 +187,7 @@ stages: displayName: 'Get test data' - bash: | set -e - python -m pip install PyQt6 "PyQt6-Qt6!=6.6.1" + python -m pip install "PyQt6!=6.6.1" "PyQt6-Qt6!=6.6.1" LD_DEBUG=libs python -c "from PyQt6.QtWidgets import QApplication, QWidget; app = QApplication([]); import matplotlib; matplotlib.use('QtAgg'); import matplotlib.pyplot as plt; plt.figure()" - bash: | mne sys_info -pd diff --git a/doc/api/realtime.rst b/doc/api/realtime.rst index 91c027a9e3f..0df65ad0d56 100644 --- a/doc/api/realtime.rst +++ b/doc/api/realtime.rst @@ -1,5 +1,6 @@ +.. include:: ../links.inc Realtime ======== -Realtime functionality has moved to the standalone module :mod:`mne_realtime`. +Realtime functionality has moved to the standalone module `MNE-LSL`_. diff --git a/doc/changes/v0.23.rst b/doc/changes/v0.23.rst index bf8ed2042e5..0fa34b0dc2d 100644 --- a/doc/changes/v0.23.rst +++ b/doc/changes/v0.23.rst @@ -246,7 +246,7 @@ Bugs - Fix bug with :func:`mne.grow_labels` where ``overlap=False`` could run forever or raise an error (:gh:`9317` by `Eric Larson`_) -- Fix compatibility bugs with :mod:`mne_realtime` (:gh:`8845` by `Eric Larson`_) +- Fix compatibility bugs with ``mne_realtime`` (:gh:`8845` by `Eric Larson`_) - Fix bug with `mne.viz.Brain` where non-inflated surfaces had an X-offset imposed by default (:gh:`8794` by `Eric Larson`_) diff --git a/doc/changes/v0.24.rst b/doc/changes/v0.24.rst index 425fd5d5759..1eb4abd2193 100644 --- a/doc/changes/v0.24.rst +++ b/doc/changes/v0.24.rst @@ -37,7 +37,7 @@ Enhancements ~~~~~~~~~~~~ .. - Add something cool (:gh:`9192` **by new contributor** |New Contributor|_) -- Add `pooch` to system information reports (:gh:`9801` **by new contributor** |Joshua Teves|_) +- Add ``pooch`` to system information reports (:gh:`9801` **by new contributor** |Joshua Teves|_) - Get annotation descriptions from the name field of SNIRF stimulus groups when reading SNIRF files via `mne.io.read_raw_snirf` (:gh:`9575` **by new contributor** |Darin Erat Sleiter|_) diff --git a/doc/conf.py b/doc/conf.py index 585274426fe..758cb7a529a 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -172,18 +172,14 @@ "patsy": ("https://patsy.readthedocs.io/en/latest", None), "pyvista": ("https://docs.pyvista.org", None), "imageio": ("https://imageio.readthedocs.io/en/latest", None), - "mne_realtime": ("https://mne.tools/mne-realtime", None), "picard": ("https://pierreablin.github.io/picard/", None), - "qdarkstyle": ("https://qdarkstylesheet.readthedocs.io/en/latest", None), "eeglabio": ("https://eeglabio.readthedocs.io/en/latest", None), "dipy": ( "https://dipy.org/documentation/1.7.0/", "https://dipy.org/documentation/1.7.0/objects.inv/", ), - "pooch": ("https://www.fatiando.org/pooch/latest/", None), "pybv": ("https://pybv.readthedocs.io/en/latest/", None), "pyqtgraph": ("https://pyqtgraph.readthedocs.io/en/latest/", None), - "openmeeg": ("https://openmeeg.github.io", None), } @@ -400,6 +396,10 @@ "CoregistrationUI", "IntracranialElectrodeLocator", "mne_qt_browser.figure.MNEQtBrowser", + # pooch, since its website is unreliable and users will rarely need the links + "pooch.Unzip", + "pooch.Untar", + "pooch.HTTPDownloader", } numpydoc_validate = True numpydoc_validation_checks = {"all"} | set(error_ignores) diff --git a/doc/install/mne_tools_suite.rst b/doc/install/mne_tools_suite.rst index 03b65671826..fac33b20b51 100644 --- a/doc/install/mne_tools_suite.rst +++ b/doc/install/mne_tools_suite.rst @@ -100,7 +100,6 @@ Help with installation is available through the `MNE Forum`_. See the .. _MNELAB: https://github.com/cbrnr/mnelab .. _autoreject: https://autoreject.github.io/ .. _alphaCSC: https://alphacsc.github.io/ -.. _picard: https://pierreablin.github.io/picard/ .. _pactools: https://pactools.github.io/ .. _rsa: https://github.com/wmvanvliet/mne-rsa .. _microstate: https://github.com/wmvanvliet/mne_microstates diff --git a/doc/links.inc b/doc/links.inc index c3f265ec3a6..9dd1f34872c 100644 --- a/doc/links.inc +++ b/doc/links.inc @@ -26,6 +26,7 @@ .. _`MNE-ICAlabel`: https://github.com/mne-tools/mne-icalabel .. _`MNE-Connectivity`: https://github.com/mne-tools/mne-connectivity .. _`MNE-NIRS`: https://github.com/mne-tools/mne-nirs +.. _PICARD: https://pierreablin.github.io/picard/ .. _OpenMEEG: https://openmeeg.github.io .. _openneuro-py: https://pypi.org/project/openneuro-py .. _EOSS2: https://chanzuckerberg.com/eoss/proposals/improving-usability-of-core-neuroscience-analysis-tools-with-mne-python diff --git a/mne/bem.py b/mne/bem.py index a78309ef626..b3f948fb123 100644 --- a/mne/bem.py +++ b/mne/bem.py @@ -415,8 +415,8 @@ def make_bem_solution(surfs, *, solver="mne", verbose=None): surfs : list of dict The BEM surfaces to use (from :func:`mne.make_bem_model`). solver : str - Can be ``'mne'`` (default) to use MNE-Python, or ``'openmeeg'`` to use - the :doc:`OpenMEEG ` package. + Can be ``'mne'`` (default) to use MNE-Python, or ``'openmeeg'`` to use the + `OpenMEEG `__ package. .. versionadded:: 1.2 %(verbose)s diff --git a/mne/datasets/_fetch.py b/mne/datasets/_fetch.py index 82d68d6e9f6..2b07ea29be0 100644 --- a/mne/datasets/_fetch.py +++ b/mne/datasets/_fetch.py @@ -56,7 +56,7 @@ def fetch_dataset( What to do after downloading the file. ``"unzip"`` and ``"untar"`` will decompress the downloaded file in place; for custom extraction (e.g., only extracting certain files from the archive) pass an instance of - :class:`pooch.Unzip` or :class:`pooch.Untar`. If ``None`` (the + ``pooch.Unzip`` or ``pooch.Untar``. If ``None`` (the default), the files are left as-is. path : None | str Directory in which to put the dataset. If ``None``, the dataset @@ -87,10 +87,10 @@ def fetch_dataset( Default is ``False``. auth : tuple | None Optional authentication tuple containing the username and - password/token, passed to :class:`pooch.HTTPDownloader` (e.g., + password/token, passed to ``pooch.HTTPDownloader`` (e.g., ``auth=('foo', 012345)``). token : str | None - Optional authentication token passed to :class:`pooch.HTTPDownloader`. + Optional authentication token passed to ``pooch.HTTPDownloader``. Returns ------- diff --git a/mne/decoding/transformer.py b/mne/decoding/transformer.py index 9cb22a43355..44930417e89 100644 --- a/mne/decoding/transformer.py +++ b/mne/decoding/transformer.py @@ -492,10 +492,9 @@ class FilterEstimator(TransformerMixin): Notes ----- - This is primarily meant for use in conjunction with - :class:`mne_realtime.RtEpochs`. In general it is not recommended in a - normal processing pipeline as it may result in edge artifacts. Use with - caution. + This is primarily meant for use in realtime applications. + In general it is not recommended in a normal processing pipeline as it may result + in edge artifacts. Use with caution. """ def __init__( diff --git a/mne/forward/_make_forward.py b/mne/forward/_make_forward.py index 04b0eaf9592..0b3ce69fe57 100644 --- a/mne/forward/_make_forward.py +++ b/mne/forward/_make_forward.py @@ -661,7 +661,7 @@ def make_forward_solution( followed by :func:`mne.convert_forward_solution`. .. note:: - If the BEM solution was computed with :doc:`OpenMEEG ` + If the BEM solution was computed with `OpenMEEG `__ in :func:`mne.make_bem_solution`, then OpenMEEG will automatically be used to compute the forward solution. diff --git a/mne/utils/docs.py b/mne/utils/docs.py index 6665e7550e2..a1d1d15679d 100644 --- a/mne/utils/docs.py +++ b/mne/utils/docs.py @@ -4532,7 +4532,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): theme : str | path-like Can be "auto", "light", or "dark" or a path-like to a custom stylesheet. For Dark-Mode and automatic Dark-Mode-Detection, - :mod:`qdarkstyle` and + `qdarkstyle `__ and `darkdetect `__, respectively, are required.\ If None (default), the config option {config_option} will be used, diff --git a/mne/viz/ica.py b/mne/viz/ica.py index dcd585c37fe..e2eb6273cb3 100644 --- a/mne/viz/ica.py +++ b/mne/viz/ica.py @@ -855,8 +855,8 @@ def _plot_ica_sources_evoked(evoked, picks, exclude, title, show, ica, labels=No lines[-1].set_pickradius(3.0) ax.set(title=title, xlim=times[[0, -1]], xlabel="Time (ms)", ylabel="(NA)") - if len(exclude) > 0: - plt.legend(loc="best") + if len(lines): + ax.legend(lines, exclude_labels, loc="best") texts.append( ax.text( diff --git a/pyproject.toml b/pyproject.toml index daba4d874fe..1145bdafcde 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,7 +59,7 @@ hdf5 = ["h5io", "pymatreader"] full = [ "mne[hdf5]", "qtpy", - "PyQt6", + "PyQt6!=6.6.1", "PyQt6-Qt6!=6.6.1", "pyobjc-framework-Cocoa>=5.2.0; platform_system=='Darwin'", "sip", diff --git a/tools/azure_dependencies.sh b/tools/azure_dependencies.sh index 32b51e631f7..70c82baf1c1 100755 --- a/tools/azure_dependencies.sh +++ b/tools/azure_dependencies.sh @@ -6,11 +6,11 @@ if [ "${TEST_MODE}" == "pip" ]; then python -m pip install --only-binary="numba,llvmlite,numpy,scipy,vtk" -e .[test,full] elif [ "${TEST_MODE}" == "pip-pre" ]; then STD_ARGS="$STD_ARGS --pre" - python -m pip install $STD_ARGS --only-binary ":all:" --extra-index-url "https://www.riverbankcomputing.com/pypi/simple" PyQt6 PyQt6-sip PyQt6-Qt6 "PyQt6-Qt6!=6.6.1" + python -m pip install $STD_ARGS --only-binary ":all:" --extra-index-url "https://www.riverbankcomputing.com/pypi/simple" "PyQt6!=6.6.1" PyQt6-sip PyQt6-Qt6 "PyQt6-Qt6!=6.6.1" echo "Numpy etc." # See github_actions_dependencies.sh for comments python -m pip install $STD_ARGS --only-binary "numpy" numpy - python -m pip install $STD_ARGS --only-binary ":all:" --extra-index-url "https://pypi.anaconda.org/scientific-python-nightly-wheels/simple" "numpy>=2.0.0.dev0" "scipy>=1.12.0.dev0" scikit-learn matplotlib pandas statsmodels + python -m pip install $STD_ARGS --only-binary ":all:" --extra-index-url "https://pypi.anaconda.org/scientific-python-nightly-wheels/simple" "numpy>=2.0.0.dev0" "scipy>=1.12.0.dev0" scikit-learn matplotlib statsmodels # echo "dipy" # python -m pip install $STD_ARGS --only-binary ":all:" --extra-index-url "https://pypi.anaconda.org/scipy-wheels-nightly/simple" dipy # echo "h5py" diff --git a/tools/github_actions_dependencies.sh b/tools/github_actions_dependencies.sh index 0c4185d6a04..69cf6413fb2 100755 --- a/tools/github_actions_dependencies.sh +++ b/tools/github_actions_dependencies.sh @@ -22,10 +22,10 @@ else echo "Numpy" pip uninstall -yq numpy echo "PyQt6" - pip install $STD_ARGS --only-binary ":all:" --default-timeout=60 --extra-index-url https://www.riverbankcomputing.com/pypi/simple PyQt6 "PyQt6-Qt6!=6.6.1" + pip install $STD_ARGS --only-binary ":all:" --default-timeout=60 --extra-index-url https://www.riverbankcomputing.com/pypi/simple "PyQt6!=6.6.1" "PyQt6-Qt6!=6.6.1" echo "NumPy/SciPy/pandas etc." - pip install $STD_ARGS --only-binary ":all:" --default-timeout=60 --extra-index-url "https://pypi.anaconda.org/scientific-python-nightly-wheels/simple" "numpy>=2.0.0.dev0" "scipy>=1.12.0.dev0" scikit-learn matplotlib pillow pandas statsmodels - # No dipy, h5py, openmeeg, python-picard (needs numexpr) until they update to NumPy 2.0 compat + pip install $STD_ARGS --only-binary ":all:" --default-timeout=60 --extra-index-url "https://pypi.anaconda.org/scientific-python-nightly-wheels/simple" "numpy>=2.0.0.dev0" "scipy>=1.12.0.dev0" scikit-learn matplotlib pillow statsmodels + # No pandas, dipy, h5py, openmeeg, python-picard (needs numexpr) until they update to NumPy 2.0 compat INSTALL_KIND="test_extra" # echo "dipy" # pip install $STD_ARGS --only-binary ":all:" --default-timeout=60 --extra-index-url "https://pypi.anaconda.org/scipy-wheels-nightly/simple" dipy From ac55693e1fa1d0da65791fb547ad4dd6fcd90dc5 Mon Sep 17 00:00:00 2001 From: Nikolai Kapralov <4dvlup@gmail.com> Date: Tue, 5 Dec 2023 01:31:42 +0100 Subject: [PATCH 100/405] MAINT: Replace `Path.parent.parent` with `Path.parents[N]` in tests (#12257) --- mne/_fiff/tests/test_compensator.py | 2 +- mne/_fiff/tests/test_meas_info.py | 2 +- mne/_fiff/tests/test_pick.py | 2 +- mne/_fiff/tests/test_proc_history.py | 2 +- mne/_fiff/tests/test_reference.py | 2 +- mne/_fiff/tests/test_show_fiff.py | 2 +- mne/channels/tests/test_channels.py | 2 +- mne/channels/tests/test_interpolation.py | 2 +- mne/channels/tests/test_layout.py | 2 +- mne/channels/tests/test_montage.py | 2 +- mne/decoding/tests/test_csp.py | 2 +- mne/decoding/tests/test_ems.py | 2 +- mne/decoding/tests/test_receptive_field.py | 2 +- mne/decoding/tests/test_transformer.py | 2 +- mne/forward/tests/test_forward.py | 4 +--- mne/forward/tests/test_make_forward.py | 6 ++---- mne/io/array/tests/test_array.py | 2 +- mne/io/fiff/tests/test_raw_fiff.py | 2 +- mne/io/tests/test_read_raw.py | 2 +- mne/preprocessing/tests/test_annotate_nan.py | 4 +--- mne/preprocessing/tests/test_csd.py | 2 +- mne/preprocessing/tests/test_ecg.py | 2 +- mne/preprocessing/tests/test_eog.py | 2 +- mne/preprocessing/tests/test_hfc.py | 2 +- mne/preprocessing/tests/test_ica.py | 2 +- mne/preprocessing/tests/test_interpolate.py | 2 +- mne/preprocessing/tests/test_maxwell.py | 4 ++-- mne/preprocessing/tests/test_ssp.py | 2 +- mne/preprocessing/tests/test_stim.py | 2 +- mne/preprocessing/tests/test_xdawn.py | 2 +- mne/report/tests/test_report.py | 10 +++------- mne/simulation/tests/test_evoked.py | 12 +++--------- mne/simulation/tests/test_raw.py | 4 +--- mne/source_space/tests/test_source_space.py | 2 +- mne/tests/test_annotations.py | 2 +- mne/tests/test_bem.py | 2 +- mne/tests/test_chpi.py | 2 +- mne/tests/test_cov.py | 2 +- mne/tests/test_docstring_parameters.py | 4 ++-- mne/tests/test_epochs.py | 2 +- mne/tests/test_event.py | 2 +- mne/tests/test_evoked.py | 2 +- mne/tests/test_label.py | 2 +- mne/tests/test_misc.py | 2 +- mne/tests/test_proj.py | 2 +- mne/tests/test_rank.py | 2 +- mne/tests/test_read_vectorview_selection.py | 2 +- mne/tests/test_surface.py | 2 +- mne/tests/test_transforms.py | 2 +- mne/time_frequency/tests/test_ar.py | 4 +--- mne/time_frequency/tests/test_stockwell.py | 2 +- mne/time_frequency/tests/test_tfr.py | 2 +- mne/utils/tests/test_logging.py | 2 +- mne/utils/tests/test_numerics.py | 2 +- mne/viz/tests/test_3d.py | 2 +- mne/viz/tests/test_evoked.py | 2 +- mne/viz/tests/test_ica.py | 2 +- mne/viz/tests/test_misc.py | 2 +- mne/viz/tests/test_montage.py | 4 ++-- mne/viz/tests/test_topo.py | 2 +- mne/viz/tests/test_topomap.py | 2 +- mne/viz/tests/test_utils.py | 2 +- 62 files changed, 70 insertions(+), 90 deletions(-) diff --git a/mne/_fiff/tests/test_compensator.py b/mne/_fiff/tests/test_compensator.py index 0a1b6f65fb3..350fb212032 100644 --- a/mne/_fiff/tests/test_compensator.py +++ b/mne/_fiff/tests/test_compensator.py @@ -14,7 +14,7 @@ from mne.io import read_raw_fif from mne.utils import requires_mne, run_subprocess -base_dir = Path(__file__).parent.parent.parent / "io" / "tests" / "data" +base_dir = Path(__file__).parents[2] / "io" / "tests" / "data" ctf_comp_fname = base_dir / "test_ctf_comp_raw.fif" diff --git a/mne/_fiff/tests/test_meas_info.py b/mne/_fiff/tests/test_meas_info.py index b8aa28b9e1d..9038c71a382 100644 --- a/mne/_fiff/tests/test_meas_info.py +++ b/mne/_fiff/tests/test_meas_info.py @@ -73,7 +73,7 @@ from mne.transforms import Transform from mne.utils import _empty_hash, _record_warnings, assert_object_equal, catch_logging -root_dir = Path(__file__).parent.parent.parent +root_dir = Path(__file__).parents[2] fiducials_fname = root_dir / "data" / "fsaverage" / "fsaverage-fiducials.fif" base_dir = root_dir / "io" / "tests" / "data" raw_fname = base_dir / "test_raw.fif" diff --git a/mne/_fiff/tests/test_pick.py b/mne/_fiff/tests/test_pick.py index 841ce2be9bd..5494093cd23 100644 --- a/mne/_fiff/tests/test_pick.py +++ b/mne/_fiff/tests/test_pick.py @@ -46,7 +46,7 @@ fname_meeg = data_path / "MEG" / "sample" / "sample_audvis_trunc-meg-eeg-oct-4-fwd.fif" fname_mc = data_path / "SSS" / "test_move_anon_movecomp_raw_sss.fif" -io_dir = Path(__file__).parent.parent.parent / "io" +io_dir = Path(__file__).parents[2] / "io" ctf_fname = io_dir / "tests" / "data" / "test_ctf_raw.fif" fif_fname = io_dir / "tests" / "data" / "test_raw.fif" diff --git a/mne/_fiff/tests/test_proc_history.py b/mne/_fiff/tests/test_proc_history.py index d63fafc1648..eb0880271b0 100644 --- a/mne/_fiff/tests/test_proc_history.py +++ b/mne/_fiff/tests/test_proc_history.py @@ -11,7 +11,7 @@ from mne._fiff.constants import FIFF from mne.io import read_info -base_dir = Path(__file__).parent.parent.parent / "io" / "tests" / "data" +base_dir = Path(__file__).parents[2] / "io" / "tests" / "data" raw_fname = base_dir / "test_chpi_raw_sss.fif" diff --git a/mne/_fiff/tests/test_reference.py b/mne/_fiff/tests/test_reference.py index d82338e5f63..166b06e460a 100644 --- a/mne/_fiff/tests/test_reference.py +++ b/mne/_fiff/tests/test_reference.py @@ -38,7 +38,7 @@ from mne.io import RawArray, read_raw_fif from mne.utils import _record_warnings, catch_logging -base_dir = Path(__file__).parent.parent.parent / "io" / "tests" / "data" +base_dir = Path(__file__).parents[2] / "io" / "tests" / "data" raw_fname = base_dir / "test_raw.fif" data_dir = testing.data_path(download=False) / "MEG" / "sample" fif_fname = data_dir / "sample_audvis_trunc_raw.fif" diff --git a/mne/_fiff/tests/test_show_fiff.py b/mne/_fiff/tests/test_show_fiff.py index 41fad7c22d5..e25f248b02c 100644 --- a/mne/_fiff/tests/test_show_fiff.py +++ b/mne/_fiff/tests/test_show_fiff.py @@ -7,7 +7,7 @@ from mne.io import show_fiff -base_dir = Path(__file__).parent.parent.parent / "io" / "tests" / "data" +base_dir = Path(__file__).parents[2] / "io" / "tests" / "data" fname_evoked = base_dir / "test-ave.fif" fname_raw = base_dir / "test_raw.fif" fname_c_annot = base_dir / "test_raw-annot.fif" diff --git a/mne/channels/tests/test_channels.py b/mne/channels/tests/test_channels.py index c3bbcdb33dc..b403a0e6713 100644 --- a/mne/channels/tests/test_channels.py +++ b/mne/channels/tests/test_channels.py @@ -54,7 +54,7 @@ from mne.parallel import parallel_func from mne.utils import requires_good_network -io_dir = Path(__file__).parent.parent.parent / "io" +io_dir = Path(__file__).parents[2] / "io" base_dir = io_dir / "tests" / "data" raw_fname = base_dir / "test_raw.fif" eve_fname = base_dir / "test-eve.fif" diff --git a/mne/channels/tests/test_interpolation.py b/mne/channels/tests/test_interpolation.py index 9630607caae..999e0c16402 100644 --- a/mne/channels/tests/test_interpolation.py +++ b/mne/channels/tests/test_interpolation.py @@ -20,7 +20,7 @@ ) from mne.utils import _record_warnings -base_dir = Path(__file__).parent.parent.parent / "io" / "tests" / "data" +base_dir = Path(__file__).parents[2] / "io" / "tests" / "data" raw_fname = base_dir / "test_raw.fif" event_name = base_dir / "test-eve.fif" raw_fname_ctf = base_dir / "test_ctf_raw.fif" diff --git a/mne/channels/tests/test_layout.py b/mne/channels/tests/test_layout.py index 05caa37735b..15eb50b7975 100644 --- a/mne/channels/tests/test_layout.py +++ b/mne/channels/tests/test_layout.py @@ -32,7 +32,7 @@ from mne.defaults import HEAD_SIZE_DEFAULT from mne.io import read_info, read_raw_kit -io_dir = Path(__file__).parent.parent.parent / "io" +io_dir = Path(__file__).parents[2] / "io" fif_fname = io_dir / "tests" / "data" / "test_raw.fif" lout_path = io_dir / "tests" / "data" bti_dir = io_dir / "bti" / "tests" / "data" diff --git a/mne/channels/tests/test_montage.py b/mne/channels/tests/test_montage.py index f4da1e6932e..a5e09440896 100644 --- a/mne/channels/tests/test_montage.py +++ b/mne/channels/tests/test_montage.py @@ -95,7 +95,7 @@ mgh70_fname = data_path / "SSS" / "mgh70_raw.fif" subjects_dir = data_path / "subjects" -io_dir = Path(__file__).parent.parent.parent / "io" +io_dir = Path(__file__).parents[2] / "io" kit_dir = io_dir / "kit" / "tests" / "data" elp = kit_dir / "test_elp.txt" hsp = kit_dir / "test_hsp.txt" diff --git a/mne/decoding/tests/test_csp.py b/mne/decoding/tests/test_csp.py index 1f72eacbc48..e632a02e2a7 100644 --- a/mne/decoding/tests/test_csp.py +++ b/mne/decoding/tests/test_csp.py @@ -15,7 +15,7 @@ from mne import Epochs, io, pick_types, read_events from mne.decoding.csp import CSP, SPoC, _ajd_pham -data_dir = Path(__file__).parent.parent.parent / "io" / "tests" / "data" +data_dir = Path(__file__).parents[2] / "io" / "tests" / "data" raw_fname = data_dir / "test_raw.fif" event_name = data_dir / "test-eve.fif" tmin, tmax = -0.2, 0.5 diff --git a/mne/decoding/tests/test_ems.py b/mne/decoding/tests/test_ems.py index 6b52ee7f6e1..e32664608ce 100644 --- a/mne/decoding/tests/test_ems.py +++ b/mne/decoding/tests/test_ems.py @@ -12,7 +12,7 @@ from mne import Epochs, io, pick_types, read_events from mne.decoding import EMS, compute_ems -data_dir = Path(__file__).parent.parent.parent / "io" / "tests" / "data" +data_dir = Path(__file__).parents[2] / "io" / "tests" / "data" raw_fname = data_dir / "test_raw.fif" event_name = data_dir / "test-eve.fif" tmin, tmax = -0.2, 0.5 diff --git a/mne/decoding/tests/test_receptive_field.py b/mne/decoding/tests/test_receptive_field.py index dfc570e374e..dc0d823dd32 100644 --- a/mne/decoding/tests/test_receptive_field.py +++ b/mne/decoding/tests/test_receptive_field.py @@ -20,7 +20,7 @@ ) from mne.decoding.time_delaying_ridge import _compute_corrs, _compute_reg_neighbors -data_dir = Path(__file__).parent.parent.parent / "io" / "tests" / "data" +data_dir = Path(__file__).parents[2] / "io" / "tests" / "data" raw_fname = data_dir / "test_raw.fif" event_name = data_dir / "test-eve.fif" diff --git a/mne/decoding/tests/test_transformer.py b/mne/decoding/tests/test_transformer.py index 88a8345d4b8..f7eeb78ff33 100644 --- a/mne/decoding/tests/test_transformer.py +++ b/mne/decoding/tests/test_transformer.py @@ -30,7 +30,7 @@ tmin, tmax = -0.2, 0.5 event_id = dict(aud_l=1, vis_l=3) start, stop = 0, 8 -data_dir = Path(__file__).parent.parent.parent / "io" / "tests" / "data" +data_dir = Path(__file__).parents[2] / "io" / "tests" / "data" raw_fname = data_dir / "test_raw.fif" event_name = data_dir / "test-eve.fif" diff --git a/mne/forward/tests/test_forward.py b/mne/forward/tests/test_forward.py index f636a424813..9020f7c9a26 100644 --- a/mne/forward/tests/test_forward.py +++ b/mne/forward/tests/test_forward.py @@ -44,9 +44,7 @@ fname_meeg_grad = ( data_path / "MEG" / "sample" / "sample_audvis_trunc-meg-eeg-oct-2-grad-fwd.fif" ) -fname_evoked = ( - Path(__file__).parent.parent.parent / "io" / "tests" / "data" / "test-ave.fif" -) +fname_evoked = Path(__file__).parents[2] / "io" / "tests" / "data" / "test-ave.fif" label_path = data_path / "MEG" / "sample" / "labels" diff --git a/mne/forward/tests/test_make_forward.py b/mne/forward/tests/test_make_forward.py index 7c0dfa110aa..7965ae2343c 100644 --- a/mne/forward/tests/test_make_forward.py +++ b/mne/forward/tests/test_make_forward.py @@ -53,9 +53,7 @@ data_path = testing.data_path(download=False) fname_meeg = data_path / "MEG" / "sample" / "sample_audvis_trunc-meg-eeg-oct-4-fwd.fif" -fname_raw = ( - Path(__file__).parent.parent.parent / "io" / "tests" / "data" / "test_raw.fif" -) +fname_raw = Path(__file__).parents[2] / "io" / "tests" / "data" / "test_raw.fif" fname_evo = data_path / "MEG" / "sample" / "sample_audvis_trunc-ave.fif" fname_cov = data_path / "MEG" / "sample" / "sample_audvis_trunc-cov.fif" fname_dip = data_path / "MEG" / "sample" / "sample_audvis_trunc_set1.dip" @@ -66,7 +64,7 @@ fname_aseg = subjects_dir / "sample" / "mri" / "aseg.mgz" fname_bem_meg = subjects_dir / "sample" / "bem" / "sample-1280-bem-sol.fif" -io_path = Path(__file__).parent.parent.parent / "io" +io_path = Path(__file__).parents[2] / "io" bti_dir = io_path / "bti" / "tests" / "data" kit_dir = io_path / "kit" / "tests" / "data" trans_path = kit_dir / "trans-sample.fif" diff --git a/mne/io/array/tests/test_array.py b/mne/io/array/tests/test_array.py index 59e9913175e..e8013d631aa 100644 --- a/mne/io/array/tests/test_array.py +++ b/mne/io/array/tests/test_array.py @@ -18,7 +18,7 @@ from mne.io.array import RawArray from mne.io.tests.test_raw import _test_raw_reader -base_dir = Path(__file__).parent.parent.parent / "tests" / "data" +base_dir = Path(__file__).parents[2] / "tests" / "data" fif_fname = base_dir / "test_raw.fif" diff --git a/mne/io/fiff/tests/test_raw_fiff.py b/mne/io/fiff/tests/test_raw_fiff.py index 985688a9c7e..5c760735800 100644 --- a/mne/io/fiff/tests/test_raw_fiff.py +++ b/mne/io/fiff/tests/test_raw_fiff.py @@ -52,7 +52,7 @@ ms_fname = testing_path / "SSS" / "test_move_anon_raw.fif" skip_fname = testing_path / "misc" / "intervalrecording_raw.fif" -base_dir = Path(__file__).parent.parent.parent / "tests" / "data" +base_dir = Path(__file__).parents[2] / "tests" / "data" test_fif_fname = base_dir / "test_raw.fif" test_fif_gz_fname = base_dir / "test_raw.fif.gz" ctf_fname = base_dir / "test_ctf_raw.fif" diff --git a/mne/io/tests/test_read_raw.py b/mne/io/tests/test_read_raw.py index a1e27166b0a..f98d1147539 100644 --- a/mne/io/tests/test_read_raw.py +++ b/mne/io/tests/test_read_raw.py @@ -14,7 +14,7 @@ from mne.io import read_raw from mne.io._read_raw import _get_readers, split_name_ext -base = Path(__file__).parent.parent +base = Path(__file__).parents[1] test_base = Path(testing.data_path(download=False)) diff --git a/mne/preprocessing/tests/test_annotate_nan.py b/mne/preprocessing/tests/test_annotate_nan.py index 48e8e95ce00..5e56a83f979 100644 --- a/mne/preprocessing/tests/test_annotate_nan.py +++ b/mne/preprocessing/tests/test_annotate_nan.py @@ -12,9 +12,7 @@ import mne from mne.preprocessing import annotate_nan -raw_fname = ( - Path(__file__).parent.parent.parent / "io" / "tests" / "data" / "test_raw.fif" -) +raw_fname = Path(__file__).parents[2] / "io" / "tests" / "data" / "test_raw.fif" @pytest.mark.parametrize("meas_date", (None, "orig")) diff --git a/mne/preprocessing/tests/test_csd.py b/mne/preprocessing/tests/test_csd.py index 31d3c64e5de..1c9be1a86cf 100644 --- a/mne/preprocessing/tests/test_csd.py +++ b/mne/preprocessing/tests/test_csd.py @@ -28,7 +28,7 @@ coords_fname = data_path / "test_eeg_pos.mat" csd_fname = data_path / "test_eeg_csd.mat" -io_path = Path(__file__).parent.parent.parent / "io" / "tests" / "data" +io_path = Path(__file__).parents[2] / "io" / "tests" / "data" raw_fname = io_path / "test_raw.fif" diff --git a/mne/preprocessing/tests/test_ecg.py b/mne/preprocessing/tests/test_ecg.py index 283009de5f1..73fee8c38f0 100644 --- a/mne/preprocessing/tests/test_ecg.py +++ b/mne/preprocessing/tests/test_ecg.py @@ -8,7 +8,7 @@ from mne.io import read_raw_fif from mne.preprocessing import create_ecg_epochs, find_ecg_events -data_path = Path(__file__).parent.parent.parent / "io" / "tests" / "data" +data_path = Path(__file__).parents[2] / "io" / "tests" / "data" raw_fname = data_path / "test_raw.fif" event_fname = data_path / "test-eve.fif" proj_fname = data_path / "test-proj.fif" diff --git a/mne/preprocessing/tests/test_eog.py b/mne/preprocessing/tests/test_eog.py index ad977cd581a..eb4163fcc13 100644 --- a/mne/preprocessing/tests/test_eog.py +++ b/mne/preprocessing/tests/test_eog.py @@ -6,7 +6,7 @@ from mne.io import read_raw_fif from mne.preprocessing.eog import find_eog_events -data_path = Path(__file__).parent.parent.parent / "io" / "tests" / "data" +data_path = Path(__file__).parents[2] / "io" / "tests" / "data" raw_fname = data_path / "test_raw.fif" event_fname = data_path / "test-eve.fif" proj_fname = data_path / "test-proj.fif" diff --git a/mne/preprocessing/tests/test_hfc.py b/mne/preprocessing/tests/test_hfc.py index 50157bc8551..66af5304cf9 100644 --- a/mne/preprocessing/tests/test_hfc.py +++ b/mne/preprocessing/tests/test_hfc.py @@ -18,7 +18,7 @@ fil_path = testing.data_path(download=False) / "FIL" fname_root = "sub-noise_ses-001_task-noise220622_run-001" -io_dir = Path(__file__).parent.parent.parent / "io" +io_dir = Path(__file__).parents[2] / "io" ctf_fname = io_dir / "tests" / "data" / "test_ctf_raw.fif" fif_fname = io_dir / "tests" / "data" / "test_raw.fif" diff --git a/mne/preprocessing/tests/test_ica.py b/mne/preprocessing/tests/test_ica.py index d96cfbfcbc9..184f7aeefdf 100644 --- a/mne/preprocessing/tests/test_ica.py +++ b/mne/preprocessing/tests/test_ica.py @@ -57,7 +57,7 @@ from mne.rank import _compute_rank_int from mne.utils import _record_warnings, catch_logging, check_version -data_dir = Path(__file__).parent.parent.parent / "io" / "tests" / "data" +data_dir = Path(__file__).parents[2] / "io" / "tests" / "data" raw_fname = data_dir / "test_raw.fif" event_name = data_dir / "test-eve.fif" test_cov_name = data_dir / "test-cov.fif" diff --git a/mne/preprocessing/tests/test_interpolate.py b/mne/preprocessing/tests/test_interpolate.py index b2b05446c84..8bf4cf0e345 100644 --- a/mne/preprocessing/tests/test_interpolate.py +++ b/mne/preprocessing/tests/test_interpolate.py @@ -12,7 +12,7 @@ from mne.preprocessing.interpolate import _find_centroid_sphere from mne.transforms import _cart_to_sph -base_dir = Path(__file__).parent.parent.parent / "io" / "tests" / "data" +base_dir = Path(__file__).parents[2] / "io" / "tests" / "data" raw_fname = base_dir / "test_raw.fif" event_name = base_dir / "test-eve.fif" raw_fname_ctf = base_dir / "test_ctf_raw.fif" diff --git a/mne/preprocessing/tests/test_maxwell.py b/mne/preprocessing/tests/test_maxwell.py index 4bfd5cd396c..b806ccf577a 100644 --- a/mne/preprocessing/tests/test_maxwell.py +++ b/mne/preprocessing/tests/test_maxwell.py @@ -58,7 +58,7 @@ use_log_level, ) -io_path = Path(__file__).parent.parent.parent / "io" / "tests" / "data" +io_path = Path(__file__).parents[2] / "io" / "tests" / "data" raw_small_fname = io_path / "test_raw.fif" data_path = testing.data_path(download=False) @@ -122,7 +122,7 @@ tri_ctc_fname = triux_path / "ct_sparse_BMLHUS.fif" tri_cal_fname = triux_path / "sss_cal_BMLHUS.dat" -io_dir = Path(__file__).parent.parent.parent / "io" +io_dir = Path(__file__).parents[2] / "io" fname_ctf_raw = io_dir / "tests" / "data" / "test_ctf_comp_raw.fif" ctf_fname_continuous = data_path / "CTF" / "testdata_ctf.ds" diff --git a/mne/preprocessing/tests/test_ssp.py b/mne/preprocessing/tests/test_ssp.py index fdcf4f9db23..359a844686c 100644 --- a/mne/preprocessing/tests/test_ssp.py +++ b/mne/preprocessing/tests/test_ssp.py @@ -12,7 +12,7 @@ from mne.io import read_raw_ctf, read_raw_fif from mne.preprocessing.ssp import compute_proj_ecg, compute_proj_eog -data_path = Path(__file__).parent.parent.parent / "io" / "tests" / "data" +data_path = Path(__file__).parents[2] / "io" / "tests" / "data" raw_fname = data_path / "test_raw.fif" dur_use = 5.0 eog_times = np.array([0.5, 2.3, 3.6, 14.5]) diff --git a/mne/preprocessing/tests/test_stim.py b/mne/preprocessing/tests/test_stim.py index 2ef1c6e367a..270b8d93354 100644 --- a/mne/preprocessing/tests/test_stim.py +++ b/mne/preprocessing/tests/test_stim.py @@ -14,7 +14,7 @@ from mne.io import read_raw_fif from mne.preprocessing.stim import fix_stim_artifact -data_path = Path(__file__).parent.parent.parent / "io" / "tests" / "data" +data_path = Path(__file__).parents[2] / "io" / "tests" / "data" raw_fname = data_path / "test_raw.fif" event_fname = data_path / "test-eve.fif" diff --git a/mne/preprocessing/tests/test_xdawn.py b/mne/preprocessing/tests/test_xdawn.py index 31e751acb37..f56629db6db 100644 --- a/mne/preprocessing/tests/test_xdawn.py +++ b/mne/preprocessing/tests/test_xdawn.py @@ -24,7 +24,7 @@ from mne.io import read_raw_fif from mne.preprocessing.xdawn import Xdawn, _XdawnTransformer -base_dir = Path(__file__).parent.parent.parent / "io" / "tests" / "data" +base_dir = Path(__file__).parents[2] / "io" / "tests" / "data" raw_fname = base_dir / "test_raw.fif" event_name = base_dir / "test-eve.fif" diff --git a/mne/report/tests/test_report.py b/mne/report/tests/test_report.py index 7a2f3f27ee4..0e74201c1cb 100644 --- a/mne/report/tests/test_report.py +++ b/mne/report/tests/test_report.py @@ -57,13 +57,9 @@ inv_fname = sample_meg_dir / "sample_audvis_trunc-meg-eeg-oct-6-meg-inv.fif" stc_fname = sample_meg_dir / "sample_audvis_trunc-meg" mri_fname = subjects_dir / "sample" / "mri" / "T1.mgz" -bdf_fname = ( - Path(__file__).parent.parent.parent / "io" / "edf" / "tests" / "data" / "test.bdf" -) -edf_fname = ( - Path(__file__).parent.parent.parent / "io" / "edf" / "tests" / "data" / "test.edf" -) -base_dir = Path(__file__).parent.parent.parent / "io" / "tests" / "data" +bdf_fname = Path(__file__).parents[2] / "io" / "edf" / "tests" / "data" / "test.bdf" +edf_fname = Path(__file__).parents[2] / "io" / "edf" / "tests" / "data" / "test.edf" +base_dir = Path(__file__).parents[2] / "io" / "tests" / "data" evoked_fname = base_dir / "test-ave.fif" nirs_fname = ( data_dir / "SNIRF" / "NIRx" / "NIRSport2" / "1.0.3" / "2021-05-05_001.snirf" diff --git a/mne/simulation/tests/test_evoked.py b/mne/simulation/tests/test_evoked.py index bc33d6195a2..b8fc7f12ff8 100644 --- a/mne/simulation/tests/test_evoked.py +++ b/mne/simulation/tests/test_evoked.py @@ -34,15 +34,9 @@ data_path = testing.data_path(download=False) fwd_fname = data_path / "MEG" / "sample" / "sample_audvis_trunc-meg-eeg-oct-6-fwd.fif" -raw_fname = ( - Path(__file__).parent.parent.parent / "io" / "tests" / "data" / "test_raw.fif" -) -ave_fname = ( - Path(__file__).parent.parent.parent / "io" / "tests" / "data" / "test-ave.fif" -) -cov_fname = ( - Path(__file__).parent.parent.parent / "io" / "tests" / "data" / "test-cov.fif" -) +raw_fname = Path(__file__).parents[2] / "io" / "tests" / "data" / "test_raw.fif" +ave_fname = Path(__file__).parents[2] / "io" / "tests" / "data" / "test-ave.fif" +cov_fname = Path(__file__).parents[2] / "io" / "tests" / "data" / "test-cov.fif" @testing.requires_testing_data diff --git a/mne/simulation/tests/test_raw.py b/mne/simulation/tests/test_raw.py index bf4caf3bdeb..97700ce9f00 100644 --- a/mne/simulation/tests/test_raw.py +++ b/mne/simulation/tests/test_raw.py @@ -59,9 +59,7 @@ from mne.tests.test_chpi import _assert_quats from mne.utils import catch_logging -raw_fname_short = ( - Path(__file__).parent.parent.parent / "io" / "tests" / "data" / "test_raw.fif" -) +raw_fname_short = Path(__file__).parents[2] / "io" / "tests" / "data" / "test_raw.fif" data_path = testing.data_path(download=False) raw_fname = data_path / "MEG" / "sample" / "sample_audvis_trunc_raw.fif" diff --git a/mne/source_space/tests/test_source_space.py b/mne/source_space/tests/test_source_space.py index 4db0286a2a5..2389e59cb5b 100644 --- a/mne/source_space/tests/test_source_space.py +++ b/mne/source_space/tests/test_source_space.py @@ -66,7 +66,7 @@ fname_src = data_path / "subjects" / "sample" / "bem" / "sample-oct-4-src.fif" fname_fwd = data_path / "MEG" / "sample" / "sample_audvis_trunc-meg-eeg-oct-4-fwd.fif" trans_fname = data_path / "MEG" / "sample" / "sample_audvis_trunc-trans.fif" -base_dir = Path(__file__).parent.parent.parent / "io" / "tests" / "data" +base_dir = Path(__file__).parents[2] / "io" / "tests" / "data" fname_small = base_dir / "small-src.fif.gz" fname_ave = base_dir / "test-ave.fif" rng = np.random.RandomState(0) diff --git a/mne/tests/test_annotations.py b/mne/tests/test_annotations.py index 35ffca3d09a..12964118f32 100644 --- a/mne/tests/test_annotations.py +++ b/mne/tests/test_annotations.py @@ -49,7 +49,7 @@ data_path = testing.data_path(download=False) data_dir = data_path / "MEG" / "sample" -fif_fname = Path(__file__).parent.parent / "io" / "tests" / "data" / "test_raw.fif" +fif_fname = Path(__file__).parents[1] / "io" / "tests" / "data" / "test_raw.fif" first_samps = pytest.mark.parametrize("first_samp", (0, 10000)) edf_reduced = data_path / "EDF" / "test_reduced.edf" edf_annot_only = data_path / "EDF" / "SC4001EC-Hypnogram.edf" diff --git a/mne/tests/test_bem.py b/mne/tests/test_bem.py index 0dd682606f6..0457444d7b3 100644 --- a/mne/tests/test_bem.py +++ b/mne/tests/test_bem.py @@ -46,7 +46,7 @@ from mne.transforms import translation from mne.utils import catch_logging, check_version -fname_raw = Path(__file__).parent.parent / "io" / "tests" / "data" / "test_raw.fif" +fname_raw = Path(__file__).parents[1] / "io" / "tests" / "data" / "test_raw.fif" subjects_dir = testing.data_path(download=False) / "subjects" fname_bem_3 = subjects_dir / "sample" / "bem" / "sample-320-320-320-bem.fif" fname_bem_1 = subjects_dir / "sample" / "bem" / "sample-320-bem.fif" diff --git a/mne/tests/test_chpi.py b/mne/tests/test_chpi.py index 3e0e3fb1e87..35b4dd00794 100644 --- a/mne/tests/test_chpi.py +++ b/mne/tests/test_chpi.py @@ -46,7 +46,7 @@ from mne.utils import assert_meg_snr, catch_logging, object_diff, verbose from mne.viz import plot_head_positions -base_dir = Path(__file__).parent.parent / "io" / "tests" / "data" +base_dir = Path(__file__).parents[1] / "io" / "tests" / "data" ctf_fname = base_dir / "test_ctf_raw.fif" hp_fif_fname = base_dir / "test_chpi_raw_sss.fif" hp_fname = base_dir / "test_chpi_raw_hp.txt" diff --git a/mne/tests/test_cov.py b/mne/tests/test_cov.py index 5398c07ace7..2b7570d127c 100644 --- a/mne/tests/test_cov.py +++ b/mne/tests/test_cov.py @@ -53,7 +53,7 @@ from mne.rank import _compute_rank_int from mne.utils import _record_warnings, assert_snr, catch_logging -base_dir = Path(__file__).parent.parent / "io" / "tests" / "data" +base_dir = Path(__file__).parents[1] / "io" / "tests" / "data" cov_fname = base_dir / "test-cov.fif" cov_gz_fname = base_dir / "test-cov.fif.gz" cov_km_fname = base_dir / "test-km-cov.fif" diff --git a/mne/tests/test_docstring_parameters.py b/mne/tests/test_docstring_parameters.py index 222165901a3..0118a6c36ba 100644 --- a/mne/tests/test_docstring_parameters.py +++ b/mne/tests/test_docstring_parameters.py @@ -285,7 +285,7 @@ def test_tabs(): def test_documented(): """Test that public functions and classes are documented.""" - doc_dir = (Path(__file__).parent.parent.parent / "doc").absolute() + 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) @@ -357,7 +357,7 @@ def test_docdict_order(): from mne.utils.docs import docdict # read the file as text, and get entries via regex - docs_path = Path(__file__).parent.parent / "utils" / "docs.py" + docs_path = Path(__file__).parents[1] / "utils" / "docs.py" assert docs_path.is_file(), docs_path with open(docs_path, "r", encoding="UTF-8") as fid: docs = fid.read() diff --git a/mne/tests/test_epochs.py b/mne/tests/test_epochs.py index 0200fb45f79..1c0ff6c027c 100644 --- a/mne/tests/test_epochs.py +++ b/mne/tests/test_epochs.py @@ -76,7 +76,7 @@ fname_raw_movecomp_sss = data_path / "SSS" / "test_move_anon_movecomp_raw_sss.fif" fname_raw_move_pos = data_path / "SSS" / "test_move_anon_raw.pos" -base_dir = Path(__file__).parent.parent / "io" / "tests" / "data" +base_dir = Path(__file__).parents[1] / "io" / "tests" / "data" raw_fname = base_dir / "test_raw.fif" event_name = base_dir / "test-eve.fif" evoked_nf_name = base_dir / "test-nf-ave.fif" diff --git a/mne/tests/test_event.py b/mne/tests/test_event.py index 0d6ad7e0416..7d899291232 100644 --- a/mne/tests/test_event.py +++ b/mne/tests/test_event.py @@ -40,7 +40,7 @@ ) from mne.io import RawArray, read_raw_fif -base_dir = Path(__file__).parent.parent / "io" / "tests" / "data" +base_dir = Path(__file__).parents[1] / "io" / "tests" / "data" fname = base_dir / "test-eve.fif" fname_raw = base_dir / "test_raw.fif" fname_gz = base_dir / "test-eve.fif.gz" diff --git a/mne/tests/test_evoked.py b/mne/tests/test_evoked.py index 8c1dd5631c4..2c5f064606d 100644 --- a/mne/tests/test_evoked.py +++ b/mne/tests/test_evoked.py @@ -36,7 +36,7 @@ from mne.io import read_raw_fif from mne.utils import grand_average -base_dir = Path(__file__).parent.parent / "io" / "tests" / "data" +base_dir = Path(__file__).parents[1] / "io" / "tests" / "data" fname = base_dir / "test-ave.fif" fname_gz = base_dir / "test-ave.fif.gz" raw_fname = base_dir / "test_raw.fif" diff --git a/mne/tests/test_label.py b/mne/tests/test_label.py index 35e41d91f6c..ff28eaa1423 100644 --- a/mne/tests/test_label.py +++ b/mne/tests/test_label.py @@ -64,7 +64,7 @@ src_bad_fname = data_path / "subjects" / "fsaverage" / "bem" / "fsaverage-ico-5-src.fif" label_dir = subjects_dir / "sample" / "label" / "aparc" -test_path = Path(__file__).parent.parent / "io" / "tests" / "data" +test_path = Path(__file__).parents[1] / "io" / "tests" / "data" label_fname = test_path / "test-lh.label" label_rh_fname = test_path / "test-rh.label" diff --git a/mne/tests/test_misc.py b/mne/tests/test_misc.py index 887d45a6ffb..54286669e57 100644 --- a/mne/tests/test_misc.py +++ b/mne/tests/test_misc.py @@ -7,7 +7,7 @@ from mne.misc import parse_config -ave_fname = Path(__file__).parent.parent / "io" / "tests" / "data" / "test.ave" +ave_fname = Path(__file__).parents[1] / "io" / "tests" / "data" / "test.ave" def test_parse_ave(): diff --git a/mne/tests/test_proj.py b/mne/tests/test_proj.py index f13272dd2c1..36437238f90 100644 --- a/mne/tests/test_proj.py +++ b/mne/tests/test_proj.py @@ -42,7 +42,7 @@ from mne.rank import _compute_rank_int from mne.utils import _record_warnings -base_dir = Path(__file__).parent.parent / "io" / "tests" / "data" +base_dir = Path(__file__).parents[1] / "io" / "tests" / "data" raw_fname = base_dir / "test_raw.fif" event_fname = base_dir / "test-eve.fif" proj_fname = base_dir / "test-proj.fif" diff --git a/mne/tests/test_rank.py b/mne/tests/test_rank.py index fb9efcba615..f726aed9a75 100644 --- a/mne/tests/test_rank.py +++ b/mne/tests/test_rank.py @@ -29,7 +29,7 @@ estimate_rank, ) -base_dir = Path(__file__).parent.parent / "io" / "tests" / "data" +base_dir = Path(__file__).parents[1] / "io" / "tests" / "data" cov_fname = base_dir / "test-cov.fif" raw_fname = base_dir / "test_raw.fif" ave_fname = base_dir / "test-ave.fif" diff --git a/mne/tests/test_read_vectorview_selection.py b/mne/tests/test_read_vectorview_selection.py index 844a30edc5d..e0ed6f0af20 100644 --- a/mne/tests/test_read_vectorview_selection.py +++ b/mne/tests/test_read_vectorview_selection.py @@ -7,7 +7,7 @@ from mne import read_vectorview_selection from mne.io import read_raw_fif -test_path = Path(__file__).parent.parent / "io" / "tests" / "data" +test_path = Path(__file__).parents[1] / "io" / "tests" / "data" raw_fname = test_path / "test_raw.fif" raw_new_fname = test_path / "test_chpi_raw_sss.fif" diff --git a/mne/tests/test_surface.py b/mne/tests/test_surface.py index 646b2793706..6199bdfbe41 100644 --- a/mne/tests/test_surface.py +++ b/mne/tests/test_surface.py @@ -49,7 +49,7 @@ def test_helmet(): """Test loading helmet surfaces.""" - base_dir = Path(__file__).parent.parent / "io" + base_dir = Path(__file__).parents[1] / "io" fname_raw = base_dir / "tests" / "data" / "test_raw.fif" fname_kit_raw = base_dir / "kit" / "tests" / "data" / "test_bin_raw.fif" fname_bti_raw = base_dir / "bti" / "tests" / "data" / "exported4D_linux_raw.fif" diff --git a/mne/tests/test_transforms.py b/mne/tests/test_transforms.py index 2246609bdb8..ef6433d951d 100644 --- a/mne/tests/test_transforms.py +++ b/mne/tests/test_transforms.py @@ -64,7 +64,7 @@ subjects_dir = data_path / "subjects" fname_t1 = subjects_dir / "fsaverage" / "mri" / "T1.mgz" -base_dir = Path(__file__).parent.parent / "io" / "tests" / "data" +base_dir = Path(__file__).parents[1] / "io" / "tests" / "data" fname_trans = base_dir / "sample-audvis-raw-trans.txt" test_fif_fname = base_dir / "test_raw.fif" ctf_fname = base_dir / "test_ctf_raw.fif" diff --git a/mne/time_frequency/tests/test_ar.py b/mne/time_frequency/tests/test_ar.py index bef37e7dd18..f0ea9db2a1e 100644 --- a/mne/time_frequency/tests/test_ar.py +++ b/mne/time_frequency/tests/test_ar.py @@ -10,9 +10,7 @@ from mne import io from mne.time_frequency.ar import _yule_walker, fit_iir_model_raw -raw_fname = ( - Path(__file__).parent.parent.parent / "io" / "tests" / "data" / "test_raw.fif" -) +raw_fname = Path(__file__).parents[2] / "io" / "tests" / "data" / "test_raw.fif" def test_yule_walker(): diff --git a/mne/time_frequency/tests/test_stockwell.py b/mne/time_frequency/tests/test_stockwell.py index 96b2c064801..ffdde8bf24e 100644 --- a/mne/time_frequency/tests/test_stockwell.py +++ b/mne/time_frequency/tests/test_stockwell.py @@ -29,7 +29,7 @@ ) from mne.utils import _record_warnings -base_dir = Path(__file__).parent.parent.parent / "io" / "tests" / "data" +base_dir = Path(__file__).parents[2] / "io" / "tests" / "data" raw_fname = base_dir / "test_raw.fif" raw_ctf_fname = base_dir / "test_ctf_raw.fif" diff --git a/mne/time_frequency/tests/test_tfr.py b/mne/time_frequency/tests/test_tfr.py index 33e82d5a126..35c132ad3f1 100644 --- a/mne/time_frequency/tests/test_tfr.py +++ b/mne/time_frequency/tests/test_tfr.py @@ -41,7 +41,7 @@ from mne.utils import catch_logging, grand_average from mne.viz.utils import _fake_click, _fake_keypress, _fake_scroll -data_path = Path(__file__).parent.parent.parent / "io" / "tests" / "data" +data_path = Path(__file__).parents[2] / "io" / "tests" / "data" raw_fname = data_path / "test_raw.fif" event_fname = data_path / "test-eve.fif" raw_ctf_fname = data_path / "test_ctf_raw.fif" diff --git a/mne/utils/tests/test_logging.py b/mne/utils/tests/test_logging.py index 81613749aaf..2128140dcf5 100644 --- a/mne/utils/tests/test_logging.py +++ b/mne/utils/tests/test_logging.py @@ -26,7 +26,7 @@ ) from mne.utils._logging import _frame_info -base_dir = Path(__file__).parent.parent.parent / "io" / "tests" / "data" +base_dir = Path(__file__).parents[2] / "io" / "tests" / "data" fname_raw = base_dir / "test_raw.fif" fname_evoked = base_dir / "test-ave.fif" fname_log = base_dir / "test-ave.log" diff --git a/mne/utils/tests/test_numerics.py b/mne/utils/tests/test_numerics.py index 12f366776ea..645623fa6a0 100644 --- a/mne/utils/tests/test_numerics.py +++ b/mne/utils/tests/test_numerics.py @@ -45,7 +45,7 @@ ) from mne.utils.numerics import _LRU_CACHE_MAXSIZES, _LRU_CACHES -base_dir = Path(__file__).parent.parent.parent / "io" / "tests" / "data" +base_dir = Path(__file__).parents[2] / "io" / "tests" / "data" fname_raw = base_dir / "test_raw.fif" ave_fname = base_dir / "test-ave.fif" cov_fname = base_dir / "test-cov.fif" diff --git a/mne/viz/tests/test_3d.py b/mne/viz/tests/test_3d.py index 54ebf0fcd83..8f6346549e2 100644 --- a/mne/viz/tests/test_3d.py +++ b/mne/viz/tests/test_3d.py @@ -66,7 +66,7 @@ ctf_fname = data_dir / "CTF" / "testdata_ctf.ds" nirx_fname = data_dir / "NIRx" / "nirscout" / "nirx_15_2_recording_w_short" -io_dir = Path(__file__).parent.parent.parent / "io" +io_dir = Path(__file__).parents[2] / "io" base_dir = io_dir / "tests" / "data" evoked_fname = base_dir / "test-ave.fif" diff --git a/mne/viz/tests/test_evoked.py b/mne/viz/tests/test_evoked.py index b22db0bfa15..b6fe6d87511 100644 --- a/mne/viz/tests/test_evoked.py +++ b/mne/viz/tests/test_evoked.py @@ -38,7 +38,7 @@ from mne.viz import plot_compare_evokeds, plot_evoked_white from mne.viz.utils import _fake_click, _get_cmap -base_dir = Path(__file__).parent.parent.parent / "io" / "tests" / "data" +base_dir = Path(__file__).parents[2] / "io" / "tests" / "data" evoked_fname = base_dir / "test-ave.fif" raw_fname = base_dir / "test_raw.fif" raw_sss_fname = base_dir / "test_chpi_raw_sss.fif" diff --git a/mne/viz/tests/test_ica.py b/mne/viz/tests/test_ica.py index 421d844e127..35903a5f802 100644 --- a/mne/viz/tests/test_ica.py +++ b/mne/viz/tests/test_ica.py @@ -26,7 +26,7 @@ from mne.viz.ica import _create_properties_layout, plot_ica_properties from mne.viz.utils import _fake_click, _fake_keypress -base_dir = Path(__file__).parent.parent.parent / "io" / "tests" / "data" +base_dir = Path(__file__).parents[2] / "io" / "tests" / "data" evoked_fname = base_dir / "test-ave.fif" raw_fname = base_dir / "test_raw.fif" cov_fname = base_dir / "test-cov.fif" diff --git a/mne/viz/tests/test_misc.py b/mne/viz/tests/test_misc.py index 49d3e7219bb..c8ef70bbbc0 100644 --- a/mne/viz/tests/test_misc.py +++ b/mne/viz/tests/test_misc.py @@ -51,7 +51,7 @@ evoked_fname = data_path / "MEG" / "sample" / "sample_audvis-ave.fif" dip_fname = data_path / "MEG" / "sample" / "sample_audvis_trunc_set1.dip" chpi_fif_fname = data_path / "SSS" / "test_move_anon_raw.fif" -base_dir = Path(__file__).parent.parent.parent / "io" / "tests" / "data" +base_dir = Path(__file__).parents[2] / "io" / "tests" / "data" raw_fname = base_dir / "test_raw.fif" cov_fname = base_dir / "test-cov.fif" event_fname = base_dir / "test-eve.fif" diff --git a/mne/viz/tests/test_montage.py b/mne/viz/tests/test_montage.py index 0a95cdbbb55..332ca82a6a4 100644 --- a/mne/viz/tests/test_montage.py +++ b/mne/viz/tests/test_montage.py @@ -15,12 +15,12 @@ from mne.channels import make_dig_montage, make_standard_montage, read_dig_fif -p_dir = Path(__file__).parent.parent.parent / "io" / "kit" / "tests" / "data" +p_dir = Path(__file__).parents[2] / "io" / "kit" / "tests" / "data" elp = p_dir / "test_elp.txt" hsp = p_dir / "test_hsp.txt" hpi = p_dir / "test_mrk.sqd" point_names = ["nasion", "lpa", "rpa", "1", "2", "3", "4", "5"] -io_dir = Path(__file__).parent.parent.parent / "io" / "tests" / "data" +io_dir = Path(__file__).parents[2] / "io" / "tests" / "data" fif_fname = io_dir / "test_raw.fif" diff --git a/mne/viz/tests/test_topo.py b/mne/viz/tests/test_topo.py index fc421136c94..12c345e6623 100644 --- a/mne/viz/tests/test_topo.py +++ b/mne/viz/tests/test_topo.py @@ -30,7 +30,7 @@ from mne.viz.topo import _imshow_tfr, _plot_update_evoked_topo_proj, iter_topography from mne.viz.utils import _fake_click -base_dir = Path(__file__).parent.parent.parent / "io" / "tests" / "data" +base_dir = Path(__file__).parents[2] / "io" / "tests" / "data" evoked_fname = base_dir / "test-ave.fif" raw_fname = base_dir / "test_raw.fif" event_name = base_dir / "test-eve.fif" diff --git a/mne/viz/tests/test_topomap.py b/mne/viz/tests/test_topomap.py index 33ae3cc645c..2774e198fe8 100644 --- a/mne/viz/tests/test_topomap.py +++ b/mne/viz/tests/test_topomap.py @@ -63,7 +63,7 @@ ecg_fname = data_dir / "MEG" / "sample" / "sample_audvis_ecg-proj.fif" triux_fname = data_dir / "SSS" / "TRIUX" / "triux_bmlhus_erm_raw.fif" -base_dir = Path(__file__).parent.parent.parent / "io" / "tests" / "data" +base_dir = Path(__file__).parents[2] / "io" / "tests" / "data" evoked_fname = base_dir / "test-ave.fif" raw_fname = base_dir / "test_raw.fif" event_name = base_dir / "test-eve.fif" diff --git a/mne/viz/tests/test_utils.py b/mne/viz/tests/test_utils.py index f0679563da3..cb9e40b583c 100644 --- a/mne/viz/tests/test_utils.py +++ b/mne/viz/tests/test_utils.py @@ -30,7 +30,7 @@ concatenate_images, ) -base_dir = Path(__file__).parent.parent.parent / "io" / "tests" / "data" +base_dir = Path(__file__).parents[2] / "io" / "tests" / "data" raw_fname = base_dir / "test_raw.fif" cov_fname = base_dir / "test-cov.fif" ev_fname = base_dir / "test_raw-eve.fif" From 6187c836cdb4bbab4f222d7918df6f006a3beaa5 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Mon, 4 Dec 2023 19:57:53 -0500 Subject: [PATCH 101/405] MAINT: Post-release deprecations (#12265) Co-authored-by: Daniel McCloy --- mne/_fiff/__init__.py | 19 -- mne/commands/tests/test_commands.py | 27 --- mne/gui/_gui.py | 67 +------ mne/io/constants.py | 9 - mne/io/meas_info.py | 11 -- mne/io/pick.py | 9 - mne/io/proj.py | 11 -- mne/io/reference.py | 11 -- mne/io/tag.py | 11 -- mne/io/tests/test_deprecation.py | 30 --- mne/io/utils.py | 11 -- mne/io/write.py | 11 -- mne/preprocessing/__init__.pyi | 2 - mne/preprocessing/maxfilter.py | 230 ---------------------- mne/source_space/_source_space.py | 3 - mne/tests/test_bem.py | 2 +- mne/time_frequency/spectrum.py | 48 +---- mne/time_frequency/tests/test_spectrum.py | 137 +------------ mne/viz/_brain/_brain.py | 10 - mne/viz/backends/_abstract.py | 9 - mne/viz/backends/_pyvista.py | 21 -- mne/viz/backends/renderer.py | 5 - mne/viz/topomap.py | 3 +- mne/viz/utils.py | 10 +- 24 files changed, 21 insertions(+), 686 deletions(-) delete mode 100644 mne/io/meas_info.py delete mode 100644 mne/io/proj.py delete mode 100644 mne/io/reference.py delete mode 100644 mne/io/tag.py delete mode 100644 mne/io/tests/test_deprecation.py delete mode 100644 mne/io/utils.py delete mode 100644 mne/io/write.py delete mode 100644 mne/preprocessing/maxfilter.py diff --git a/mne/_fiff/__init__.py b/mne/_fiff/__init__.py index 6402d78b325..877068fe54d 100644 --- a/mne/_fiff/__init__.py +++ b/mne/_fiff/__init__.py @@ -7,22 +7,3 @@ # All imports should be done directly to submodules, so we don't import # anything here or use lazy_loader. - -# This warn import (made private as _warn) is just for the temporary -# _io_dep_getattr and can be removed in 1.6 along with _dep_msg and _io_dep_getattr. -from ..utils import warn as _warn - - -_dep_msg = ( - "is deprecated will be removed in 1.6, use documented public API instead. " - "If no appropriate public API exists, please open an issue on GitHub." -) - - -def _io_dep_getattr(name, mod): - import importlib - - fiff_mod = importlib.import_module(f"mne._fiff.{mod}") - obj = getattr(fiff_mod, name) - _warn(f"mne.io.{mod}.{name} {_dep_msg}", FutureWarning) - return obj diff --git a/mne/commands/tests/test_commands.py b/mne/commands/tests/test_commands.py index ae5e84cbd58..ea87c717db0 100644 --- a/mne/commands/tests/test_commands.py +++ b/mne/commands/tests/test_commands.py @@ -31,7 +31,6 @@ mne_flash_bem, mne_kit2fiff, mne_make_scalp_surfaces, - mne_maxfilter, mne_prepare_bem_model, mne_report, mne_setup_forward_model, @@ -206,32 +205,6 @@ def test_make_scalp_surfaces(tmp_path, monkeypatch): assert "SUBJECTS_DIR" not in os.environ -def test_maxfilter(): - """Test mne maxfilter.""" - check_usage(mne_maxfilter) - with ArgvSetter( - ( - "-i", - raw_fname, - "--st", - "--movecomp", - "--linefreq", - "60", - "--trans", - raw_fname, - ) - ) as out: - with pytest.warns(RuntimeWarning, match="Don't use"): - os.environ["_MNE_MAXFILTER_TEST"] = "true" - try: - mne_maxfilter.run() - finally: - del os.environ["_MNE_MAXFILTER_TEST"] - out = out.stdout.getvalue() - for check in ("maxfilter", "-trans", "-movecomp"): - assert check in out, check - - @pytest.mark.slowtest @testing.requires_testing_data def test_report(tmp_path): diff --git a/mne/gui/_gui.py b/mne/gui/_gui.py index 3c437f9d266..e522986a60c 100644 --- a/mne/gui/_gui.py +++ b/mne/gui/_gui.py @@ -3,31 +3,24 @@ # License: BSD-3-Clause # Copyright the MNE-Python contributors. -from ..utils import get_config, verbose, warn +from ..utils import get_config, verbose @verbose def coregistration( *, - tabbed=None, - split=None, width=None, + height=None, inst=None, subject=None, subjects_dir=None, - guess_mri_subject=None, - height=None, head_opacity=None, head_high_res=None, trans=None, - scrollable=None, orient_to_surface=None, scale_by_distance=None, mark_inside=None, interaction=None, - scale=None, - advanced_rendering=None, - head_inside=None, fullscreen=None, show=True, block=False, @@ -45,29 +38,20 @@ def coregistration( Parameters ---------- - tabbed : bool - Combine the data source panel and the coregistration panel into a - single panel with tabs. - split : bool - Split the main panels with a movable splitter (good for QT4 but - unnecessary for wx backend). width : int | None Specify the width for window (in logical pixels). Default is None, which uses ``MNE_COREG_WINDOW_WIDTH`` config value (which defaults to 800). + height : int | None + Specify a height for window (in logical pixels). + Default is None, which uses ``MNE_COREG_WINDOW_WIDTH`` config value + (which defaults to 400). inst : None | str Path to an instance file containing the digitizer data. Compatible for Raw, Epochs, and Evoked files. subject : None | str Name of the mri subject. %(subjects_dir)s - guess_mri_subject : bool - When selecting a new head shape file, guess the subject's name based - on the filename and change the MRI subject accordingly (default True). - height : int | None - Specify a height for window (in logical pixels). - Default is None, which uses ``MNE_COREG_WINDOW_WIDTH`` config value - (which defaults to 400). head_opacity : float | None The opacity of the head surface in the range [0., 1.]. Default is None, which uses ``MNE_COREG_HEAD_OPACITY`` config value @@ -78,8 +62,6 @@ def coregistration( (which defaults to True). trans : path-like | None The transform file to use. - scrollable : bool - Make the coregistration panel vertically scrollable (default True). orient_to_surface : bool | None If True (default), orient EEG electrode and head shape points to the head surface. @@ -102,21 +84,6 @@ def coregistration( .. versionchanged:: 1.0 Default interaction mode if ``None`` and no config setting found changed from ``'trackball'`` to ``'terrain'``. - scale : float | None - The scaling for the scene. - - .. versionadded:: 0.16 - advanced_rendering : bool - Use advanced OpenGL rendering techniques (default True). - For some renderers (such as MESA software) this can cause rendering - bugs. - - .. versionadded:: 0.18 - head_inside : bool - If True (default), add opaque inner scalp head surface to help occlude - points behind the head. - - .. versionadded:: 0.23 %(fullscreen)s Default is None, which uses ``MNE_COREG_FULLSCREEN`` config value (which defaults to False). @@ -143,28 +110,6 @@ def coregistration( .. youtube:: ALV5qqMHLlQ """ - unsupported_params = { - "tabbed": tabbed, - "split": split, - "scrollable": scrollable, - "head_inside": head_inside, - "guess_mri_subject": guess_mri_subject, - "scale": scale, - "advanced_rendering": advanced_rendering, - } - for key, val in unsupported_params.items(): - if isinstance(val, tuple): - to_raise = val[0] != val[1] - else: - to_raise = val is not None - if to_raise: - warn( - f"The parameter {key} is deprecated and will be removed in 1.7, do " - "not pass a value for it", - FutureWarning, - ) - del tabbed, split, scrollable, head_inside, guess_mri_subject, scale - del advanced_rendering config = get_config() if head_high_res is None: head_high_res = config.get("MNE_COREG_HEAD_HIGH_RES", "true") == "true" diff --git a/mne/io/constants.py b/mne/io/constants.py index 61db99600f2..fdac1a856c2 100644 --- a/mne/io/constants.py +++ b/mne/io/constants.py @@ -3,15 +3,6 @@ # License: BSD-3-Clause # Copyright the MNE-Python contributors. -from .._fiff import _io_dep_getattr from .._fiff.constants import FIFF __all__ = ["FIFF"] - - -def __getattr__(name): - try: - return globals()[name] - except KeyError: - pass - return _io_dep_getattr(name, "constants") diff --git a/mne/io/meas_info.py b/mne/io/meas_info.py deleted file mode 100644 index f971fff18d4..00000000000 --- a/mne/io/meas_info.py +++ /dev/null @@ -1,11 +0,0 @@ -# Author: Eric Larson -# -# License: BSD-3-Clause -# Copyright the MNE-Python contributors. - - -from .._fiff import _io_dep_getattr - - -def __getattr__(name): - return _io_dep_getattr(name, "meas_info") diff --git a/mne/io/pick.py b/mne/io/pick.py index f7c77b1af14..4ae1d25b3c5 100644 --- a/mne/io/pick.py +++ b/mne/io/pick.py @@ -4,7 +4,6 @@ # Copyright the MNE-Python contributors. -from .._fiff import _io_dep_getattr from .._fiff.pick import ( _DATA_CH_TYPES_ORDER_DEFAULT, _DATA_CH_TYPES_SPLIT, @@ -18,11 +17,3 @@ "_DATA_CH_TYPES_ORDER_DEFAULT", "_DATA_CH_TYPES_SPLIT", ] - - -def __getattr__(name): - try: - return globals()[name] - except KeyError: - pass - return _io_dep_getattr(name, "pick") diff --git a/mne/io/proj.py b/mne/io/proj.py deleted file mode 100644 index 98445f1ce7e..00000000000 --- a/mne/io/proj.py +++ /dev/null @@ -1,11 +0,0 @@ -# Author: Eric Larson -# -# License: BSD-3-Clause -# Copyright the MNE-Python contributors. - - -from .._fiff import _io_dep_getattr - - -def __getattr__(name): - return _io_dep_getattr(name, "proj") diff --git a/mne/io/reference.py b/mne/io/reference.py deleted file mode 100644 index 850d6bd7294..00000000000 --- a/mne/io/reference.py +++ /dev/null @@ -1,11 +0,0 @@ -# Author: Eric Larson -# -# License: BSD-3-Clause -# Copyright the MNE-Python contributors. - - -from .._fiff import _io_dep_getattr - - -def __getattr__(name): - return _io_dep_getattr(name, "reference") diff --git a/mne/io/tag.py b/mne/io/tag.py deleted file mode 100644 index 41dc15fd40d..00000000000 --- a/mne/io/tag.py +++ /dev/null @@ -1,11 +0,0 @@ -# Author: Eric Larson -# -# License: BSD-3-Clause -# Copyright the MNE-Python contributors. - - -from .._fiff import _io_dep_getattr - - -def __getattr__(name): - return _io_dep_getattr(name, "tag") diff --git a/mne/io/tests/test_deprecation.py b/mne/io/tests/test_deprecation.py deleted file mode 100644 index fecf9a78091..00000000000 --- a/mne/io/tests/test_deprecation.py +++ /dev/null @@ -1,30 +0,0 @@ -"""Test deprecation of mne.io private attributes to mne._fiff.""" - -# Author: Eric Larson -# -# License: BSD-3-Clause -# Copyright the MNE-Python contributors. - -import pytest - - -def test_deprecation(): - """Test deprecation of mne.io FIFF stuff.""" - import mne.io - - # Shouldn't warn (backcompat) - mne.io.constants.FIFF - mne.io.pick._picks_to_idx - mne.io.get_channel_type_constants() - - # Should warn - with pytest.warns(FutureWarning, match=r"mne\.io\.pick\.pick_channels is dep"): - from mne.io.pick import pick_channels # noqa: F401 - with pytest.warns(FutureWarning, match=r"mne\.io\.pick\.pick_channels is dep"): - mne.io.pick.pick_channels - with pytest.warns(FutureWarning, match=r"mne\.io\.meas_info\.read_info is dep"): - from mne.io.meas_info import read_info # noqa: F401 - from mne.io import meas_info - - with pytest.warns(FutureWarning, match=r"mne\.io\.meas_info\.read_info is dep"): - meas_info.read_info diff --git a/mne/io/utils.py b/mne/io/utils.py deleted file mode 100644 index 9460ceed55e..00000000000 --- a/mne/io/utils.py +++ /dev/null @@ -1,11 +0,0 @@ -# Author: Eric Larson -# -# License: BSD-3-Clause -# Copyright the MNE-Python contributors. - - -from .._fiff import _io_dep_getattr - - -def __getattr__(name): - return _io_dep_getattr(name, "utils") diff --git a/mne/io/write.py b/mne/io/write.py deleted file mode 100644 index 12c0ae00ca0..00000000000 --- a/mne/io/write.py +++ /dev/null @@ -1,11 +0,0 @@ -# Author: Eric Larson -# -# License: BSD-3-Clause -# Copyright the MNE-Python contributors. - - -from .._fiff import _io_dep_getattr - - -def __getattr__(name): - return _io_dep_getattr(name, "write") diff --git a/mne/preprocessing/__init__.pyi b/mne/preprocessing/__init__.pyi index 7d0741ab30a..0ea66345687 100644 --- a/mne/preprocessing/__init__.pyi +++ b/mne/preprocessing/__init__.pyi @@ -7,7 +7,6 @@ __all__ = [ "annotate_movement", "annotate_muscle_zscore", "annotate_nan", - "apply_maxfilter", "compute_average_dev_head_t", "compute_bridged_electrodes", "compute_current_source_density", @@ -77,7 +76,6 @@ from .ica import ( ) from .infomax_ import infomax from .interpolate import equalize_bads, interpolate_bridged_electrodes -from .maxfilter import apply_maxfilter from .maxwell import ( compute_maxwell_basis, find_bad_channels_maxwell, diff --git a/mne/preprocessing/maxfilter.py b/mne/preprocessing/maxfilter.py deleted file mode 100644 index 64a48b68cf3..00000000000 --- a/mne/preprocessing/maxfilter.py +++ /dev/null @@ -1,230 +0,0 @@ -# Authors: Alexandre Gramfort -# Matti Hämäläinen -# Martin Luessi -# -# License: BSD-3-Clause -# Copyright the MNE-Python contributors. - -import os - -from ..bem import fit_sphere_to_headshape -from ..io import read_raw_fif -from ..utils import deprecated, logger, verbose, warn - - -def _mxwarn(msg): - """Warn about a bug.""" - warn( - "Possible MaxFilter bug: %s, more info: " - "http://imaging.mrc-cbu.cam.ac.uk/meg/maxbugs" % msg - ) - - -@deprecated( - "apply_maxfilter will be removed in 1.7, use mne.preprocessing.maxwell_filter or " - "the MEGIN command-line utility maxfilter and mne.bem.fit_sphere_to_headshape " - "instead." -) -@verbose -def apply_maxfilter( - in_fname, - out_fname, - origin=None, - frame="device", - bad=None, - autobad="off", - skip=None, - force=False, - st=False, - st_buflen=16.0, - st_corr=0.96, - mv_trans=None, - mv_comp=False, - mv_headpos=False, - mv_hp=None, - mv_hpistep=None, - mv_hpisubt=None, - mv_hpicons=True, - linefreq=None, - cal=None, - ctc=None, - mx_args="", - overwrite=True, - verbose=None, -): - """Apply NeuroMag MaxFilter to raw data. - - Needs Maxfilter license, maxfilter has to be in PATH. - - Parameters - ---------- - in_fname : path-like - Input file name. - out_fname : path-like - Output file name. - origin : array-like or str - Head origin in mm. If None it will be estimated from headshape points. - frame : ``'device'`` | ``'head'`` - Coordinate frame for head center. - bad : str, list (or None) - List of static bad channels. Can be a list with channel names, or a - string with channels (names or logical channel numbers). - autobad : str ('on', 'off', 'n') - Sets automated bad channel detection on or off. - skip : str or a list of float-tuples (or None) - Skips raw data sequences, time intervals pairs in s, - e.g.: 0 30 120 150. - force : bool - Ignore program warnings. - st : bool - Apply the time-domain MaxST extension. - st_buflen : float - MaxSt buffer length in s (disabled if st is False). - st_corr : float - MaxSt subspace correlation limit (disabled if st is False). - mv_trans : str (filename or 'default') (or None) - Transforms the data into the coil definitions of in_fname, or into the - default frame (None: don't use option). - mv_comp : bool (or 'inter') - Estimates and compensates head movements in continuous raw data. - mv_headpos : bool - Estimates and stores head position parameters, but does not compensate - movements (disabled if mv_comp is False). - mv_hp : str (or None) - Stores head position data in an ascii file - (disabled if mv_comp is False). - mv_hpistep : float (or None) - Sets head position update interval in ms (disabled if mv_comp is - False). - mv_hpisubt : str ('amp', 'base', 'off') (or None) - Subtracts hpi signals: sine amplitudes, amp + baseline, or switch off - (disabled if mv_comp is False). - mv_hpicons : bool - Check initial consistency isotrak vs hpifit - (disabled if mv_comp is False). - linefreq : int (50, 60) (or None) - Sets the basic line interference frequency (50 or 60 Hz) - (None: do not use line filter). - cal : str - Path to calibration file. - ctc : str - Path to Cross-talk compensation file. - mx_args : str - Additional command line arguments to pass to MaxFilter. - %(overwrite)s - %(verbose)s - - Returns - ------- - origin: str - Head origin in selected coordinate frame. - """ - # check for possible maxfilter bugs - if mv_trans is not None and mv_comp: - _mxwarn("Don't use '-trans' with head-movement compensation " "'-movecomp'") - - if autobad != "off" and (mv_headpos or mv_comp): - _mxwarn( - "Don't use '-autobad' with head-position estimation " - "'-headpos' or movement compensation '-movecomp'" - ) - - if st and autobad != "off": - _mxwarn("Don't use '-autobad' with '-st' option") - - # determine the head origin if necessary - if origin is None: - logger.info("Estimating head origin from headshape points..") - raw = read_raw_fif(in_fname) - r, o_head, o_dev = fit_sphere_to_headshape(raw.info, units="mm") - raw.close() - logger.info("[done]") - if frame == "head": - origin = o_head - elif frame == "device": - origin = o_dev - else: - raise RuntimeError("invalid frame for origin") - - if not isinstance(origin, str): - origin = "%0.1f %0.1f %0.1f" % (origin[0], origin[1], origin[2]) - - # format command - cmd = "maxfilter -f %s -o %s -frame %s -origin %s " % ( - in_fname, - out_fname, - frame, - origin, - ) - - if bad is not None: - # format the channels - if not isinstance(bad, list): - bad = bad.split() - bad = map(str, bad) - bad_logic = [ch[3:] if ch.startswith("MEG") else ch for ch in bad] - bad_str = " ".join(bad_logic) - - cmd += "-bad %s " % bad_str - - cmd += "-autobad %s " % autobad - - if skip is not None: - if isinstance(skip, list): - skip = " ".join(["%0.3f %0.3f" % (s[0], s[1]) for s in skip]) - cmd += "-skip %s " % skip - - if force: - cmd += "-force " - - if st: - cmd += "-st " - cmd += " %d " % st_buflen - cmd += "-corr %0.4f " % st_corr - - if mv_trans is not None: - cmd += "-trans %s " % mv_trans - - if mv_comp: - cmd += "-movecomp " - if mv_comp == "inter": - cmd += " inter " - - if mv_headpos: - cmd += "-headpos " - - if mv_hp is not None: - cmd += "-hp %s " % mv_hp - - if mv_hpisubt is not None: - cmd += "hpisubt %s " % mv_hpisubt - - if mv_hpicons: - cmd += "-hpicons " - - if linefreq is not None: - cmd += "-linefreq %d " % linefreq - - if cal is not None: - cmd += "-cal %s " % cal - - if ctc is not None: - cmd += "-ctc %s " % ctc - - cmd += mx_args - - if overwrite and os.path.exists(out_fname): - os.remove(out_fname) - - logger.info("Running MaxFilter: %s " % cmd) - if os.getenv("_MNE_MAXFILTER_TEST", "") != "true": # fake maxfilter - # OK to `nosec` because it's deprecated / will be removed - st = os.system(cmd) # nosec B605 - else: - print(cmd) # we can check the output - st = 0 - if st != 0: - raise RuntimeError("MaxFilter returned non-zero exit status %d" % st) - logger.info("[done]") - - return origin diff --git a/mne/source_space/_source_space.py b/mne/source_space/_source_space.py index 3cfedb9d7a1..e1d8611354c 100644 --- a/mne/source_space/_source_space.py +++ b/mne/source_space/_source_space.py @@ -35,13 +35,10 @@ write_int_matrix, write_string, ) - -# Remove get_mni_fiducials in 1.6 (deprecated) from .._freesurfer import ( _check_mri, _get_atlas_values, _get_mri_info_data, - get_mni_fiducials, # noqa: F401 get_volume_labels_from_aseg, read_freesurfer_lut, ) diff --git a/mne/tests/test_bem.py b/mne/tests/test_bem.py index 0457444d7b3..d2ee3fe9f93 100644 --- a/mne/tests/test_bem.py +++ b/mne/tests/test_bem.py @@ -37,11 +37,11 @@ _ico_downsample, _order_surfaces, distance_to_bem, + fit_sphere_to_headshape, make_scalp_surfaces, ) from mne.datasets import testing from mne.io import read_info -from mne.preprocessing.maxfilter import fit_sphere_to_headshape from mne.surface import _get_ico_surface, read_surface from mne.transforms import translation from mne.utils import catch_logging, check_version diff --git a/mne/time_frequency/spectrum.py b/mne/time_frequency/spectrum.py index aa459124347..2b31ca46340 100644 --- a/mne/time_frequency/spectrum.py +++ b/mne/time_frequency/spectrum.py @@ -322,12 +322,9 @@ def __init__( # don't allow complex output psd_funcs = dict(welch=psd_array_welch, multitaper=psd_array_multitaper) if method_kw.get("output", "") == "complex": - warn( - f"Complex output support in {type(self).__name__} objects is " - "deprecated and will be removed in version 1.7. If you need complex " - f"output please use mne.time_frequency.{psd_funcs[method].__name__}() " - "instead.", - FutureWarning, + raise ValueError( + f"Complex output is not supported in {type(self).__name__} objects. " + f"Please use mne.time_frequency.{psd_funcs[method].__name__}() instead." ) # triage method and kwargs. partial() doesn't check validity of kwargs, # so we do it manually to save compute time if any are invalid. @@ -368,8 +365,6 @@ def __init__( ) if method_kw.get("average", "") in (None, False): self._dims += ("segment",) - if self._returns_complex_tapers(**method_kw): - self._dims = self._dims[:-1] + ("taper",) + self._dims[-1:] # record data type (for repr and html_repr) self._data_type = ( "Fourier Coefficients" if "taper" in self._dims else "Power Spectrum" @@ -411,8 +406,6 @@ def __setstate__(self, state): # instance type inst_types = dict(Raw=Raw, Epochs=Epochs, Evoked=Evoked, Array=np.ndarray) self._inst_type = inst_types[state["inst_type_str"]] - if "weights" in state and state["weights"] is not None: - self._mt_weights = state["weights"] def __repr__(self): """Build string representation of the Spectrum object.""" @@ -456,23 +449,14 @@ def _check_values(self): s = _pl(bad_value.sum()) warn(f'Zero value in spectrum for channel{s} {", ".join(chs)}', UserWarning) - def _returns_complex_tapers(self, **method_kw): - return method_kw.get("output", "") == "complex" and self.method == "multitaper" - def _compute_spectra(self, data, fmin, fmax, n_jobs, method_kw, verbose): # make the spectra result = self._psd_func( data, self.sfreq, fmin=fmin, fmax=fmax, n_jobs=n_jobs, verbose=verbose ) - # assign ._data (handling unaggregated multitaper output) - if self._returns_complex_tapers(**method_kw): - fourier_coefs, freqs, weights = result - self._data = fourier_coefs - self._mt_weights = weights - else: - psds, freqs = result - self._data = psds - # assign properties (._data already assigned above) + # assign ._data ._freqs, ._shape + psds, freqs = result + self._data = psds self._freqs = freqs # this is *expected* shape, it gets asserted later in _check_values() # (and then deleted afterwards) @@ -481,9 +465,6 @@ def _compute_spectra(self, data, fmin, fmax, n_jobs, method_kw, verbose): if method_kw.get("average", "") in (None, False): n_welch_segments = _compute_n_welch_segments(data.shape[-1], method_kw) self._shape += (n_welch_segments,) - # insert n_tapers - if self._returns_complex_tapers(**method_kw): - self._shape = self._shape[:-1] + (self._mt_weights.size,) + self._shape[-1:] # we don't need these anymore, and they make save/load harder del self._picks del self._psd_func @@ -662,7 +643,6 @@ def plot( # Must nest this _mpl_figure import because of the BACKEND global # stuff from ..viz._mpl_figure import _line_figure, _split_picks_by_type - from .multitaper import _psd_from_mt # arg checking ci = _check_ci(ci) @@ -683,12 +663,8 @@ def plot( (picks_list, units_list, scalings_list, titles_list) = _split_picks_by_type( self, picks, units, scalings, titles ) - # handle unaggregated multitaper - if hasattr(self, "_mt_weights"): - logger.info("Aggregating multitaper estimates before plotting...") - _f = partial(_psd_from_mt, weights=self._mt_weights) # handle unaggregated Welch - elif "segment" in self._dims: + if "segment" in self._dims: logger.info("Aggregating Welch estimates (median) before plotting...") seg_axis = self._dims.index("segment") _f = partial(np.nanmedian, axis=seg_axis) @@ -1059,9 +1035,8 @@ def units(self, latex=False): for that channel type. """ units = _handle_default("si_units", None) - power = not hasattr(self, "_mt_weights") return { - ch_type: _format_units_psd(units[ch_type], power=power, latex=latex) + ch_type: _format_units_psd(units[ch_type], power=True, latex=latex) for ch_type in sorted(self.get_channel_types(unique=True)) } @@ -1447,12 +1422,7 @@ def average(self, method="mean"): f"got a {type(method).__name__} ({method})." ) # averaging unaggregated spectral estimates are not supported - if hasattr(self, "_mt_weights"): - raise NotImplementedError( - "Averaging complex spectra is not supported. Consider " - "averaging the signals before computing the complex spectrum." - ) - elif "segment" in self._dims: + if "segment" in self._dims: raise NotImplementedError( "Averaging individual Welch segments across epochs is not " "supported. Consider averaging the signals before computing " diff --git a/mne/time_frequency/tests/test_spectrum.py b/mne/time_frequency/tests/test_spectrum.py index 58e3309bcc8..653f0ab1411 100644 --- a/mne/time_frequency/tests/test_spectrum.py +++ b/mne/time_frequency/tests/test_spectrum.py @@ -5,12 +5,10 @@ import numpy as np import pytest from matplotlib.colors import same_color -from numpy.testing import assert_allclose, assert_array_equal +from numpy.testing import assert_array_equal -from mne import Annotations, create_info, make_fixed_length_epochs -from mne.io import RawArray +from mne import Annotations from mne.time_frequency import read_spectrum -from mne.time_frequency.multitaper import _psd_from_mt from mne.time_frequency.spectrum import EpochsSpectrumArray, SpectrumArray @@ -22,7 +20,7 @@ def test_compute_psd_errors(raw): raw.compute_psd(foo=None) with pytest.raises(TypeError, match="keyword arguments foo, bar for"): raw.compute_psd(foo=None, bar=None) - with pytest.warns(FutureWarning, match="Complex output support in.*deprecated"): + with pytest.raises(ValueError, match="Complex output is not supported in "): raw.compute_psd(output="complex") @@ -205,78 +203,6 @@ def test_epochs_spectrum_average(epochs_spectrum, method): assert avg_spect._dims == ("channel", "freq") # no 'epoch' -def _agg_helper(df, weights, group_cols): - """Aggregate complex multitaper spectrum after conversion to DataFrame.""" - from pandas import Series - - unagged_columns = df[group_cols].iloc[0].values.tolist() - x_mt = df.drop(columns=group_cols).values[np.newaxis].T - psd = _psd_from_mt(x_mt, weights) - psd = np.atleast_1d(np.squeeze(psd)).tolist() - _df = dict(zip(df.columns, unagged_columns + psd)) - return Series(_df) - - -@pytest.mark.filterwarnings("ignore:Complex output support.*:FutureWarning") -@pytest.mark.parametrize("long_format", (False, True)) -@pytest.mark.parametrize( - "method, output", - [ - ("welch", "complex"), - ("welch", "power"), - ("multitaper", "complex"), - ], -) -def test_unaggregated_spectrum_to_data_frame(raw, long_format, method, output): - """Test converting complex multitaper spectra to data frame.""" - pytest.importorskip("pandas") - from pandas.testing import assert_frame_equal - - from mne.utils.dataframe import _inplace - - # aggregated spectrum → dataframe - orig_df = raw.compute_psd(method=method).to_data_frame(long_format=long_format) - # unaggregated welch or complex multitaper → - # aggregate w/ pandas (to make sure we did reshaping right) - kwargs = dict() - if method == "welch": - kwargs.update(average=False, verbose="error") - spectrum = raw.compute_psd(method=method, output=output, **kwargs) - df = spectrum.to_data_frame(long_format=long_format) - grouping_cols = ["freq"] - drop_cols = ["segment"] if method == "welch" else ["taper"] - if long_format: - grouping_cols.append("channel") - drop_cols.append("ch_type") - orig_df.drop(columns="ch_type", inplace=True) - # only do a couple freq bins, otherwise test takes forever for multitaper - subset = partial(np.isin, test_elements=spectrum.freqs[:2]) - df = df.loc[subset(df["freq"])] - orig_df = orig_df.loc[subset(orig_df["freq"])] - # sort orig_df, because at present we can't actually prevent pandas from - # sorting at the agg step *sigh* - _inplace(orig_df, "sort_values", by=grouping_cols, ignore_index=True) - # aggregate - df = df.drop(columns=drop_cols) - gb = df.groupby(grouping_cols, as_index=False, observed=False) - if method == "welch": - if output == "complex": - - def _fun(x): - return np.nanmean(np.abs(x)) - - agg_df = gb.agg(_fun) - else: - agg_df = gb.mean() # excludes missing values itself - else: - gb = gb[df.columns] # https://github.com/pandas-dev/pandas/pull/52477 - agg_df = gb.apply(_agg_helper, spectrum._mt_weights, grouping_cols) - # even with check_categorical=False, we know that the *data* matches; - # what may differ is the order of the "levels" in the *metadata* for the - # channel name column - assert_frame_equal(agg_df, orig_df, check_categorical=False) - - @pytest.mark.parametrize("inst", ("raw_spectrum", "epochs_spectrum", "evoked")) def test_spectrum_to_data_frame(inst, request, evoked): """Test the to_data_frame method for Spectrum.""" @@ -339,63 +265,6 @@ def test_spectrum_proj(inst, request): assert has_proj == no_proj -@pytest.mark.filterwarnings("ignore:Complex output support.*:FutureWarning") -@pytest.mark.parametrize( - "method, average", - [ - ("welch", False), - ("welch", "mean"), - ("multitaper", False), - ], -) -def test_spectrum_complex(method, average): - """Test output='complex' support.""" - sfreq = 100 - n = 10 * sfreq - freq = 3.0 - phase = np.pi / 4 # should be recoverable - data = np.cos(2 * np.pi * freq * np.arange(n) / sfreq + phase)[np.newaxis] - raw = RawArray(data, create_info(1, sfreq, "eeg")) - epochs = make_fixed_length_epochs(raw, duration=2.0, preload=True) - assert len(epochs) == 5 - assert len(epochs.times) == 2 * sfreq - kwargs = dict(output="complex", method=method) - if method == "welch": - kwargs["n_fft"] = sfreq - want_dims = ("epoch", "channel", "freq") - want_shape = (5, 1, sfreq // 2 + 1) - if not average: - want_dims = want_dims + ("segment",) - want_shape = want_shape + (2,) - kwargs["average"] = average - else: - assert method == "multitaper" - assert not average - want_dims = ("epoch", "channel", "taper", "freq") - want_shape = (5, 1, 7, sfreq + 1) - spectrum = epochs.compute_psd(**kwargs) - idx = np.argmin(np.abs(spectrum.freqs - freq)) - assert spectrum.freqs[idx] == freq - assert spectrum._dims == want_dims - assert spectrum.shape == want_shape - data = spectrum.get_data() - assert data.dtype == np.complex128 - coef = spectrum.get_data(fmin=freq, fmax=freq).mean(0) - if method == "multitaper": - coef = coef[..., 0, :] # first taper - elif not average: - coef = coef.mean(-1) # over segments - coef = coef.item() - assert_allclose(np.angle(coef), phase, rtol=1e-4) - # Now test that it warns appropriately - epochs._data[0, 0, :] = 0 # actually zero for one epoch and ch - with pytest.warns(UserWarning, match="Zero value.*channel 0"): - epochs.compute_psd(**kwargs) - # But not if we mark that channel as bad - epochs.info["bads"] = epochs.ch_names[:1] - epochs.compute_psd(**kwargs) - - def test_spectrum_kwarg_triaging(raw): """Test kwarg triaging in legacy plot_psd() method.""" import matplotlib.pyplot as plt diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 4725e8664aa..69f70786645 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -2183,8 +2183,6 @@ def add_label( borders=False, hemi=None, subdir=None, - *, - reset_camera=None, ): """Add an ROI label to the image. @@ -2216,8 +2214,6 @@ def add_label( label directory rather than in the label directory itself (e.g. for ``$SUBJECTS_DIR/$SUBJECT/label/aparc/lh.cuneus.label`` ``brain.add_label('cuneus', subdir='aparc')``). - reset_camera : bool - Deprecated. Use :meth:`show_view` instead. Notes ----- @@ -2324,12 +2320,6 @@ def add_label( keep_idx = np.unique(keep_idx) show[keep_idx] = 1 scalars *= show - if reset_camera is not None: - warn( - "reset_camera is deprecated and will be removed in 1.7, " - "use show_view instead", - FutureWarning, - ) for _, _, v in self._iter_views(hemi): mesh = self._layered_meshes[hemi] mesh.add_overlay( diff --git a/mne/viz/backends/_abstract.py b/mne/viz/backends/_abstract.py index a8e902aa33b..b28468ebc77 100644 --- a/mne/viz/backends/_abstract.py +++ b/mne/viz/backends/_abstract.py @@ -549,8 +549,6 @@ def set_camera( distance=None, focalpoint=None, roll=None, - *, - reset_camera=None, ): """Configure the camera of the scene. @@ -566,16 +564,9 @@ def set_camera( The focal point of the camera: (x, y, z). roll : float The rotation of the camera along its axis. - reset_camera : bool - Deprecated, used ``distance="auto"`` instead. """ pass - @abstractclassmethod - def reset_camera(self): - """Reset the camera properties.""" - pass - @abstractclassmethod def screenshot(self, mode="rgb", filename=None): """Take a screenshot of the scene. diff --git a/mne/viz/backends/_pyvista.py b/mne/viz/backends/_pyvista.py index b8fca4c995a..c1fb06eb8ff 100644 --- a/mne/viz/backends/_pyvista.py +++ b/mne/viz/backends/_pyvista.py @@ -26,8 +26,6 @@ _check_option, _require_version, _validate_type, - copy_base_doc_to_subclass_doc, - deprecated, warn, ) from ._abstract import Figure3D, _AbstractRenderer @@ -195,7 +193,6 @@ def visible(self, state): self.plotter.render() -@copy_base_doc_to_subclass_doc class _PyVistaRenderer(_AbstractRenderer): """Class managing rendering scene. @@ -843,7 +840,6 @@ def set_camera( *, rigid=None, update=True, - reset_camera=None, ): _set_3d_view( self.figure, @@ -852,18 +848,10 @@ def set_camera( distance=distance, focalpoint=focalpoint, roll=roll, - reset_camera=reset_camera, rigid=rigid, update=update, ) - @deprecated( - "reset_camera is deprecated and will be removed in 1.7, use " - "set_camera(distance='auto') instead" - ) - def reset_camera(self): - self.plotter.reset_camera() - def screenshot(self, mode="rgb", filename=None): return _take_3d_screenshot(figure=self.figure, mode=mode, filename=filename) @@ -1190,7 +1178,6 @@ def _set_3d_view( focalpoint=None, distance=None, roll=None, - reset_camera=None, rigid=None, update=True, ): @@ -1201,14 +1188,6 @@ def _set_3d_view( # camera slides along the vector defined from camera position to focal point until # all of the actors can be seen (quoting PyVista's docs) - if reset_camera is not None: - reset_camera = False - warn( - "reset_camera is deprecated and will be removed in 1.7, use " - "distance='auto' instead", - FutureWarning, - ) - # Figure out our current parameters in the transformed space _, phi, theta = _get_user_camera_direction(figure.plotter, rigid) diff --git a/mne/viz/backends/renderer.py b/mne/viz/backends/renderer.py index 1bb396d165c..2e2ab1e7333 100644 --- a/mne/viz/backends/renderer.py +++ b/mne/viz/backends/renderer.py @@ -264,8 +264,6 @@ def set_3d_view( focalpoint=None, distance=None, roll=None, - *, - reset_camera=None, ): """Configure the view of the given scene. @@ -278,8 +276,6 @@ def set_3d_view( %(focalpoint)s %(distance)s %(roll)s - reset_camera : bool - Deprecated, use ``distance="auto"`` instead. """ backend._set_3d_view( figure=figure, @@ -288,7 +284,6 @@ def set_3d_view( focalpoint=focalpoint, distance=distance, roll=roll, - reset_camera=reset_camera, ) diff --git a/mne/viz/topomap.py b/mne/viz/topomap.py index 0c2f6f273b0..f78d035e0ad 100644 --- a/mne/viz/topomap.py +++ b/mne/viz/topomap.py @@ -376,8 +376,7 @@ def plot_projs_topomap( %(info_not_none)s Must be associated with the channels in the projectors. .. versionchanged:: 0.20 - The positional argument ``layout`` was deprecated and replaced - by ``info``. + The positional argument ``layout`` was replaced by ``info``. %(sensors_topomap)s %(show_names_topomap)s diff --git a/mne/viz/utils.py b/mne/viz/utils.py index 4223bafad6c..15a43916dc4 100644 --- a/mne/viz/utils.py +++ b/mne/viz/utils.py @@ -1784,15 +1784,7 @@ def _get_color_list(annotations=False): from matplotlib import rcParams color_cycle = rcParams.get("axes.prop_cycle") - - if not color_cycle: - # Use deprecated color_cycle to avoid KeyErrors in environments - # with Python 2.7 and Matplotlib < 1.5 - # this will already be a list - colors = rcParams.get("axes.color_cycle") - else: - # we were able to use the prop_cycle. Now just convert to list - colors = color_cycle.by_key()["color"] + colors = color_cycle.by_key()["color"] # If we want annotations, red is reserved ... remove if present. This # checks for the reddish color in MPL dark background style, normal style, From 092cf9002c0510de7d66255a8b11e3a591036f92 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Tue, 5 Dec 2023 10:31:38 -0500 Subject: [PATCH 102/405] MAINT: Make selenium optional and use on CircleCI (#12263) Co-authored-by: Daniel McCloy --- doc/sphinxext/contrib_avatars.py | 44 +++++++++++++++++++------------- tools/circleci_bash_env.sh | 1 + 2 files changed, 27 insertions(+), 18 deletions(-) diff --git a/doc/sphinxext/contrib_avatars.py b/doc/sphinxext/contrib_avatars.py index bbfd17de7d3..5082618a9be 100644 --- a/doc/sphinxext/contrib_avatars.py +++ b/doc/sphinxext/contrib_avatars.py @@ -1,33 +1,41 @@ # License: BSD-3-Clause # Copyright the MNE-Python contributors. +import os from pathlib import Path -from selenium import webdriver -from selenium.webdriver.common.by import By -from selenium.webdriver.support.ui import WebDriverWait -from selenium.common.exceptions import WebDriverException - def generate_contrib_avatars(app, config): """Render a template webpage with avatars generated by JS and a GitHub API call.""" root = Path(app.srcdir) infile = root / "sphinxext" / "_avatar_template.html" outfile = root / "_templates" / "avatars.html" - try: - options = webdriver.ChromeOptions() - options.add_argument("--headless=new") - driver = webdriver.Chrome(options=options) - except WebDriverException: - options = webdriver.FirefoxOptions() - options.add_argument("--headless=new") - driver = webdriver.Firefox(options=options) - driver.get(f"file://{infile}") - wait = WebDriverWait(driver, 20) - wait.until(lambda d: d.find_element(by=By.ID, value="contributor-avatars")) - body = driver.find_element(by=By.TAG_NAME, value="body").get_attribute("innerHTML") + if os.getenv("MNE_ADD_CONTRIBUTOR_IMAGE", "false").lower() != "true": + body = """\ +

Contributor avators will appear here in full doc builds. Set \ +MNE_ADD_CONTRIBUTOR_IMAGE=true in your environment to generate it.

""" + else: + from selenium import webdriver + from selenium.webdriver.common.by import By + from selenium.webdriver.support.ui import WebDriverWait + from selenium.common.exceptions import WebDriverException + + try: + options = webdriver.ChromeOptions() + options.add_argument("--headless=new") + driver = webdriver.Chrome(options=options) + except WebDriverException: + options = webdriver.FirefoxOptions() + options.add_argument("-headless") + driver = webdriver.Firefox(options=options) + driver.get(f"file://{infile}") + wait = WebDriverWait(driver, 20) + wait.until(lambda d: d.find_element(by=By.ID, value="contributor-avatars")) + body = driver.find_element(by=By.TAG_NAME, value="body").get_attribute( + "innerHTML" + ) + driver.quit() with open(outfile, "w") as fid: fid.write(body) - driver.quit() def setup(app): diff --git a/tools/circleci_bash_env.sh b/tools/circleci_bash_env.sh index fb5e471c9fd..55cdb2e157c 100755 --- a/tools/circleci_bash_env.sh +++ b/tools/circleci_bash_env.sh @@ -17,6 +17,7 @@ source tools/get_minimal_commands.sh echo "export MNE_3D_BACKEND=pyvistaqt" >> $BASH_ENV echo "export MNE_BROWSER_BACKEND=qt" >> $BASH_ENV echo "export MNE_BROWSER_PRECOMPUTE=false" >> $BASH_ENV +echo "export MNE_ADD_CONTRIBUTOR_IMAGE=true" >> $BASH_ENV echo "export PATH=~/.local/bin/:$PATH" >> $BASH_ENV echo "export DISPLAY=:99" >> $BASH_ENV echo "source ~/python_env/bin/activate" >> $BASH_ENV From e7dd1588013179013a50d3f6b8e8f9ae0a185783 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20H=C3=B6chenberger?= Date: Tue, 5 Dec 2023 17:47:19 +0100 Subject: [PATCH 103/405] MRG: Use ruff-format instead of Black (#12261) Co-authored-by: Eric Larson Co-authored-by: Daniel McCloy --- .github/workflows/tests.yml | 1 - .pre-commit-config.yaml | 16 +- mne/_fiff/tag.py | 2 +- mne/_fiff/tests/test_constants.py | 12 +- mne/annotations.py | 4 +- mne/beamformer/tests/test_rap_music.py | 4 +- mne/channels/channels.py | 73 +- mne/channels/interpolation.py | 5 +- mne/channels/layout.py | 2 +- mne/commands/mne_freeview_bem_surfaces.py | 3 +- mne/coreg.py | 4 +- mne/datasets/sleep_physionet/_utils.py | 4 +- mne/datasets/sleep_physionet/age.py | 4 +- mne/decoding/base.py | 2 +- mne/decoding/search_light.py | 2 +- mne/decoding/time_frequency.py | 2 +- mne/decoding/transformer.py | 12 +- mne/dipole.py | 12 +- mne/epochs.py | 12 +- mne/event.py | 2 +- mne/evoked.py | 4 +- mne/forward/forward.py | 6 +- mne/forward/tests/test_field_interpolation.py | 10 +- mne/inverse_sparse/mxne_optim.py | 10 +- mne/io/array/array.py | 4 +- mne/io/artemis123/artemis123.py | 2 +- mne/io/base.py | 12 +- mne/io/bti/bti.py | 4 +- mne/io/cnt/cnt.py | 2 +- mne/io/ctf/ctf.py | 2 +- mne/io/ctf/tests/test_ctf.py | 4 +- mne/io/eeglab/eeglab.py | 4 +- mne/io/eeglab/tests/test_eeglab.py | 4 +- mne/io/egi/egi.py | 2 +- mne/io/fiff/raw.py | 2 +- mne/io/fil/fil.py | 4 +- mne/io/hitachi/tests/test_hitachi.py | 8 +- mne/io/kit/kit.py | 6 +- mne/io/nicolet/nicolet.py | 2 +- mne/io/nihon/nihon.py | 3 +- mne/label.py | 4 +- mne/preprocessing/eyetracking/eyetracking.py | 3 +- mne/preprocessing/ica.py | 2 +- mne/preprocessing/interpolate.py | 3 +- .../tests/test_annotate_amplitude.py | 6 +- .../tests/test_eeglab_infomax.py | 4 +- mne/preprocessing/tests/test_maxwell.py | 6 +- mne/source_estimate.py | 12 +- mne/source_space/_source_space.py | 2 +- mne/stats/permutations.py | 2 +- mne/tests/test_annotations.py | 6 +- mne/tests/test_docstring_parameters.py | 4 +- mne/time_frequency/_stockwell.py | 2 +- mne/time_frequency/tfr.py | 3 +- mne/transforms.py | 2 +- mne/utils/_bunch.py | 2 +- mne/utils/_logging.py | 2 +- mne/utils/_testing.py | 4 +- mne/utils/docs.py | 1532 +++++------------ mne/utils/progressbar.py | 2 +- mne/viz/_3d.py | 3 +- mne/viz/backends/_utils.py | 7 +- mne/viz/epochs.py | 6 +- mne/viz/evoked.py | 3 +- mne/viz/topomap.py | 5 +- mne/viz/utils.py | 3 +- pyproject.toml | 4 - tutorials/epochs/20_visualize_epochs.py | 4 +- tutorials/inverse/20_dipole_fit.py | 3 +- tutorials/raw/10_raw_overview.py | 4 +- .../60_cluster_rmANOVA_spatiotemporal.py | 4 +- 71 files changed, 541 insertions(+), 1381 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 14b4ba53f19..1f3f0eb7ea8 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -23,7 +23,6 @@ jobs: - uses: actions/setup-python@v4 with: python-version: '3.11' - - uses: psf/black@stable - uses: pre-commit/action@v3.0.0 bandit: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 25d15b2157d..29c11f935ec 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,29 +1,29 @@ repos: - - repo: https://github.com/psf/black - rev: 23.11.0 - hooks: - - id: black - args: [--quiet] - # Ruff mne - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.1.6 hooks: - id: ruff - name: ruff mne + name: ruff lint mne args: ["--fix"] files: ^mne/ + - id: ruff-format + name: ruff format mne + files: ^mne/ # Ruff tutorials and examples - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.1.6 hooks: - id: ruff - name: ruff tutorials and examples + name: ruff lint tutorials and examples # D103: missing docstring in public function # D400: docstring first line must end with period args: ["--ignore=D103,D400", "--fix"] files: ^tutorials/|^examples/ + - id: ruff-format + name: ruff format tutorials and examples + files: ^tutorials/|^examples/ # Codespell - repo: https://github.com/codespell-project/codespell diff --git a/mne/_fiff/tag.py b/mne/_fiff/tag.py index 1b87d828619..81ed12baf6f 100644 --- a/mne/_fiff/tag.py +++ b/mne/_fiff/tag.py @@ -45,7 +45,7 @@ class Tag: Position of Tag is the original file. """ - def __init__(self, kind, type_, size, next, pos=None): # noqa: D102 + def __init__(self, kind, type_, size, next, pos=None): self.kind = int(kind) self.type = int(type_) self.size = int(size) diff --git a/mne/_fiff/tests/test_constants.py b/mne/_fiff/tests/test_constants.py index 3fc33513635..8f65e2609d5 100644 --- a/mne/_fiff/tests/test_constants.py +++ b/mne/_fiff/tests/test_constants.py @@ -385,16 +385,16 @@ def test_constants(tmp_path): 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]) - assert ( - len(bad_list) == 0 - ), "\nIn fiff-constants, missing from coil_def:\n" + "\n".join(bad_list) + 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) - assert ( - len(bad_list) == 0 - ), "In coil_def, missing from fiff-constants:\n" + "\n".join(bad_list) + assert len(bad_list) == 0, ( + "In coil_def, missing from fiff-constants:\n" + "\n".join(bad_list) + ) @pytest.mark.parametrize( diff --git a/mne/annotations.py b/mne/annotations.py index be62dac9dba..20ee351e7fa 100644 --- a/mne/annotations.py +++ b/mne/annotations.py @@ -276,9 +276,7 @@ class Annotations: :meth:`Raw.save() ` notes for details. """ # noqa: E501 - def __init__( - self, onset, duration, description, orig_time=None, ch_names=None - ): # noqa: D102 + def __init__(self, onset, duration, description, orig_time=None, ch_names=None): self._orig_time = _handle_meas_date(orig_time) self.onset, self.duration, self.description, self.ch_names = _check_o_d_s_c( onset, duration, description, ch_names diff --git a/mne/beamformer/tests/test_rap_music.py b/mne/beamformer/tests/test_rap_music.py index c98c83a7722..de6f047def8 100644 --- a/mne/beamformer/tests/test_rap_music.py +++ b/mne/beamformer/tests/test_rap_music.py @@ -50,9 +50,7 @@ def simu_data(evoked, forward, noise_cov, n_dipoles, times, nave=1): # Generate the two dipoles data mu, sigma = 0.1, 0.005 s1 = ( - 1 - / (sigma * np.sqrt(2 * np.pi)) - * np.exp(-((times - mu) ** 2) / (2 * sigma**2)) + 1 / (sigma * np.sqrt(2 * np.pi)) * np.exp(-((times - mu) ** 2) / (2 * sigma**2)) ) mu, sigma = 0.075, 0.008 diff --git a/mne/channels/channels.py b/mne/channels/channels.py index bc0f52cb56c..325be7350a6 100644 --- a/mne/channels/channels.py +++ b/mne/channels/channels.py @@ -585,14 +585,13 @@ def drop_channels(self, ch_names, on_missing="raise"): all_str = all([isinstance(ch, str) for ch in ch_names]) except TypeError: raise ValueError( - "'ch_names' must be iterable, got " - "type {} ({}).".format(type(ch_names), ch_names) + f"'ch_names' must be iterable, got type {type(ch_names)} ({ch_names})." ) if not all_str: raise ValueError( "Each element in 'ch_names' must be str, got " - "{}.".format([type(ch) for ch in ch_names]) + f"{[type(ch) for ch in ch_names]}." ) missing = [ch for ch in ch_names if ch not in self.ch_names] @@ -1057,9 +1056,7 @@ class _BuiltinChannelAdjacency: name="bti248grad", description="BTI 248 gradiometer system", fname="bti248grad_neighb.mat", - source_url=_ft_neighbor_url_t.substitute( - fname="bti248grad_neighb.mat" - ), # noqa: E501 + source_url=_ft_neighbor_url_t.substitute(fname="bti248grad_neighb.mat"), ), _BuiltinChannelAdjacency( name="ctf64", @@ -1083,25 +1080,19 @@ class _BuiltinChannelAdjacency: name="easycap32ch-avg", description="", fname="easycap32ch-avg_neighb.mat", - source_url=_ft_neighbor_url_t.substitute( - fname="easycap32ch-avg_neighb.mat" - ), # noqa: E501 + source_url=_ft_neighbor_url_t.substitute(fname="easycap32ch-avg_neighb.mat"), ), _BuiltinChannelAdjacency( name="easycap64ch-avg", description="", fname="easycap64ch-avg_neighb.mat", - source_url=_ft_neighbor_url_t.substitute( - fname="easycap64ch-avg_neighb.mat" - ), # noqa: E501 + source_url=_ft_neighbor_url_t.substitute(fname="easycap64ch-avg_neighb.mat"), ), _BuiltinChannelAdjacency( name="easycap128ch-avg", description="", fname="easycap128ch-avg_neighb.mat", - source_url=_ft_neighbor_url_t.substitute( - fname="easycap128ch-avg_neighb.mat" - ), # noqa: E501 + source_url=_ft_neighbor_url_t.substitute(fname="easycap128ch-avg_neighb.mat"), ), _BuiltinChannelAdjacency( name="easycapM1", @@ -1113,25 +1104,19 @@ class _BuiltinChannelAdjacency: name="easycapM11", description="Easycap M11", fname="easycapM11_neighb.mat", - source_url=_ft_neighbor_url_t.substitute( - fname="easycapM11_neighb.mat" - ), # noqa: E501 + source_url=_ft_neighbor_url_t.substitute(fname="easycapM11_neighb.mat"), # noqa: E501 ), _BuiltinChannelAdjacency( name="easycapM14", description="Easycap M14", fname="easycapM14_neighb.mat", - source_url=_ft_neighbor_url_t.substitute( - fname="easycapM14_neighb.mat" - ), # noqa: E501 + source_url=_ft_neighbor_url_t.substitute(fname="easycapM14_neighb.mat"), # noqa: E501 ), _BuiltinChannelAdjacency( name="easycapM15", description="Easycap M15", fname="easycapM15_neighb.mat", - source_url=_ft_neighbor_url_t.substitute( - fname="easycapM15_neighb.mat" - ), # noqa: E501 + source_url=_ft_neighbor_url_t.substitute(fname="easycapM15_neighb.mat"), # noqa: E501 ), _BuiltinChannelAdjacency( name="KIT-157", @@ -1179,49 +1164,37 @@ class _BuiltinChannelAdjacency: name="neuromag306mag", description="Neuromag306, only magnetometers", fname="neuromag306mag_neighb.mat", - source_url=_ft_neighbor_url_t.substitute( - fname="neuromag306mag_neighb.mat" - ), # noqa: E501 + source_url=_ft_neighbor_url_t.substitute(fname="neuromag306mag_neighb.mat"), # noqa: E501 ), _BuiltinChannelAdjacency( name="neuromag306planar", description="Neuromag306, only planar gradiometers", fname="neuromag306planar_neighb.mat", - source_url=_ft_neighbor_url_t.substitute( - fname="neuromag306planar_neighb.mat" - ), # noqa: E501 + source_url=_ft_neighbor_url_t.substitute(fname="neuromag306planar_neighb.mat"), # noqa: E501 ), _BuiltinChannelAdjacency( name="neuromag122cmb", description="Neuromag122, only combined planar gradiometers", fname="neuromag122cmb_neighb.mat", - source_url=_ft_neighbor_url_t.substitute( - fname="neuromag122cmb_neighb.mat" - ), # noqa: E501 + source_url=_ft_neighbor_url_t.substitute(fname="neuromag122cmb_neighb.mat"), # noqa: E501 ), _BuiltinChannelAdjacency( name="neuromag306cmb", description="Neuromag306, only combined planar gradiometers", fname="neuromag306cmb_neighb.mat", - source_url=_ft_neighbor_url_t.substitute( - fname="neuromag306cmb_neighb.mat" - ), # noqa: E501 + source_url=_ft_neighbor_url_t.substitute(fname="neuromag306cmb_neighb.mat"), # noqa: E501 ), _BuiltinChannelAdjacency( name="ecog256", description="ECOG 256channels, average referenced", fname="ecog256_neighb.mat", - source_url=_ft_neighbor_url_t.substitute( - fname="ecog256_neighb.mat" - ), # noqa: E501 + source_url=_ft_neighbor_url_t.substitute(fname="ecog256_neighb.mat"), # noqa: E501 ), _BuiltinChannelAdjacency( name="ecog256bipolar", description="ECOG 256channels, bipolar referenced", fname="ecog256bipolar_neighb.mat", - source_url=_ft_neighbor_url_t.substitute( - fname="ecog256bipolar_neighb.mat" - ), # noqa: E501 + source_url=_ft_neighbor_url_t.substitute(fname="ecog256bipolar_neighb.mat"), # noqa: E501 ), _BuiltinChannelAdjacency( name="eeg1010_neighb", @@ -1263,33 +1236,25 @@ class _BuiltinChannelAdjacency: name="language29ch-avg", description="MPI for Psycholinguistic: Averaged 29-channel cap", fname="language29ch-avg_neighb.mat", - source_url=_ft_neighbor_url_t.substitute( - fname="language29ch-avg_neighb.mat" - ), # noqa: E501 + source_url=_ft_neighbor_url_t.substitute(fname="language29ch-avg_neighb.mat"), # noqa: E501 ), _BuiltinChannelAdjacency( name="mpi_59_channels", description="MPI for Psycholinguistic: 59-channel cap", fname="mpi_59_channels_neighb.mat", - source_url=_ft_neighbor_url_t.substitute( - fname="mpi_59_channels_neighb.mat" - ), # noqa: E501 + source_url=_ft_neighbor_url_t.substitute(fname="mpi_59_channels_neighb.mat"), # noqa: E501 ), _BuiltinChannelAdjacency( name="yokogawa160", description="", fname="yokogawa160_neighb.mat", - source_url=_ft_neighbor_url_t.substitute( - fname="yokogawa160_neighb.mat" - ), # noqa: E501 + source_url=_ft_neighbor_url_t.substitute(fname="yokogawa160_neighb.mat"), # noqa: E501 ), _BuiltinChannelAdjacency( name="yokogawa440", description="", fname="yokogawa440_neighb.mat", - source_url=_ft_neighbor_url_t.substitute( - fname="yokogawa440_neighb.mat" - ), # noqa: E501 + source_url=_ft_neighbor_url_t.substitute(fname="yokogawa440_neighb.mat"), # noqa: E501 ), ] diff --git a/mne/channels/interpolation.py b/mne/channels/interpolation.py index 77e660901a9..807639b8bcf 100644 --- a/mne/channels/interpolation.py +++ b/mne/channels/interpolation.py @@ -165,10 +165,7 @@ def _interpolate_bads_eeg(inst, origin, exclude=None, verbose=None): pos_good = pos[goods_idx_pos] - origin pos_bad = pos[bads_idx_pos] - origin - logger.info( - "Computing interpolation matrix from {} sensor " - "positions".format(len(pos_good)) - ) + logger.info(f"Computing interpolation matrix from {len(pos_good)} sensor positions") interpolation = _make_interpolation_matrix(pos_good, pos_bad) logger.info("Interpolating {} sensors".format(len(pos_bad))) diff --git a/mne/channels/layout.py b/mne/channels/layout.py index a0f12cc594f..043bb9c33b7 100644 --- a/mne/channels/layout.py +++ b/mne/channels/layout.py @@ -58,7 +58,7 @@ class Layout: The type of Layout (e.g. 'Vectorview-all'). """ - def __init__(self, box, pos, names, ids, kind): # noqa: D102 + def __init__(self, box, pos, names, ids, kind): self.box = box self.pos = pos self.names = names diff --git a/mne/commands/mne_freeview_bem_surfaces.py b/mne/commands/mne_freeview_bem_surfaces.py index 7f1c6491ba1..504ca3378bf 100644 --- a/mne/commands/mne_freeview_bem_surfaces.py +++ b/mne/commands/mne_freeview_bem_surfaces.py @@ -41,8 +41,7 @@ def freeview_bem_surfaces(subject, subjects_dir, method): if not op.isdir(subject_dir): raise ValueError( - "Wrong path: '{}'. Check subjects-dir or" - "subject argument.".format(subject_dir) + f"Wrong path: '{subject_dir}'. Check subjects-dir or subject argument." ) env = os.environ.copy() diff --git a/mne/coreg.py b/mne/coreg.py index fe6895270b7..32f2cb6d614 100644 --- a/mne/coreg.py +++ b/mne/coreg.py @@ -422,8 +422,8 @@ def fit_matched_points( tgt_pts = np.atleast_2d(tgt_pts) if src_pts.shape != tgt_pts.shape: raise ValueError( - "src_pts and tgt_pts must have same shape (got " - "{}, {})".format(src_pts.shape, tgt_pts.shape) + "src_pts and tgt_pts must have same shape " + f"(got {src_pts.shape}, {tgt_pts.shape})" ) if weights is not None: weights = np.asarray(weights, src_pts.dtype) diff --git a/mne/datasets/sleep_physionet/_utils.py b/mne/datasets/sleep_physionet/_utils.py index db9982e8c71..8e4506a1be5 100644 --- a/mne/datasets/sleep_physionet/_utils.py +++ b/mne/datasets/sleep_physionet/_utils.py @@ -20,9 +20,7 @@ ) TEMAZEPAM_RECORDS_URL_SHA1 = "f52fffe5c18826a2bd4c5d5cb375bb4a9008c885" -AGE_RECORDS_URL = ( - "https://physionet.org/physiobank/database/sleep-edfx/SC-subjects.xls" # noqa: E501 -) +AGE_RECORDS_URL = "https://physionet.org/physiobank/database/sleep-edfx/SC-subjects.xls" AGE_RECORDS_URL_SHA1 = "0ba6650892c5d33a8e2b3f62ce1cc9f30438c54f" sha1sums_fname = op.join(op.dirname(__file__), "SHA1SUMS") diff --git a/mne/datasets/sleep_physionet/age.py b/mne/datasets/sleep_physionet/age.py index 29afe9d9562..f947874aa0d 100644 --- a/mne/datasets/sleep_physionet/age.py +++ b/mne/datasets/sleep_physionet/age.py @@ -21,9 +21,7 @@ data_path = _data_path # expose _data_path(..) as data_path(..) -BASE_URL = ( - "https://physionet.org/physiobank/database/sleep-edfx/sleep-cassette/" # noqa: E501 -) +BASE_URL = "https://physionet.org/physiobank/database/sleep-edfx/sleep-cassette/" @verbose diff --git a/mne/decoding/base.py b/mne/decoding/base.py index 08a0d65e951..8caea981194 100644 --- a/mne/decoding/base.py +++ b/mne/decoding/base.py @@ -64,7 +64,7 @@ class LinearModel(BaseEstimator): "classes_", ) - def __init__(self, model=None): # noqa: D102 + def __init__(self, model=None): if model is None: from sklearn.linear_model import LogisticRegression diff --git a/mne/decoding/search_light.py b/mne/decoding/search_light.py index 06b7b010651..873efe89465 100644 --- a/mne/decoding/search_light.py +++ b/mne/decoding/search_light.py @@ -46,7 +46,7 @@ def __init__( position=0, allow_2d=False, verbose=None, - ): # noqa: D102 + ): _check_estimator(base_estimator) self.base_estimator = base_estimator self.n_jobs = n_jobs diff --git a/mne/decoding/time_frequency.py b/mne/decoding/time_frequency.py index e085e9e2706..bd0076d0355 100644 --- a/mne/decoding/time_frequency.py +++ b/mne/decoding/time_frequency.py @@ -74,7 +74,7 @@ def __init__( output="complex", n_jobs=1, verbose=None, - ): # noqa: D102 + ): """Init TimeFrequency transformer.""" # Check non-average output output = _check_option("output", output, ["complex", "power", "phase"]) diff --git a/mne/decoding/transformer.py b/mne/decoding/transformer.py index 44930417e89..184bfca8f53 100644 --- a/mne/decoding/transformer.py +++ b/mne/decoding/transformer.py @@ -107,9 +107,7 @@ class Scaler(TransformerMixin, BaseEstimator): if ``scalings`` is a dict or None). """ - def __init__( - self, info=None, scalings=None, with_mean=True, with_std=True - ): # noqa: D102 + def __init__(self, info=None, scalings=None, with_mean=True, with_std=True): self.info = info self.with_mean = with_mean self.with_std = with_std @@ -384,7 +382,7 @@ def __init__( normalization="length", *, verbose=None, - ): # noqa: D102 + ): self.sfreq = sfreq self.fmin = fmin self.fmax = fmax @@ -512,7 +510,7 @@ def __init__( fir_design="firwin", *, verbose=None, - ): # noqa: D102 + ): self.info = info self.l_freq = l_freq self.h_freq = h_freq @@ -625,7 +623,7 @@ class UnsupervisedSpatialFilter(TransformerMixin, BaseEstimator): (e.g. epochs). """ - def __init__(self, estimator, average=False): # noqa: D102 + def __init__(self, estimator, average=False): # XXX: Use _check_estimator #3381 for attr in ("fit", "transform", "fit_transform"): if not hasattr(estimator, attr): @@ -838,7 +836,7 @@ def __init__( fir_design="firwin", *, verbose=None, - ): # noqa: D102 + ): self.l_freq = l_freq self.h_freq = h_freq self.sfreq = sfreq diff --git a/mne/dipole.py b/mne/dipole.py index 59531463da8..42e27438b4a 100644 --- a/mne/dipole.py +++ b/mne/dipole.py @@ -130,7 +130,7 @@ def __init__( nfree=None, *, verbose=None, - ): # noqa: D102 + ): self._set_times(np.array(times)) self.pos = np.array(pos) self.amplitude = np.array(amplitude) @@ -481,7 +481,7 @@ class DipoleFixed(ExtendedTimeMixin): @verbose def __init__( self, info, data, times, nave, aspect_kind, comment="", *, verbose=None - ): # noqa: D102 + ): self.info = info self.nave = nave self._aspect_kind = aspect_kind @@ -654,7 +654,9 @@ def _read_dipole_text(fname): def_line, ) fields = re.sub( - r"\((.*?)\)", lambda match: "/" + match.group(1), fields # "Q(nAm)", etc. + r"\((.*?)\)", + lambda match: "/" + match.group(1), + fields, # "Q(nAm)", etc. ) fields = re.sub( "(begin|end) ", # "begin" and "end" with no units @@ -1522,9 +1524,7 @@ def fit_dipole( # Use the minimum distance to the MEG sensors as the radius then R = np.dot( np.linalg.inv(info["dev_head_t"]["trans"]), np.hstack([r0, [1.0]]) - )[ - :3 - ] # r0 -> device + )[:3] # r0 -> device R = R - [ info["chs"][pick]["loc"][:3] for pick in pick_types(info, meg=True, exclude=[]) diff --git a/mne/epochs.py b/mne/epochs.py index 864f4021b42..915670af516 100644 --- a/mne/epochs.py +++ b/mne/epochs.py @@ -465,7 +465,7 @@ def __init__( raw_sfreq=None, annotations=None, verbose=None, - ): # noqa: D102 + ): if events is not None: # RtEpochs can have events=None events = _ensure_events(events) # Allow reading empty epochs (ToDo: Maybe not anymore in the future) @@ -3231,7 +3231,7 @@ def __init__( metadata=None, event_repeated="error", verbose=None, - ): # noqa: D102 + ): from .io import BaseRaw if not isinstance(raw, BaseRaw): @@ -3403,7 +3403,7 @@ def __init__( drop_log=None, raw_sfreq=None, verbose=None, - ): # noqa: D102 + ): dtype = np.complex128 if np.any(np.iscomplex(data)) else np.float64 data = np.asanyarray(data, dtype=dtype) if data.ndim != 3: @@ -3873,9 +3873,7 @@ def read_epochs(fname, proj=True, preload=True, verbose=None): class _RawContainer: """Helper for a raw data container.""" - def __init__( - self, fid, data_tag, event_samps, epoch_shape, cals, fmt - ): # noqa: D102 + def __init__(self, fid, data_tag, event_samps, epoch_shape, cals, fmt): self.fid = fid self.data_tag = data_tag self.event_samps = event_samps @@ -3909,7 +3907,7 @@ class EpochsFIF(BaseEpochs): """ @verbose - def __init__(self, fname, proj=True, preload=True, verbose=None): # noqa: D102 + def __init__(self, fname, proj=True, preload=True, verbose=None): from .io.base import _get_fname_rep if _path_like(fname): diff --git a/mne/event.py b/mne/event.py index 211ed4e5d5d..fca229f2884 100644 --- a/mne/event.py +++ b/mne/event.py @@ -1144,7 +1144,7 @@ class AcqParserFIF: "OldMask", ) - def __init__(self, info): # noqa: D102 + def __init__(self, info): acq_pars = info["acq_pars"] if not acq_pars: raise ValueError("No acquisition parameters") diff --git a/mne/evoked.py b/mne/evoked.py index 31083795507..03923533fbb 100644 --- a/mne/evoked.py +++ b/mne/evoked.py @@ -174,7 +174,7 @@ def __init__( allow_maxshield=False, *, verbose=None, - ): # noqa: D102 + ): _validate_type(proj, bool, "'proj'") # Read the requested data fname = str(_check_fname(fname=fname, must_exist=True, overwrite="read")) @@ -1316,7 +1316,7 @@ def __init__( baseline=None, *, verbose=None, - ): # noqa: D102 + ): dtype = np.complex128 if np.iscomplexobj(data) else np.float64 data = np.asanyarray(data, dtype=dtype) diff --git a/mne/forward/forward.py b/mne/forward/forward.py index 9531445edd1..dc39a58bd8f 100644 --- a/mne/forward/forward.py +++ b/mne/forward/forward.py @@ -1456,8 +1456,10 @@ def compute_depth_prior( # d[k] = linalg.svdvals(x)[0] G.shape = (G.shape[0], -1, 3) d = np.linalg.norm( - np.einsum("svj,svk->vjk", G, G), ord=2, axis=(1, 2) # vector dot prods - ) # ord=2 spectral (largest s.v.) + np.einsum("svj,svk->vjk", G, G), # vector dot prods + ord=2, # ord=2 spectral (largest s.v.) + axis=(1, 2), + ) G.shape = (G.shape[0], -1) # XXX Currently the fwd solns never have "patch_areas" defined diff --git a/mne/forward/tests/test_field_interpolation.py b/mne/forward/tests/test_field_interpolation.py index f19b844d46c..4f09a90df73 100644 --- a/mne/forward/tests/test_field_interpolation.py +++ b/mne/forward/tests/test_field_interpolation.py @@ -237,10 +237,16 @@ def test_make_field_map_meeg(): assert_allclose(map_["data"].min(), min_, rtol=5e-2) # calculated from correct looking mapping on 2015/12/26 assert_allclose( - np.sqrt(np.sum(maps[0]["data"] ** 2)), 19.0903, atol=1e-3, rtol=1e-3 # 16.6088, + np.sqrt(np.sum(maps[0]["data"] ** 2)), + 19.0903, + atol=1e-3, + rtol=1e-3, ) assert_allclose( - np.sqrt(np.sum(maps[1]["data"] ** 2)), 19.4748, atol=1e-3, rtol=1e-3 # 20.1245, + np.sqrt(np.sum(maps[1]["data"] ** 2)), + 19.4748, + atol=1e-3, + rtol=1e-3, ) diff --git a/mne/inverse_sparse/mxne_optim.py b/mne/inverse_sparse/mxne_optim.py index b70476991a2..5d785f5eec5 100644 --- a/mne/inverse_sparse/mxne_optim.py +++ b/mne/inverse_sparse/mxne_optim.py @@ -778,7 +778,7 @@ def safe_max_abs_diff(A, ia, B, ib): class _Phi: """Have phi stft as callable w/o using a lambda that does not pickle.""" - def __init__(self, wsize, tstep, n_coefs, n_times): # noqa: D102 + def __init__(self, wsize, tstep, n_coefs, n_times): self.wsize = np.atleast_1d(wsize) self.tstep = np.atleast_1d(tstep) self.n_coefs = np.atleast_1d(n_coefs) @@ -819,7 +819,7 @@ def norm(self, z, ord=2): class _PhiT: """Have phi.T istft as callable w/o using a lambda that does not pickle.""" - def __init__(self, tstep, n_freqs, n_steps, n_times): # noqa: D102 + def __init__(self, tstep, n_freqs, n_steps, n_times): self.tstep = tstep self.n_freqs = n_freqs self.n_steps = n_steps @@ -977,9 +977,9 @@ def norm_epsilon(Y, l1_ratio, phi, w_space=1.0, w_time=None): p_sum_w2 = np.cumsum(w_time**2) p_sum_Yw = np.cumsum(Y * w_time) upper = p_sum_Y2 / (Y / w_time) ** 2 - 2.0 * p_sum_Yw / (Y / w_time) + p_sum_w2 - upper_greater = np.where( - upper > w_space**2 * (1.0 - l1_ratio) ** 2 / l1_ratio**2 - )[0] + upper_greater = np.where(upper > w_space**2 * (1.0 - l1_ratio) ** 2 / l1_ratio**2)[ + 0 + ] i0 = upper_greater[0] - 1 if upper_greater.size else K - 1 diff --git a/mne/io/array/array.py b/mne/io/array/array.py index 456bd763015..a0df061821f 100644 --- a/mne/io/array/array.py +++ b/mne/io/array/array.py @@ -52,9 +52,7 @@ class RawArray(BaseRaw): """ @verbose - def __init__( - self, data, info, first_samp=0, copy="auto", verbose=None - ): # noqa: D102 + def __init__(self, data, info, first_samp=0, copy="auto", verbose=None): _validate_type(info, "info", "info") _check_option("copy", copy, ("data", "info", "both", "auto", None)) dtype = np.complex128 if np.any(np.iscomplex(data)) else np.float64 diff --git a/mne/io/artemis123/artemis123.py b/mne/io/artemis123/artemis123.py index 3cdedb3770d..fb7b33e5b6c 100644 --- a/mne/io/artemis123/artemis123.py +++ b/mne/io/artemis123/artemis123.py @@ -340,7 +340,7 @@ def __init__( verbose=None, pos_fname=None, add_head_trans=True, - ): # noqa: D102 + ): from ...chpi import ( _fit_coil_order_dev_head_trans, compute_chpi_amplitudes, diff --git a/mne/io/base.py b/mne/io/base.py index fd8dde30258..95ba7038865 100644 --- a/mne/io/base.py +++ b/mne/io/base.py @@ -203,7 +203,7 @@ def __init__( orig_units=None, *, verbose=None, - ): # noqa: D102 + ): # wait until the end to preload data, but triage here if isinstance(preload, np.ndarray): # some functions (e.g., filtering) only work w/64-bit data @@ -265,8 +265,7 @@ def __init__( if orig_units: if not isinstance(orig_units, dict): raise ValueError( - "orig_units must be of type dict, but got " - " {}".format(type(orig_units)) + f"orig_units must be of type dict, but got {type(orig_units)}" ) # original units need to be truncated to 15 chars or renamed @@ -291,8 +290,7 @@ def __init__( if not all(ch_correspond): ch_without_orig_unit = ch_names[ch_correspond.index(False)] raise ValueError( - "Channel {} has no associated original " - "unit.".format(ch_without_orig_unit) + f"Channel {ch_without_orig_unit} has no associated original unit." ) # Final check of orig_units, editing a unit if it is not a valid @@ -1127,7 +1125,7 @@ def filter( skip_by_annotation=("edge", "bad_acq_skip"), pad="reflect_limited", verbose=None, - ): # noqa: D102 + ): return super().filter( l_freq, h_freq, @@ -2522,7 +2520,7 @@ def _read_segment_file(self, data, idx, fi, start, stop, cals, mult): class _RawShell: """Create a temporary raw object.""" - def __init__(self): # noqa: D102 + def __init__(self): self.first_samp = None self.last_samp = None self._first_time = None diff --git a/mne/io/bti/bti.py b/mne/io/bti/bti.py index 190625f8ee0..99a77cd2b8c 100644 --- a/mne/io/bti/bti.py +++ b/mne/io/bti/bti.py @@ -72,7 +72,7 @@ def _instantiate_default_info_chs(): class _bytes_io_mock_context: """Make a context for BytesIO.""" - def __init__(self, target): # noqa: D102 + def __init__(self, target): self.target = target def __enter__(self): # noqa: D105 @@ -1077,7 +1077,7 @@ def __init__( eog_ch=("E63", "E64"), preload=False, verbose=None, - ): # noqa: D102 + ): _validate_type(pdf_fname, ("path-like", BytesIO), "pdf_fname") info, bti_info = _get_bti_info( pdf_fname=pdf_fname, diff --git a/mne/io/cnt/cnt.py b/mne/io/cnt/cnt.py index a242e85952b..496ed91cd38 100644 --- a/mne/io/cnt/cnt.py +++ b/mne/io/cnt/cnt.py @@ -508,7 +508,7 @@ def __init__( header="auto", preload=False, verbose=None, - ): # noqa: D102 + ): _check_option("date_format", date_format, ["mm/dd/yy", "dd/mm/yy"]) if date_format == "dd/mm/yy": _date_format = "%d/%m/%y %H:%M:%S" diff --git a/mne/io/ctf/ctf.py b/mne/io/ctf/ctf.py index feb5a04dda2..1d4970624bd 100644 --- a/mne/io/ctf/ctf.py +++ b/mne/io/ctf/ctf.py @@ -111,7 +111,7 @@ def __init__( preload=False, verbose=None, clean_names=False, - ): # noqa: D102 + ): # adapted from mne_ctf2fiff.c directory = str( _check_fname(directory, "read", True, "directory", need_dir=True) diff --git a/mne/io/ctf/tests/test_ctf.py b/mne/io/ctf/tests/test_ctf.py index f5340421a70..20fdf2e0127 100644 --- a/mne/io/ctf/tests/test_ctf.py +++ b/mne/io/ctf/tests/test_ctf.py @@ -92,9 +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("%0.5f" % x 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/eeglab/eeglab.py b/mne/io/eeglab/eeglab.py index 413a8ae4bfc..f4beee56119 100644 --- a/mne/io/eeglab/eeglab.py +++ b/mne/io/eeglab/eeglab.py @@ -449,7 +449,7 @@ def __init__( uint16_codec=None, montage_units="auto", verbose=None, - ): # noqa: D102 + ): input_fname = str(_check_fname(input_fname, "read", True, "input_fname")) eeg = _check_load_mat(input_fname, uint16_codec) if eeg.trials != 1: @@ -602,7 +602,7 @@ def __init__( uint16_codec=None, montage_units="auto", verbose=None, - ): # noqa: D102 + ): input_fname = str( _check_fname(fname=input_fname, must_exist=True, overwrite="read") ) diff --git a/mne/io/eeglab/tests/test_eeglab.py b/mne/io/eeglab/tests/test_eeglab.py index 7d78f95ef6a..ce34a186910 100644 --- a/mne/io/eeglab/tests/test_eeglab.py +++ b/mne/io/eeglab/tests/test_eeglab.py @@ -568,9 +568,7 @@ def test_position_information(three_chanpos_fname): input_fname=three_chanpos_fname, preload=True, montage_units="cm", - ).set_montage( - None - ) # Flush the montage builtin within input_fname + ).set_montage(None) # Flush the montage builtin within input_fname _assert_array_allclose_nan( np.array([ch["loc"] for ch in raw.info["chs"]]), EXPECTED_LOCATIONS_FROM_MONTAGE diff --git a/mne/io/egi/egi.py b/mne/io/egi/egi.py index 0b62d7b6389..32cb71db28f 100644 --- a/mne/io/egi/egi.py +++ b/mne/io/egi/egi.py @@ -193,7 +193,7 @@ def __init__( preload=False, channel_naming="E%d", verbose=None, - ): # noqa: D102 + ): input_fname = str(_check_fname(input_fname, "read", True, "input_fname")) if eog is None: eog = [] diff --git a/mne/io/fiff/raw.py b/mne/io/fiff/raw.py index d81fd99c556..f4053f88b37 100644 --- a/mne/io/fiff/raw.py +++ b/mne/io/fiff/raw.py @@ -97,7 +97,7 @@ def __init__( preload=False, on_split_missing="raise", verbose=None, - ): # noqa: D102 + ): raws = [] do_check_ext = not _file_like(fname) next_fname = fname diff --git a/mne/io/fil/fil.py b/mne/io/fil/fil.py index 08b7778398a..ea990b741de 100644 --- a/mne/io/fil/fil.py +++ b/mne/io/fil/fil.py @@ -311,8 +311,8 @@ def _from_tsv(fname, dtypes=None): dtypes = [dtypes] * info.shape[1] if not len(dtypes) == info.shape[1]: raise ValueError( - "dtypes length mismatch. Provided: {0}, " - "Expected: {1}".format(len(dtypes), info.shape[1]) + f"dtypes length mismatch. Provided: {len(dtypes)}, " + f"Expected: {info.shape[1]}" ) for i, name in enumerate(column_names): data_dict[name] = info[:, i].astype(dtypes[i]).tolist() diff --git a/mne/io/hitachi/tests/test_hitachi.py b/mne/io/hitachi/tests/test_hitachi.py index edad56dc75e..300af7cf5e8 100644 --- a/mne/io/hitachi/tests/test_hitachi.py +++ b/mne/io/hitachi/tests/test_hitachi.py @@ -22,9 +22,7 @@ ) CONTENTS = dict() -CONTENTS[ - "1.18" -] = b"""\ +CONTENTS["1.18"] = b"""\ Header,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, File Version,1.18,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, Patient Information,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, @@ -129,9 +127,7 @@ """ # noqa: E501 -CONTENTS[ - "1.25" -] = b"""\ +CONTENTS["1.25"] = b"""\ Header File Version,1.25 Patient Information diff --git a/mne/io/kit/kit.py b/mne/io/kit/kit.py index fa9ff8cfeea..88af0b2dc85 100644 --- a/mne/io/kit/kit.py +++ b/mne/io/kit/kit.py @@ -75,7 +75,7 @@ def _call_digitization(info, mrk, elp, hsp, kit_info): class UnsupportedKITFormat(ValueError): """Our reader is not guaranteed to work with old files.""" - def __init__(self, sqd_version, *args, **kwargs): # noqa: D102 + def __init__(self, sqd_version, *args, **kwargs): self.sqd_version = sqd_version ValueError.__init__(self, *args, **kwargs) @@ -134,7 +134,7 @@ def __init__( allow_unknown_format=False, standardize_names=None, verbose=None, - ): # noqa: D102 + ): logger.info("Extracting SQD Parameters from %s..." % input_fname) input_fname = op.abspath(input_fname) self.preload = False @@ -382,7 +382,7 @@ def __init__( allow_unknown_format=False, standardize_names=None, verbose=None, - ): # noqa: D102 + ): if isinstance(events, (str, PathLike, Path)): events = read_events(events) diff --git a/mne/io/nicolet/nicolet.py b/mne/io/nicolet/nicolet.py index 85a7d1e5607..37855b97054 100644 --- a/mne/io/nicolet/nicolet.py +++ b/mne/io/nicolet/nicolet.py @@ -183,7 +183,7 @@ def __init__( misc=(), preload=False, verbose=None, - ): # noqa: D102 + ): input_fname = path.abspath(input_fname) info, header_info = _get_nicolet_info(input_fname, ch_type, eog, ecg, emg, misc) last_samps = [header_info["num_samples"] - 1] diff --git a/mne/io/nihon/nihon.py b/mne/io/nihon/nihon.py index b39a18af838..919719f24a2 100644 --- a/mne/io/nihon/nihon.py +++ b/mne/io/nihon/nihon.py @@ -178,8 +178,7 @@ def _read_nihon_header(fname): control_block = np.fromfile(fid, "|S16", 1).astype("U16")[0] if control_block not in _valid_headers: raise ValueError( - "Not a valid Nihon Kohden EEG file " - "(control block {})".format(version) + f"Not a valid Nihon Kohden EEG file (control block {version})" ) fid.seek(0x17FE) diff --git a/mne/label.py b/mne/label.py index b57b466df27..77ddf5bcffd 100644 --- a/mne/label.py +++ b/mne/label.py @@ -242,7 +242,7 @@ def __init__( color=None, *, verbose=None, - ): # noqa: D102 + ): # check parameters if not isinstance(hemi, str): raise ValueError("hemi must be a string, not %s" % type(hemi)) @@ -1017,7 +1017,7 @@ class BiHemiLabel: The name of the subject. """ - def __init__(self, lh, rh, name=None, color=None): # noqa: D102 + def __init__(self, lh, rh, name=None, color=None): if lh.subject != rh.subject: raise ValueError( "lh.subject (%s) and rh.subject (%s) must " diff --git a/mne/preprocessing/eyetracking/eyetracking.py b/mne/preprocessing/eyetracking/eyetracking.py index ab3d51c6af1..f6b1b0fd0d4 100644 --- a/mne/preprocessing/eyetracking/eyetracking.py +++ b/mne/preprocessing/eyetracking/eyetracking.py @@ -78,8 +78,7 @@ def set_channel_types_eyetrack(inst, mapping): ch_type = ch_desc[0].lower() if ch_type not in valid_types: raise ValueError( - "ch_type must be one of {}. " - "Got '{}' instead.".format(valid_types, ch_type) + f"ch_type must be one of {valid_types}. Got '{ch_type}' instead." ) if ch_type == "eyegaze": coil_type = FIFF.FIFFV_COIL_EYETRACK_POS diff --git a/mne/preprocessing/ica.py b/mne/preprocessing/ica.py index 64667185330..1290c3d1e5a 100644 --- a/mne/preprocessing/ica.py +++ b/mne/preprocessing/ica.py @@ -445,7 +445,7 @@ def __init__( max_iter="auto", allow_ref_meg=False, verbose=None, - ): # noqa: D102 + ): _validate_type(method, str, "method") _validate_type(n_components, (float, "int-like", None)) diff --git a/mne/preprocessing/interpolate.py b/mne/preprocessing/interpolate.py index 8e69f364a10..828261d2651 100644 --- a/mne/preprocessing/interpolate.py +++ b/mne/preprocessing/interpolate.py @@ -123,8 +123,7 @@ def interpolate_bridged_electrodes(inst, bridged_idx, bad_limit=4): pos = montage.get_positions() if pos["coord_frame"] != "head": raise RuntimeError( - "Montage channel positions must be in ``head``" - "got {}".format(pos["coord_frame"]) + f"Montage channel positions must be in ``head`` got {pos['coord_frame']}" ) # store bads orig to put back at the end bads_orig = inst.info["bads"] diff --git a/mne/preprocessing/tests/test_annotate_amplitude.py b/mne/preprocessing/tests/test_annotate_amplitude.py index d39fabdb3ce..3618e480657 100644 --- a/mne/preprocessing/tests/test_annotate_amplitude.py +++ b/mne/preprocessing/tests/test_annotate_amplitude.py @@ -247,11 +247,11 @@ def test_flat_bad_acq_skip(): raw = read_raw_fif(skip_fname, preload=True) annots, bads = annotate_amplitude(raw, flat=0) assert len(annots) == 0 - assert bads == [ # MaxFilter finds the same 21 channels - "MEG%04d" % (int(num),) + assert bads == [ + f"MEG{num.zfill(4)}" for num in "141 331 421 431 611 641 1011 1021 1031 1241 1421 " "1741 1841 2011 2131 2141 2241 2531 2541 2611 2621".split() - ] + ] # MaxFilter finds the same 21 channels # -- overlap of flat segment with bad_acq_skip -- n_ch, n_times = 11, 1000 diff --git a/mne/preprocessing/tests/test_eeglab_infomax.py b/mne/preprocessing/tests/test_eeglab_infomax.py index f0835099c96..f4f4d1d68dc 100644 --- a/mne/preprocessing/tests/test_eeglab_infomax.py +++ b/mne/preprocessing/tests/test_eeglab_infomax.py @@ -171,9 +171,7 @@ def test_mne_python_vs_eeglab(): sources = np.dot(unmixing, Y) mixing = pinv(unmixing) - mvar = ( - np.sum(mixing**2, axis=0) * np.sum(sources**2, axis=1) / (N * T - 1) - ) + mvar = np.sum(mixing**2, axis=0) * np.sum(sources**2, axis=1) / (N * T - 1) windex = np.argsort(mvar)[::-1] unmixing_ordered = unmixing[windex, :] diff --git a/mne/preprocessing/tests/test_maxwell.py b/mne/preprocessing/tests/test_maxwell.py index b806ccf577a..6234b79c544 100644 --- a/mne/preprocessing/tests/test_maxwell.py +++ b/mne/preprocessing/tests/test_maxwell.py @@ -992,9 +992,9 @@ def _assert_shielding(raw_sss, erm_power, min_factor, max_factor=np.inf, meg="ma sss_power = raw_sss[picks][0].ravel() sss_power = np.sqrt(np.sum(sss_power * sss_power)) factor = erm_power / sss_power - assert ( - min_factor <= factor < max_factor - ), "Shielding factor not %0.3f <= %0.3f < %0.3f" % (min_factor, factor, max_factor) + assert min_factor <= factor < max_factor, ( + "Shielding factor not %0.3f <= %0.3f < %0.3f" % (min_factor, factor, max_factor) + ) @buggy_mkl_svd diff --git a/mne/source_estimate.py b/mne/source_estimate.py index efc5a06515a..213d00e5baa 100644 --- a/mne/source_estimate.py +++ b/mne/source_estimate.py @@ -497,9 +497,7 @@ class _BaseSourceEstimate(TimeMixin): _data_ndim = 2 @verbose - def __init__( - self, data, vertices, tmin, tstep, subject=None, verbose=None - ): # noqa: D102 + def __init__(self, data, vertices, tmin, tstep, subject=None, verbose=None): assert hasattr(self, "_data_ndim"), self.__class__.__name__ assert hasattr(self, "_src_type"), self.__class__.__name__ assert hasattr(self, "_src_count"), self.__class__.__name__ @@ -2001,7 +1999,7 @@ class _BaseVectorSourceEstimate(_BaseSourceEstimate): @verbose def __init__( self, data, vertices=None, tmin=None, tstep=None, subject=None, verbose=None - ): # noqa: D102 + ): assert hasattr(self, "_scalar_class") super().__init__(data, vertices, tmin, tstep, subject, verbose) @@ -2138,7 +2136,7 @@ def plot( add_data_kwargs=None, brain_kwargs=None, verbose=None, - ): # noqa: D102 + ): return plot_vector_source_estimates( self, subject=subject, @@ -2643,7 +2641,7 @@ def plot_3d( add_data_kwargs=None, brain_kwargs=None, verbose=None, - ): # noqa: D102 + ): return _BaseVectorSourceEstimate.plot( self, subject=subject, @@ -2734,7 +2732,7 @@ class _BaseMixedSourceEstimate(_BaseSourceEstimate): @verbose def __init__( self, data, vertices=None, tmin=None, tstep=None, subject=None, verbose=None - ): # noqa: D102 + ): if not isinstance(vertices, list) or len(vertices) < 2: raise ValueError( "Vertices must be a list of numpy arrays with " diff --git a/mne/source_space/_source_space.py b/mne/source_space/_source_space.py index e1d8611354c..ee8ef432a90 100644 --- a/mne/source_space/_source_space.py +++ b/mne/source_space/_source_space.py @@ -286,7 +286,7 @@ class SourceSpaces(list): access, like ``src.kind``. """ # noqa: E501 - def __init__(self, source_spaces, info=None): # noqa: D102 + def __init__(self, source_spaces, info=None): # First check the types is actually a valid config _validate_type(source_spaces, list, "source_spaces") super(SourceSpaces, self).__init__(source_spaces) # list diff --git a/mne/stats/permutations.py b/mne/stats/permutations.py index 3f515559c72..15c78ae0872 100644 --- a/mne/stats/permutations.py +++ b/mne/stats/permutations.py @@ -146,7 +146,7 @@ def stat_fun(x): rng = check_random_state(random_state) boot_indices = rng.choice(indices, replace=True, size=(n_bootstraps, len(indices))) stat = np.array([stat_fun(arr[inds]) for inds in boot_indices]) - ci = (((1 - ci) / 2) * 100, ((1 - ((1 - ci) / 2))) * 100) + ci = (((1 - ci) / 2) * 100, (1 - ((1 - ci) / 2)) * 100) ci_low, ci_up = np.percentile(stat, ci, axis=0) return np.array([ci_low, ci_up]) diff --git a/mne/tests/test_annotations.py b/mne/tests/test_annotations.py index 12964118f32..1a351de5527 100644 --- a/mne/tests/test_annotations.py +++ b/mne/tests/test_annotations.py @@ -425,7 +425,11 @@ def test_raw_reject(first_samp): with pytest.warns(RuntimeWarning, match="outside the data range"): raw.set_annotations(Annotations([2, 100, 105, 148], [2, 8, 5, 8], "BAD")) data, times = raw.get_data( - [0, 1, 3, 4], 100, 11200, "omit", return_times=True # 1-112 s + [0, 1, 3, 4], + 100, + 11200, + "omit", + return_times=True, # 1-112 s ) bad_times = np.concatenate( [np.arange(200, 400), np.arange(10000, 10800), np.arange(10500, 11000)] diff --git a/mne/tests/test_docstring_parameters.py b/mne/tests/test_docstring_parameters.py index 0118a6c36ba..f42147f378f 100644 --- a/mne/tests/test_docstring_parameters.py +++ b/mne/tests/test_docstring_parameters.py @@ -278,9 +278,7 @@ def test_tabs(): whiten_evoked write_fiducials write_info -""".split( - "\n" -) +""".split("\n") def test_documented(): diff --git a/mne/time_frequency/_stockwell.py b/mne/time_frequency/_stockwell.py index 1abf0c8e5a6..f92cc02a804 100644 --- a/mne/time_frequency/_stockwell.py +++ b/mne/time_frequency/_stockwell.py @@ -22,7 +22,7 @@ def _check_input_st(x_in, n_fft): n_times = x_in.shape[-1] def _is_power_of_two(n): - return not (n > 0 and ((n & (n - 1)))) + return not (n > 0 and (n & (n - 1))) if n_fft is None or (not _is_power_of_two(n_fft) and n_times > n_fft): # Compute next power of 2 diff --git a/mne/time_frequency/tfr.py b/mne/time_frequency/tfr.py index ce547568232..400b711512e 100644 --- a/mne/time_frequency/tfr.py +++ b/mne/time_frequency/tfr.py @@ -1401,7 +1401,7 @@ class AverageTFR(_BaseTFR): @verbose def __init__( self, info, data, times, freqs, nave, comment=None, method=None, verbose=None - ): # noqa: D102 + ): super().__init__() self.info = info if data.ndim != 3: @@ -2699,7 +2699,6 @@ def __init__( metadata=None, verbose=None, ): - # noqa: D102 super().__init__() self.info = info if data.ndim != 4: diff --git a/mne/transforms.py b/mne/transforms.py index f0efd287f40..b8dcb1728ff 100644 --- a/mne/transforms.py +++ b/mne/transforms.py @@ -111,7 +111,7 @@ class Transform(dict): ``'ctf_meg'``, ``'unknown'``. """ - def __init__(self, fro, to, trans=None): # noqa: D102 + def __init__(self, fro, to, trans=None): super(Transform, self).__init__() # we could add some better sanity checks here fro = _to_const(fro) diff --git a/mne/utils/_bunch.py b/mne/utils/_bunch.py index 0fdac59139f..ff04fcec91a 100644 --- a/mne/utils/_bunch.py +++ b/mne/utils/_bunch.py @@ -15,7 +15,7 @@ class Bunch(dict): """Dictionary-like object that exposes its keys as attributes.""" - def __init__(self, **kwargs): # noqa: D102 + def __init__(self, **kwargs): dict.__init__(self, kwargs) self.__dict__ = self diff --git a/mne/utils/_logging.py b/mne/utils/_logging.py index 1dcb1a5e8a6..f4546e5e7d8 100644 --- a/mne/utils/_logging.py +++ b/mne/utils/_logging.py @@ -159,7 +159,7 @@ class use_log_level: This message will be printed! """ - def __init__(self, verbose=None, *, add_frames=None): # noqa: D102 + def __init__(self, verbose=None, *, add_frames=None): self._level = verbose self._add_frames = add_frames self._old_frames = _filter.add_frames diff --git a/mne/utils/_testing.py b/mne/utils/_testing.py index 999d6242695..d767e25711c 100644 --- a/mne/utils/_testing.py +++ b/mne/utils/_testing.py @@ -50,7 +50,7 @@ def __new__(self): # noqa: D105 new = str.__new__(self, tempfile.mkdtemp(prefix="tmp_mne_tempdir_")) return new - def __init__(self): # noqa: D102 + def __init__(self): self._path = self.__str__() def __del__(self): # noqa: D105 @@ -121,7 +121,7 @@ def run_command_if_main(): class ArgvSetter: """Temporarily set sys.argv.""" - def __init__(self, args=(), disable_stdout=True, disable_stderr=True): # noqa: D102 + def __init__(self, args=(), disable_stdout=True, disable_stderr=True): self.argv = list(("python",) + args) self.stdout = ClosingStringIO() if disable_stdout else sys.stdout self.stderr = ClosingStringIO() if disable_stderr else sys.stderr diff --git a/mne/utils/docs.py b/mne/utils/docs.py index a1d1d15679d..806d774f221 100644 --- a/mne/utils/docs.py +++ b/mne/utils/docs.py @@ -64,42 +64,32 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): # %% # A -docdict[ - "accept" -] = """ +docdict["accept"] = """ accept : bool If True (default False), accept the license terms of this dataset. """ -docdict[ - "add_ch_type_export_params" -] = """ +docdict["add_ch_type_export_params"] = """ add_ch_type : bool Whether to incorporate the channel type into the signal label (e.g. whether to store channel "Fz" as "EEG Fz"). Only used for EDF format. Default is ``False``. """ -docdict[ - "add_data_kwargs" -] = """ +docdict["add_data_kwargs"] = """ add_data_kwargs : dict | None Additional arguments to brain.add_data (e.g., ``dict(time_label_size=10)``). """ -docdict[ - "add_frames" -] = """ +docdict["add_frames"] = """ add_frames : int | None If int, enable (>=1) or disable (0) the printing of stack frame information using formatting. Default (None) does not change the formatting. This can add overhead so is meant only for debugging. """ -docdict[ - "adjacency_clust" -] = """ +docdict["adjacency_clust"] = """ adjacency : scipy.sparse.spmatrix | None | False Defines adjacency between locations in the data, where "locations" can be spatial vertices, frequency bins, time points, etc. For spatial vertices @@ -155,25 +145,19 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): docdict["adjacency_clust"].format(**st).format(**groups) ) -docdict[ - "adjust_dig_chpi" -] = """ +docdict["adjust_dig_chpi"] = """ adjust_dig : bool If True, adjust the digitization locations used for fitting based on the positions localized at the start of the file. """ -docdict[ - "agg_fun_psd_topo" -] = """ +docdict["agg_fun_psd_topo"] = """ agg_fun : callable The function used to aggregate over frequencies. Defaults to :func:`numpy.sum` if ``normalize=True``, else :func:`numpy.mean`. """ -docdict[ - "align_view" -] = """ +docdict["align_view"] = """ align : bool If True, consider view arguments relative to canonical MRI directions (closest to MNI for the subject) rather than native MRI @@ -181,16 +165,12 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): have large rotations). """ -docdict[ - "allow_2d" -] = """ +docdict["allow_2d"] = """ allow_2d : bool If True, allow 2D data as input (i.e. n_samples, n_features). """ -docdict[ - "allow_empty_eltc" -] = """ +docdict["allow_empty_eltc"] = """ allow_empty : bool | str ``False`` (default) will emit an error if there are labels that have no vertices in the source estimate. ``True`` and ``'ignore'`` will return @@ -202,16 +182,12 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): Support for "ignore". """ -docdict[ - "alpha" -] = """ +docdict["alpha"] = """ alpha : float in [0, 1] Alpha level to control opacity. """ -docdict[ - "anonymize_info_notes" -] = """ +docdict["anonymize_info_notes"] = """ Removes potentially identifying information if it exists in ``info``. Specifically for each of the following we use: @@ -261,16 +237,12 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): docdict["applyfun_summary_evoked"] = applyfun_summary.format("evoked", "") docdict["applyfun_summary_raw"] = applyfun_summary.format("raw", applyfun_preload) -docdict[ - "area_alpha_plot_psd" -] = """\ +docdict["area_alpha_plot_psd"] = """\ area_alpha : float Alpha for the area. """ -docdict[ - "area_mode_plot_psd" -] = """\ +docdict["area_mode_plot_psd"] = """\ area_mode : str | None Mode for plotting area. If 'std', the mean +/- 1 STD (across channels) will be plotted. If 'range', the min and max (across channels) will be @@ -278,18 +250,14 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): If None, no area will be plotted. If average=False, no area is plotted. """ -docdict[ - "aseg" -] = """ +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. """ -docdict[ - "average_plot_evoked_topomap" -] = """ +docdict["average_plot_evoked_topomap"] = """ average : float | array-like of float, shape (n_times,) | None The time window (in seconds) around a given time point to be used for averaging. For example, 0.2 would translate into a time window that @@ -303,9 +271,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): Support for ``array-like`` input. """ -docdict[ - "average_plot_psd" -] = """\ +docdict["average_plot_psd"] = """\ average : bool If False, the PSDs of all channels is displayed. No averaging is done and parameters area_mode and area_alpha are ignored. When @@ -313,9 +279,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): drag) to plot a topomap. """ -docdict[ - "average_psd" -] = """\ +docdict["average_psd"] = """\ average : str | None How to average the segments. If ``mean`` (default), calculate the arithmetic mean. If ``median``, calculate the median, corrected for @@ -323,9 +287,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): segments. """ -docdict[ - "average_tfr" -] = """ +docdict["average_tfr"] = """ average : bool, default True If ``False`` return an `EpochsTFR` containing separate TFRs for each epoch. If ``True`` return an `AverageTFR` containing the average of all @@ -358,9 +320,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): docdict["axes_evoked_plot_topomap"] = _axes_list.format( "axes", "match the number of ``times`` provided (unless ``times`` is ``None``)" ) -docdict[ - "axes_montage" -] = """ +docdict["axes_montage"] = """ axes : instance of Axes | instance of Axes3D | None Axes to draw the sensors to. If ``kind='3d'``, axes must be an instance of Axes3D. If None (default), a new axes will be created.""" @@ -380,17 +340,13 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): "axes", "match the length of ``bands``" ) -docdict[ - "axis_facecolor" -] = """\ +docdict["axis_facecolor"] = """\ axis_facecolor : str | tuple A matplotlib-compatible color to use for the axis background. Defaults to black. """ -docdict[ - "azimuth" -] = """ +docdict["azimuth"] = """ azimuth : float The azimuthal angle of the camera rendering the view in degrees. """ @@ -398,17 +354,13 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): # %% # B -docdict[ - "bad_condition_maxwell_cond" -] = """ +docdict["bad_condition_maxwell_cond"] = """ bad_condition : str How to deal with ill-conditioned SSS matrices. Can be ``"error"`` (default), ``"warning"``, ``"info"``, or ``"ignore"``. """ -docdict[ - "bands_psd_topo" -] = """ +docdict["bands_psd_topo"] = """ bands : None | dict | list of tuple The frequencies or frequency ranges to plot. If a :class:`dict`, keys will be used as subplot titles and values should be either a single frequency @@ -431,9 +383,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): Allow passing a dict and discourage passing tuples. """ -docdict[ - "base_estimator" -] = """ +docdict["base_estimator"] = """ base_estimator : object The base estimator to iteratively fit on a subset of the dataset. """ @@ -452,9 +402,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): timepoints ``t`` such that ``a <= t <= b``. """ -docdict[ - "baseline_epochs" -] = f"""{_baseline_rescale_base} +docdict["baseline_epochs"] = f"""{_baseline_rescale_base} Correction is applied **to each epoch and channel individually** in the following way: @@ -463,9 +411,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): """ -docdict[ - "baseline_evoked" -] = f"""{_baseline_rescale_base} +docdict["baseline_evoked"] = f"""{_baseline_rescale_base} Correction is applied **to each channel individually** in the following way: @@ -474,9 +420,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): """ -docdict[ - "baseline_report" -] = f"""{_baseline_rescale_base} +docdict["baseline_report"] = f"""{_baseline_rescale_base} Correction is applied in the following way **to each channel:** 1. Calculate the mean signal of the baseline period. @@ -487,9 +431,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): docdict["baseline_rescale"] = _baseline_rescale_base -docdict[ - "baseline_stc" -] = f"""{_baseline_rescale_base} +docdict["baseline_stc"] = f"""{_baseline_rescale_base} Correction is applied **to each source individually** in the following way: @@ -505,47 +447,35 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): """ -docdict[ - "block" -] = """\ +docdict["block"] = """\ block : bool Whether to halt program execution until the figure is closed. May not work on all systems / platforms. Defaults to ``False``. """ -docdict[ - "border_topomap" -] = """ +docdict["border_topomap"] = """ border : float | 'mean' Value to extrapolate to on the topomap borders. If ``'mean'`` (default), then each extrapolated point has the average value of its neighbours. """ -docdict[ - "brain_kwargs" -] = """ +docdict["brain_kwargs"] = """ brain_kwargs : dict | None Additional arguments to the :class:`mne.viz.Brain` constructor (e.g., ``dict(silhouette=True)``). """ -docdict[ - "brain_update" -] = """ +docdict["brain_update"] = """ update : bool Force an update of the plot. Defaults to True. """ -docdict[ - "browser" -] = """ +docdict["browser"] = """ fig : matplotlib.figure.Figure | mne_qt_browser.figure.MNEQtBrowser Browser instance. """ -docdict[ - "buffer_size_clust" -] = """ +docdict["buffer_size_clust"] = """ buffer_size : int | None Block size to use when computing test statistics. This can significantly reduce memory usage when ``n_jobs > 1`` and memory sharing between @@ -554,9 +484,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): a small block of locations at a time. """ -docdict[ - "by_event_type" -] = """ +docdict["by_event_type"] = """ by_event_type : bool When ``False`` (the default) all epochs are processed together and a single :class:`~mne.Evoked` object is returned. When ``True``, epochs are first @@ -571,18 +499,14 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): # %% # C -docdict[ - "calibration_maxwell_cal" -] = """ +docdict["calibration_maxwell_cal"] = """ calibration : str | None Path to the ``'.dat'`` file with fine calibration coefficients. File can have 1D or 3D gradiometer imbalance correction. This file is machine/site-specific. """ -docdict[ - "cbar_fmt_topomap" -] = """\ +docdict["cbar_fmt_topomap"] = """\ cbar_fmt : str Formatting string for colorbar tick labels. See :ref:`formatspec` for details. @@ -595,17 +519,13 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): """ ) -docdict[ - "center" -] = """ +docdict["center"] = """ center : float or None If not None, center of a divergent colormap, changes the meaning of fmin, fmax and fmid. """ -docdict[ - "ch_name_ecg" -] = """ +docdict["ch_name_ecg"] = """ ch_name : None | str The name of the channel to use for ECG peak detection. If ``None`` (default), ECG channel is used if present. If ``None`` and @@ -614,9 +534,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): MEG channels. """ -docdict[ - "ch_name_eog" -] = """ +docdict["ch_name_eog"] = """ ch_name : str | list of str | None The name of the channel(s) to use for EOG peak detection. If a string, can be an arbitrary channel. This doesn't have to be a channel of @@ -628,9 +546,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): If ``None`` (default), use the channel(s) in ``raw`` with type ``eog``. """ -docdict[ - "ch_names_annot" -] = """ +docdict["ch_names_annot"] = """ ch_names : list | None List of lists of channel names associated with the annotations. Empty entries are assumed to be associated with no specific channel, @@ -644,9 +560,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): ch_names=[[], ['MEG0111', 'MEG2563'], ['MEG1443']]) """ -docdict[ - "ch_type_set_eeg_reference" -] = """ +docdict["ch_type_set_eeg_reference"] = """ ch_type : list of str | str The name of the channel type to apply the reference to. Valid channel types are ``'auto'``, ``'eeg'``, ``'ecog'``, ``'seeg'``, @@ -685,34 +599,26 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): docdict["channel_wise_applyfun_epo"] = chwise.format("in each epoch ", "epochs and ") -docdict[ - "check_disjoint_clust" -] = """ +docdict["check_disjoint_clust"] = """ check_disjoint : bool Whether to check if the connectivity matrix can be separated into disjoint sets before clustering. This may lead to faster clustering, especially if the second dimension of ``X`` (usually the "time" dimension) is large. """ -docdict[ - "chpi_amplitudes" -] = """ +docdict["chpi_amplitudes"] = """ chpi_amplitudes : dict The time-varying cHPI coil amplitudes, with entries "times", "proj", and "slopes". """ -docdict[ - "chpi_locs" -] = """ +docdict["chpi_locs"] = """ chpi_locs : dict The time-varying cHPI coils locations, with entries "times", "rrs", "moments", and "gofs". """ -docdict[ - "clim" -] = """ +docdict["clim"] = """ clim : str | dict Colorbar properties specification. If 'auto', set clim automatically based on data percentiles. If dict, should contain: @@ -731,9 +637,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): only divergent colormaps should be used with ``pos_lims``. """ -docdict[ - "clim_onesided" -] = """ +docdict["clim_onesided"] = """ clim : str | dict Colorbar properties specification. If 'auto', set clim automatically based on data percentiles. If dict, should contain: @@ -747,17 +651,13 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): ``pos_lims``, as the surface plot must show the magnitude. """ -docdict[ - "cmap" -] = """ +docdict["cmap"] = """ cmap : matplotlib colormap | str | None The :class:`~matplotlib.colors.Colormap` to use. Defaults to ``None``, which will use the matplotlib default colormap. """ -docdict[ - "cmap_topomap" -] = """ +docdict["cmap_topomap"] = """ cmap : matplotlib colormap | (colormap, bool) | 'interactive' | None Colormap to use. If :class:`tuple`, the first value indicates the colormap to use and the second value is a boolean defining interactivity. In @@ -774,17 +674,13 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): 2 topomaps. """ -docdict[ - "cmap_topomap_simple" -] = """ +docdict["cmap_topomap_simple"] = """ cmap : matplotlib colormap | None Colormap to use. If None, 'Reds' is used for all positive data, otherwise defaults to 'RdBu_r'. """ -docdict[ - "cnorm" -] = """ +docdict["cnorm"] = """ cnorm : matplotlib.colors.Normalize | None How to normalize the colormap. If ``None``, standard linear normalization is performed. If not ``None``, ``vmin`` and ``vmax`` will be ignored. @@ -793,57 +689,43 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): :ref:`the ERDs example` for an example of its use. """ -docdict[ - "color_matplotlib" -] = """ +docdict["color_matplotlib"] = """ color : color A list of anything matplotlib accepts: string, RGB, hex, etc. """ -docdict[ - "color_plot_psd" -] = """\ +docdict["color_plot_psd"] = """\ color : str | tuple A matplotlib-compatible color to use. Has no effect when spatial_colors=True. """ -docdict[ - "color_spectrum_plot_topo" -] = """\ +docdict["color_spectrum_plot_topo"] = """\ color : str | tuple A matplotlib-compatible color to use for the curves. Defaults to white. """ -docdict[ - "colorbar_topomap" -] = """ +docdict["colorbar_topomap"] = """ colorbar : bool Plot a colorbar in the rightmost column of the figure. """ -docdict[ - "colormap" -] = """ +docdict["colormap"] = """ colormap : str | np.ndarray of float, shape(n_colors, 3 | 4) Name of colormap to use or a custom look up table. If array, must be (n x 3) or (n x 4) array for with RGB or RGBA values between 0 and 255. """ -docdict[ - "combine" -] = """ +docdict["combine"] = """ combine : None | str | callable How to combine information across channels. If a :class:`str`, must be one of 'mean', 'median', 'std' (standard deviation) or 'gfp' (global field power). """ -docdict[ - "compute_proj_ecg" -] = """This function will: +docdict["compute_proj_ecg"] = """This function will: #. Filter the ECG data channel. @@ -858,9 +740,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): #. Calculate SSP projection vectors on that data to capture the artifacts.""" -docdict[ - "compute_proj_eog" -] = """This function will: +docdict["compute_proj_eog"] = """This function will: #. Filter the EOG data channel. @@ -876,18 +756,14 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): #. Calculate SSP projection vectors on that data to capture the artifacts.""" -docdict[ - "compute_ssp" -] = """This function aims to find those SSP vectors that +docdict["compute_ssp"] = """This function aims to find those SSP vectors that will project out the ``n`` most prominent signals from the data for each specified sensor type. Consequently, if the provided input data contains high levels of noise, the produced SSP vectors can then be used to eliminate that noise from the data. """ -docdict[ - "contours_topomap" -] = """ +docdict["contours_topomap"] = """ contours : int | array-like The number of contour lines to draw. If ``0``, no contours will be drawn. If a positive integer, that number of contour levels are chosen using the @@ -898,9 +774,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): corresponding to the contour levels. Default is ``6``. """ -docdict[ - "coord_frame_maxwell" -] = """ +docdict["coord_frame_maxwell"] = """ coord_frame : str The coordinate frame that the ``origin`` is specified in, either ``'meg'`` or ``'head'``. For empty-room recordings that do not have @@ -908,17 +782,13 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): frame should be used. """ -docdict[ - "copy_df" -] = """ +docdict["copy_df"] = """ copy : bool If ``True``, data will be copied. Otherwise data may be modified in place. Defaults to ``True``. """ -docdict[ - "create_ecg_epochs" -] = """This function will: +docdict["create_ecg_epochs"] = """This function will: #. Filter the ECG data channel. @@ -927,9 +797,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): #. Create `~mne.Epochs` around the R wave peaks, capturing the heartbeats. """ -docdict[ - "create_eog_epochs" -] = """This function will: +docdict["create_eog_epochs"] = """This function will: #. Filter the EOG data channel. @@ -939,9 +807,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): #. Create `~mne.Epochs` around the eyeblinks. """ -docdict[ - "cross_talk_maxwell" -] = """ +docdict["cross_talk_maxwell"] = """ cross_talk : str | None Path to the FIF file with cross-talk correction information. """ @@ -955,9 +821,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): 10 × log₁₀(spectral power){}.{} """ -docdict[ - "dB_plot_psd" -] = """\ +docdict["dB_plot_psd"] = """\ dB : bool Plot Power Spectral Density (PSD), in units (amplitude**2/Hz (dB)) if ``dB=True``, and ``estimate='power'`` or ``estimate='auto'``. Plot PSD @@ -973,9 +837,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): docdict["dB_spectrum_plot"] = _dB.format("", "") docdict["dB_spectrum_plot_topo"] = _dB.format("", " Ignored if ``normalize=True``.") -docdict[ - "daysback_anonymize_info" -] = """ +docdict["daysback_anonymize_info"] = """ daysback : int | None Number of days to subtract from all dates. If ``None`` (default), the acquisition date, ``info['meas_date']``, @@ -983,15 +845,11 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): ``info['meas_date']`` is ``None`` (i.e., no acquisition date has been set). """ -docdict[ - "dbs" -] = """ +docdict["dbs"] = """ dbs : bool If True (default), show DBS (deep brain stimulation) electrodes. """ -docdict[ - "decim" -] = """ +docdict["decim"] = """ decim : int Factor by which to subsample the data. @@ -1002,9 +860,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): may occur. """ -docdict[ - "decim_notes" -] = """ +docdict["decim_notes"] = """ For historical reasons, ``decim`` / "decimation" refers to simply subselecting samples from a given signal. This contrasts with the broader signal processing literature, where decimation is defined as (quoting @@ -1024,9 +880,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): ``inst.decimate(4)``. """ -docdict[ - "decim_tfr" -] = """ +docdict["decim_tfr"] = """ decim : int | slice, default 1 To reduce memory usage, decimation factor after time-frequency decomposition. @@ -1039,9 +893,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): artifacts. """ -docdict[ - "depth" -] = """ +docdict["depth"] = """ depth : None | float | dict How to weight (or normalize) the forward using a depth prior. If float (default 0.8), it acts as the depth weighting exponent (``exp``) @@ -1054,9 +906,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): Depth bias ignored for ``method='eLORETA'``. """ -docdict[ - "destination_maxwell_dest" -] = """ +docdict["destination_maxwell_dest"] = """ destination : path-like | array-like, shape (3,) | None The destination location for the head. Can be ``None``, which will not change the head position, or a path to a FIF file @@ -1067,9 +917,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): head location). """ -docdict[ - "detrend_epochs" -] = """ +docdict["detrend_epochs"] = """ detrend : int | None If 0 or 1, the data channels (MEG and EEG) will be detrended when loaded. 0 is a constant (DC) detrend, 1 is a linear detrend. None @@ -1080,17 +928,13 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): (will yield equivalent results but be slower). """ -docdict[ - "df_return" -] = """ +docdict["df_return"] = """ df : instance of pandas.DataFrame A dataframe suitable for usage with other statistical/plotting/analysis packages. """ -docdict[ - "dig_kinds" -] = """ +docdict["dig_kinds"] = """ dig_kinds : list of str | str Kind of digitization points to use in the fitting. These can be any combination of ('cardinal', 'hpi', 'eeg', 'extra'). Can also @@ -1099,9 +943,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): 'eeg' points. """ -docdict[ - "dipole" -] = """ +docdict["dipole"] = """ dipole : instance of Dipole | list of Dipole Dipole object containing position, orientation and amplitude of one or more dipoles. Multiple simultaneous dipoles may be defined by @@ -1112,9 +954,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): Added support for a list of :class:`mne.Dipole` instances. """ -docdict[ - "distance" -] = """ +docdict["distance"] = """ distance : float | "auto" | None The distance from the camera rendering the view to the focalpoint in plot units (either m or mm). If "auto", the bounds of visible objects will be @@ -1124,17 +964,13 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): ``None`` will no longer change the distance, use ``"auto"`` instead. """ -docdict[ - "drop_log" -] = """ +docdict["drop_log"] = """ drop_log : tuple | None Tuple of tuple of strings indicating which epochs have been marked to be ignored. """ -docdict[ - "dtype_applyfun" -] = """ +docdict["dtype_applyfun"] = """ dtype : numpy.dtype Data type to use after applying the function. If None (default) the data type is not modified. @@ -1143,16 +979,12 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): # %% # E -docdict[ - "ecog" -] = """ +docdict["ecog"] = """ ecog : bool If True (default), show ECoG sensors. """ -docdict[ - "edf_resamp_note" -] = """ +docdict["edf_resamp_note"] = """ :class:`mne.io.Raw` only stores signals with matching sampling frequencies. Therefore, if mixed sampling frequency signals are requested, all signals are upsampled to the highest loaded sampling frequency. In this case, using @@ -1160,9 +992,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): slices of the signal are requested. """ -docdict[ - "eeg" -] = """ +docdict["eeg"] = """ eeg : bool | str | list | dict String options are: @@ -1180,16 +1010,12 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): Added support for specifying alpha values as a dict. """ -docdict[ - "elevation" -] = """ +docdict["elevation"] = """ elevation : float The The zenith angle of the camera rendering the view in degrees. """ -docdict[ - "eltc_mode_notes" -] = """ +docdict["eltc_mode_notes"] = """ Valid values for ``mode`` are: - ``'max'`` @@ -1228,32 +1054,24 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): ``'max'``, and ``'auto'``. """ -docdict[ - "emit_warning" -] = """ +docdict["emit_warning"] = """ emit_warning : bool Whether to emit warnings when cropping or omitting annotations. """ -docdict[ - "encoding_edf" -] = """ +docdict["encoding_edf"] = """ encoding : str Encoding of annotations channel(s). Default is "utf8" (the only correct encoding according to the EDF+ standard). """ -docdict[ - "epochs_preload" -] = """ +docdict["epochs_preload"] = """ Load all epochs from disk when creating the object or wait before accessing each epoch (more memory efficient but can be slower). """ -docdict[ - "epochs_reject_tmin_tmax" -] = """ +docdict["epochs_reject_tmin_tmax"] = """ reject_tmin, reject_tmax : float | None Start and end of the time window used to reject epochs based on peak-to-peak (PTP) amplitudes as specified via ``reject`` and ``flat``. @@ -1264,27 +1082,21 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): both, ``reject`` and ``flat``. """ -docdict[ - "epochs_tmin_tmax" -] = """ +docdict["epochs_tmin_tmax"] = """ tmin, tmax : float Start and end time of the epochs in seconds, relative to the time-locked event. The closest or matching samples corresponding to the start and end time are included. Defaults to ``-0.2`` and ``0.5``, respectively. """ -docdict[ - "estimate_plot_psd" -] = """\ +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. """ -docdict[ - "event_color" -] = """ +docdict["event_color"] = """ event_color : color object | dict | None Color(s) to use for :term:`events`. To show all :term:`events` in the same color, pass any matplotlib-compatible color. To color events differently, @@ -1294,9 +1106,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): color cycle. """ -docdict[ - "event_id" -] = """ +docdict["event_id"] = """ event_id : int | list of int | dict | None The id of the :term:`events` to consider. If dict, the keys can later be used to access associated :term:`events`. Example: @@ -1305,16 +1115,12 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): are used. If None, all :term:`events` will be used and a dict is created with string integer names corresponding to the event id integers.""" -docdict[ - "event_id_ecg" -] = """ +docdict["event_id_ecg"] = """ event_id : int The index to assign to found ECG events. """ -docdict[ - "event_repeated_epochs" -] = """ +docdict["event_repeated_epochs"] = """ event_repeated : str How to handle duplicates in ``events[:, 0]``. Can be ``'error'`` (default), to raise an error, 'drop' to only retain the row occurring @@ -1324,17 +1130,13 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): .. versionadded:: 0.19 """ -docdict[ - "events" -] = """ +docdict["events"] = """ events : array of int, shape (n_events, 3) The array of :term:`events`. The first column contains the event time in samples, with :term:`first_samp` included. The third column contains the event id.""" -docdict[ - "events_epochs" -] = """ +docdict["events_epochs"] = """ events : array of int, shape (n_events, 3) The array of :term:`events`. The first column contains the event time in samples, with :term:`first_samp` included. The third column contains the @@ -1342,9 +1144,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): If some events don't match the events of interest as specified by ``event_id``, they will be marked as ``IGNORED`` in the drop log.""" -docdict[ - "evoked_by_event_type_returns" -] = """ +docdict["evoked_by_event_type_returns"] = """ evoked : instance of Evoked | list of Evoked The averaged epochs. When ``by_event_type=True`` was specified, a list is returned containing a @@ -1353,18 +1153,14 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): dictionary. """ -docdict[ - "exclude_clust" -] = """ +docdict["exclude_clust"] = """ exclude : bool array or None Mask to apply to the data to exclude certain points from clustering (e.g., medial wall vertices). Should be the same shape as ``X``. If ``None``, no points are excluded. """ -docdict[ - "exclude_frontal" -] = """ +docdict["exclude_frontal"] = """ exclude_frontal : bool If True, exclude points that have both negative Z values (below the nasion) and positive Y values (in front of the LPA/RPA). @@ -1383,9 +1179,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): " from being drawn", "spectrum." ) -docdict[ - "export_edf_note" -] = """ +docdict["export_edf_note"] = """ For EDF exports, only channels measured in Volts are allowed; in MNE-Python this means channel types 'eeg', 'ecog', 'seeg', 'emg', 'eog', 'ecg', 'dbs', 'bio', and 'misc'. 'stim' channels are dropped. Although this function @@ -1404,9 +1198,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): `. """ -docdict[ - "export_eeglab_note" -] = """ +docdict["export_eeglab_note"] = """ For EEGLAB exports, channel locations are expanded to full EEGLAB format. For more details see :func:`eeglabio.utils.cart_to_eeglab`. """ @@ -1416,59 +1208,39 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): from the filename extension. See supported formats above for more information.""" -docdict[ - "export_fmt_params_epochs" -] = """ +docdict["export_fmt_params_epochs"] = """ fmt : 'auto' | 'eeglab' {} -""".format( - _export_fmt_params_base -) +""".format(_export_fmt_params_base) -docdict[ - "export_fmt_params_evoked" -] = """ +docdict["export_fmt_params_evoked"] = """ fmt : 'auto' | 'mff' {} -""".format( - _export_fmt_params_base -) +""".format(_export_fmt_params_base) -docdict[ - "export_fmt_params_raw" -] = """ +docdict["export_fmt_params_raw"] = """ fmt : 'auto' | 'brainvision' | 'edf' | 'eeglab' {} -""".format( - _export_fmt_params_base -) +""".format(_export_fmt_params_base) -docdict[ - "export_fmt_support_epochs" -] = """\ +docdict["export_fmt_support_epochs"] = """\ Supported formats: - EEGLAB (``.set``, uses :mod:`eeglabio`) """ -docdict[ - "export_fmt_support_evoked" -] = """\ +docdict["export_fmt_support_evoked"] = """\ Supported formats: - MFF (``.mff``, uses :func:`mne.export.export_evokeds_mff`) """ -docdict[ - "export_fmt_support_raw" -] = """\ +docdict["export_fmt_support_raw"] = """\ Supported formats: - BrainVision (``.vhdr``, ``.vmrk``, ``.eeg``, uses `pybv `_) - EEGLAB (``.set``, uses :mod:`eeglabio`) - EDF (``.edf``, uses `edfio `_) """ # noqa: E501 -docdict[ - "export_warning" -] = """\ +docdict["export_warning"] = """\ .. warning:: Since we are exporting to external formats, there's no guarantee that all the info will be preserved in the external format. See Notes for details. @@ -1488,9 +1260,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): docdict["export_warning_note_raw"] = _export_warning_note_base.format("io.Raw") -docdict[ - "ext_order_chpi" -] = """ +docdict["ext_order_chpi"] = """ ext_order : int The external order for SSS-like interfence suppression. The SSS bases are used as projection vectors during fitting. @@ -1500,16 +1270,12 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): detection of true HPI signals. """ -docdict[ - "ext_order_maxwell" -] = """ +docdict["ext_order_maxwell"] = """ ext_order : int Order of external component of spherical expansion. """ -docdict[ - "extended_proj_maxwell" -] = """ +docdict["extended_proj_maxwell"] = """ extended_proj : list The empty-room projection vectors used to extend the external SSS basis (i.e., use eSSS). @@ -1517,9 +1283,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): .. versionadded:: 0.21 """ -docdict[ - "extrapolate_topomap" -] = """ +docdict["extrapolate_topomap"] = """ extrapolate : str Options: @@ -1538,18 +1302,14 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): the head circle. """ -docdict[ - "eyelink_apply_offsets" -] = """ +docdict["eyelink_apply_offsets"] = """ apply_offsets : bool (default False) Adjusts the onset time of the :class:`~mne.Annotations` created from Eyelink experiment messages, if offset values exist in the ASCII file. If False, any offset-like values will be prepended to the annotation description. """ -docdict[ - "eyelink_create_annotations" -] = """ +docdict["eyelink_create_annotations"] = """ create_annotations : bool | list (default True) Whether to create :class:`~mne.Annotations` from occular events (blinks, fixations, saccades) and experiment messages. If a list, must @@ -1558,24 +1318,18 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): experiment messages. """ -docdict[ - "eyelink_find_overlaps" -] = """ +docdict["eyelink_find_overlaps"] = """ find_overlaps : bool (default False) Combine left and right eye :class:`mne.Annotations` (blinks, fixations, saccades) if their start times and their stop times are both not separated by more than overlap_threshold. """ -docdict[ - "eyelink_fname" -] = """ +docdict["eyelink_fname"] = """ fname : path-like Path to the eyelink file (``.asc``).""" -docdict[ - "eyelink_overlap_threshold" -] = """ +docdict["eyelink_overlap_threshold"] = """ overlap_threshold : float (default 0.05) Time in seconds. Threshold of allowable time-gap between both the start and stop times of the left and right eyes. If the gap is larger than the threshold, @@ -1591,9 +1345,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): # %% # F -docdict[ - "f_power_clust" -] = """ +docdict["f_power_clust"] = """ t_power : float Power to raise the statistical values (usually F-values) by before summing (sign will be retained). Note that ``t_power=0`` will give a @@ -1601,9 +1353,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): by its statistical score. """ -docdict[ - "fiducials" -] = """ +docdict["fiducials"] = """ fiducials : list | dict | str The fiducials given in the MRI (surface RAS) coordinate system. If a dictionary is provided, it must contain the **keys** @@ -1618,17 +1368,13 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): and if absent, falls back to ``'estimated'``. """ -docdict[ - "fig_facecolor" -] = """\ +docdict["fig_facecolor"] = """\ fig_facecolor : str | tuple A matplotlib-compatible color to use for the figure background. Defaults to black. """ -docdict[ - "filter_length" -] = """ +docdict["filter_length"] = """ filter_length : str | int Length of the FIR filter to use (if applicable): @@ -1645,16 +1391,12 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): this should not be used. """ -docdict[ - "filter_length_ecg" -] = """ +docdict["filter_length_ecg"] = """ filter_length : str | int | None Number of taps to use for filtering. """ -docdict[ - "filter_length_notch" -] = """ +docdict["filter_length_notch"] = """ filter_length : str | int Length of the FIR filter to use (if applicable): @@ -1679,9 +1421,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): The default in 0.21 is None, but this will change to ``'10s'`` in 0.22. """ -docdict[ - "fir_design" -] = """ +docdict["fir_design"] = """ fir_design : str Can be "firwin" (default) to use :func:`scipy.signal.firwin`, or "firwin2" to use :func:`scipy.signal.firwin2`. "firwin" uses @@ -1691,9 +1431,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): .. versionadded:: 0.15 """ -docdict[ - "fir_window" -] = """ +docdict["fir_window"] = """ fir_window : str The window to use in FIR design, can be "hamming" (default), "hann" (default in 0.13), or "blackman". @@ -1708,9 +1446,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): is smaller than this threshold, the epoch will be dropped. If ``None`` then no rejection is performed based on flatness of the signal.""" -docdict[ - "flat" -] = f""" +docdict["flat"] = f""" flat : dict | None {_flat_common} @@ -1718,9 +1454,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): quality, pass the ``reject_tmin`` and ``reject_tmax`` parameters. """ -docdict[ - "flat_drop_bad" -] = f""" +docdict["flat_drop_bad"] = f""" flat : dict | str | None {_flat_common} If ``'existing'``, then the flat parameters set during epoch creation are @@ -1737,9 +1471,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): docdict["fmin_fmax_psd_topo"] = _fmin_fmax.format("``fmin=0, fmax=100``.") -docdict[ - "fmin_fmid_fmax" -] = """ +docdict["fmin_fmid_fmax"] = """ fmin : float Minimum value in colormap (uses real fmin if None). fmid : float @@ -1749,33 +1481,25 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): Maximum value in colormap (uses real max if None). """ -docdict[ - "fname_epochs" -] = """ +docdict["fname_epochs"] = """ fname : path-like | file-like The epochs to load. If a filename, should end with ``-epo.fif`` or ``-epo.fif.gz``. If a file-like object, preloading must be used. """ -docdict[ - "fname_export_params" -] = """ +docdict["fname_export_params"] = """ fname : str Name of the output file. """ -docdict[ - "fname_fwd" -] = """ +docdict["fname_fwd"] = """ fname : path-like File name to save the forward solution to. It should end with ``-fwd.fif`` or ``-fwd.fif.gz`` to save to FIF, or ``-fwd.h5`` to save to HDF5. """ -docdict[ - "fnirs" -] = """ +docdict["fnirs"] = """ fnirs : str | list | dict | bool | None Can be "channels", "pairs", "detectors", and/or "sources" to show the fNIRS channel locations, optode locations, or line between @@ -1788,34 +1512,26 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): Added support for specifying alpha values as a dict. """ -docdict[ - "focalpoint" -] = """ +docdict["focalpoint"] = """ focalpoint : tuple, shape (3,) | str | None The focal point of the camera rendering the view: (x, y, z) in plot units (either m or mm). When ``"auto"``, it is set to the center of mass of the visible bounds. """ -docdict[ - "forward_set_eeg_reference" -] = """ +docdict["forward_set_eeg_reference"] = """ forward : instance of Forward | None Forward solution to use. Only used with ``ref_channels='REST'``. .. versionadded:: 0.21 """ -docdict[ - "freqs_tfr" -] = """ +docdict["freqs_tfr"] = """ freqs : array of float, shape (n_freqs,) The frequencies of interest in Hz. """ -docdict[ - "fullscreen" -] = """ +docdict["fullscreen"] = """ fullscreen : bool Whether to start in fullscreen (``True``) or windowed mode (``False``). @@ -1835,17 +1551,13 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): " because it will apply channel-wise" ) -docdict[ - "fwd" -] = """ +docdict["fwd"] = """ fwd : instance of Forward The forward solution. If present, the orientations of the dipoles present in the forward solution are displayed. """ -docdict[ - "fwhm_morlet_notes" -] = r""" +docdict["fwhm_morlet_notes"] = r""" Convolution of a signal with a Morlet wavelet will impose temporal smoothing that is determined by the duration of the wavelet. In MNE-Python, the duration of the wavelet is determined by the ``sigma`` parameter, which gives the @@ -1879,9 +1591,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): # %% # G -docdict[ - "get_peak_parameters" -] = """ +docdict["get_peak_parameters"] = """ tmin : float | None The minimum point in time to be considered for peak getting. tmax : float | None @@ -1911,9 +1621,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): docdict["getitem_epochspectrum_return"] = _getitem_base.format(*_fill_epochs) docdict["getitem_spectrum_return"] = _getitem_base.format("", "", "") -docdict[ - "group_by_browse" -] = """ +docdict["group_by_browse"] = """ group_by : str How to group channels. ``'type'`` groups by channel type, ``'original'`` plots in the order of ch_names, ``'selection'`` uses @@ -1929,17 +1637,13 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): # %% # H -docdict[ - "h_freq" -] = """ +docdict["h_freq"] = """ h_freq : float | None For FIR filters, the upper pass-band edge; for IIR filters, the upper cutoff frequency. If None the data are only high-passed. """ -docdict[ - "h_trans_bandwidth" -] = """ +docdict["h_trans_bandwidth"] = """ h_trans_bandwidth : float | str Width of the transition band at the high cut-off frequency in Hz (low pass or cutoff 2 in bandpass). Can be "auto" @@ -1950,9 +1654,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): Only used for ``method='fir'``. """ -docdict[ - "head_pos" -] = """ +docdict["head_pos"] = """ head_pos : None | path-like | dict | tuple | array Path to the position estimates file. Should be in the format of the files produced by MaxFilter. If dict, keys should @@ -1964,26 +1666,20 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): :func:`mne.chpi.read_head_pos`. """ -docdict[ - "head_pos_maxwell" -] = """ +docdict["head_pos_maxwell"] = """ head_pos : array | None If array, movement compensation will be performed. The array should be of shape (N, 10), holding the position parameters as returned by e.g. ``read_head_pos``. """ -docdict[ - "head_source" -] = """ +docdict["head_source"] = """ head_source : str | list of str Head source(s) to use. See the ``source`` option of :func:`mne.get_head_surf` for more information. """ -docdict[ - "hitachi_fname" -] = """ +docdict["hitachi_fname"] = """ fname : list | str Path(s) to the Hitachi CSV file(s). This should only be a list for multiple probes that were acquired simultaneously. @@ -1992,9 +1688,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): Added support for list-of-str. """ -docdict[ - "hitachi_notes" -] = """ +docdict["hitachi_notes"] = """ Hitachi does not encode their channel positions, so you will need to create a suitable mapping using :func:`mne.channels.make_standard_montage` or :func:`mne.channels.make_dig_montage` like (for a 3x5/ETG-7000 example): @@ -2049,9 +1743,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): # %% # I -docdict[ - "idx_pctf" -] = """ +docdict["idx_pctf"] = """ idx : list of int | list of Label Source for indices for which to compute PSFs or CTFs. If mode is None, PSFs/CTFs will be returned for all indices. If mode is not None, the @@ -2065,27 +1757,21 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): specified labels. """ -docdict[ - "ignore_ref_maxwell" -] = """ +docdict["ignore_ref_maxwell"] = """ ignore_ref : bool If True, do not include reference channels in compensation. This option should be True for KIT files, since Maxwell filtering with reference channels is not currently supported. """ -docdict[ - "iir_params" -] = """ +docdict["iir_params"] = """ iir_params : dict | None Dictionary of parameters to use for IIR filtering. If ``iir_params=None`` and ``method="iir"``, 4th order Butterworth will be used. For more information, see :func:`mne.filter.construct_iir_filter`. """ -docdict[ - "image_format_report" -] = """ +docdict["image_format_report"] = """ image_format : 'png' | 'svg' | 'gif' | None The image format to be used for the report, can be ``'png'``, ``'svg'``, or ``'gif'``. @@ -2093,9 +1779,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): instantiation. """ -docdict[ - "image_interp_topomap" -] = """ +docdict["image_interp_topomap"] = """ image_interp : str The image interpolation to be used. Options are ``'cubic'`` (default) to use :class:`scipy.interpolate.CloughTocher2DInterpolator`, @@ -2103,9 +1787,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): ``'linear'`` to use :class:`scipy.interpolate.LinearNDInterpolator`. """ -docdict[ - "include_tmax" -] = """ +docdict["include_tmax"] = """ include_tmax : bool If True (default), include tmax. If False, exclude tmax (similar to how Python indexing typically works). @@ -2139,39 +1821,29 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): "sensors and methods of measurement." ) -docdict[ - "info" -] = f""" +docdict["info"] = f""" info : mne.Info | None {_info_base} """ -docdict[ - "info_not_none" -] = f""" +docdict["info_not_none"] = f""" info : mne.Info {_info_base} """ -docdict[ - "info_str" -] = f""" +docdict["info_str"] = f""" info : mne.Info | path-like {_info_base} If ``path-like``, it should be a :class:`str` or :class:`pathlib.Path` to a file with measurement information (e.g. :class:`mne.io.Raw`). """ -docdict[ - "int_order_maxwell" -] = """ +docdict["int_order_maxwell"] = """ int_order : int Order of internal component of spherical expansion. """ -docdict[ - "interaction_scene" -] = """ +docdict["interaction_scene"] = """ interaction : 'trackball' | 'terrain' How interactions with the scene via an input device (e.g., mouse or trackpad) modify the camera position. If ``'terrain'``, one axis is @@ -2181,9 +1853,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): some axes. """ -docdict[ - "interaction_scene_none" -] = """ +docdict["interaction_scene_none"] = """ interaction : 'trackball' | 'terrain' | None How interactions with the scene via an input device (e.g., mouse or trackpad) modify the camera position. If ``'terrain'``, one axis is @@ -2195,27 +1865,21 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): used. """ -docdict[ - "interp" -] = """ +docdict["interp"] = """ interp : str Either ``'hann'``, ``'cos2'`` (default), ``'linear'``, or ``'zero'``, the type of forward-solution interpolation to use between forward solutions at different head positions. """ -docdict[ - "interpolation_brain_time" -] = """ +docdict["interpolation_brain_time"] = """ interpolation : str | None Interpolation method (:class:`scipy.interpolate.interp1d` parameter). Must be one of ``'linear'``, ``'nearest'``, ``'zero'``, ``'slinear'``, ``'quadratic'`` or ``'cubic'``. """ -docdict[ - "inversion_bf" -] = """ +docdict["inversion_bf"] = """ inversion : 'single' | 'matrix' This determines how the beamformer deals with source spaces in "free" orientation. Such source spaces define three orthogonal dipoles at each @@ -2232,9 +1896,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): # %% # J -docdict[ - "joint_set_eeg_reference" -] = """ +docdict["joint_set_eeg_reference"] = """ joint : bool How to handle list-of-str ``ch_type``. If False (default), one projector is created per channel type. If True, one projector is created across @@ -2246,9 +1908,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): # %% # K -docdict[ - "keep_his_anonymize_info" -] = """ +docdict["keep_his_anonymize_info"] = """ keep_his : bool If ``True``, ``his_id`` of ``subject_info`` will **not** be overwritten. Defaults to ``False``. @@ -2257,35 +1917,27 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): anonymized. Use with caution. """ -docdict[ - "kit_elp" -] = """ +docdict["kit_elp"] = """ elp : path-like | array of shape (8, 3) | None Digitizer points representing the location of the fiducials and the marker coils with respect to the digitized head shape, or path to a file containing these points. """ -docdict[ - "kit_hsp" -] = """ +docdict["kit_hsp"] = """ hsp : path-like | array of shape (n_points, 3) | None Digitizer head shape points, or path to head shape file. If more than 10,000 points are in the head shape, they are automatically decimated. """ -docdict[ - "kit_mrk" -] = """ +docdict["kit_mrk"] = """ mrk : path-like | array of shape (5, 3) | list | None Marker points representing the location of the marker coils with respect to the MEG sensors, or path to a marker file. If list, all of the markers will be averaged together. """ -docdict[ - "kit_slope" -] = r""" +docdict["kit_slope"] = r""" slope : ``'+'`` | ``'-'`` How to interpret values on KIT trigger channels when synthesizing a Neuromag-style stim channel. With ``'+'``\, a positive slope (low-to-high) @@ -2293,9 +1945,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): is interpreted as an event. """ -docdict[ - "kit_stim" -] = r""" +docdict["kit_stim"] = r""" stim : list of int | ``'<'`` | ``'>'`` | None Channel-value correspondence when converting KIT trigger channels to a Neuromag-style stim channel. For ``'<'``\, the largest values are @@ -2305,25 +1955,19 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): generated. """ -docdict[ - "kit_stimcode" -] = """ +docdict["kit_stimcode"] = """ stim_code : ``'binary'`` | ``'channel'`` How to decode trigger values from stim channels. ``'binary'`` read stim channel events as binary code, 'channel' encodes channel number. """ -docdict[ - "kit_stimthresh" -] = """ +docdict["kit_stimthresh"] = """ stimthresh : float | None The threshold level for accepting voltage changes in KIT trigger channels as a trigger event. If None, stim must also be set to None. """ -docdict[ - "kwargs_fun" -] = """ +docdict["kwargs_fun"] = """ **kwargs : dict Additional keyword arguments to pass to ``fun``. """ @@ -2331,26 +1975,20 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): # %% # L -docdict[ - "l_freq" -] = """ +docdict["l_freq"] = """ l_freq : float | None For FIR filters, the lower pass-band edge; for IIR filters, the lower cutoff frequency. If None the data are only low-passed. """ -docdict[ - "l_freq_ecg_filter" -] = """ +docdict["l_freq_ecg_filter"] = """ l_freq : float Low pass frequency to apply to the ECG channel while finding events. h_freq : float High pass frequency to apply to the ECG channel while finding events. """ -docdict[ - "l_trans_bandwidth" -] = """ +docdict["l_trans_bandwidth"] = """ l_trans_bandwidth : float | str Width of the transition band at the low cut-off frequency in Hz (high pass or cutoff 1 in bandpass). Can be "auto" @@ -2361,16 +1999,12 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): Only used for ``method='fir'``. """ -docdict[ - "label_tc_el_returns" -] = """ +docdict["label_tc_el_returns"] = """ label_tc : array | list (or generator) of array, shape (n_labels[, n_orient], n_times) Extracted time course for each label and source estimate. """ -docdict[ - "labels_eltc" -] = """ +docdict["labels_eltc"] = """ labels : Label | BiHemiLabel | list | tuple | str If using a surface or mixed source space, this should be the :class:`~mne.Label`'s for which to extract the time course. @@ -2387,18 +2021,14 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): Support for volume source estimates. """ -docdict[ - "layout_spectrum_plot_topo" -] = """\ +docdict["layout_spectrum_plot_topo"] = """\ layout : instance of Layout | None Layout instance specifying sensor positions (does not need to be specified for Neuromag data). If ``None`` (default), the layout is inferred from the data. """ -docdict[ - "line_alpha_plot_psd" -] = """\ +docdict["line_alpha_plot_psd"] = """\ line_alpha : float | None Alpha for the PSD line. Can be None (default) to use 1.0 when ``average=True`` and 0.1 when ``average=False``. @@ -2425,9 +2055,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): docdict["long_format_df_spe"] = _long_format_df_base.format(*spe) docdict["long_format_df_stc"] = _long_format_df_base.format(*stc) -docdict[ - "loose" -] = """ +docdict["loose"] = """ loose : float | 'auto' | dict Value that weights the source variances of the dipole components that are parallel (tangential) to the cortical surface. Can be: @@ -2446,9 +2074,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): # %% # M -docdict[ - "mag_scale_maxwell" -] = """ +docdict["mag_scale_maxwell"] = """ mag_scale : float | str The magenetometer scale-factor used to bring the magnetometers to approximately the same order of magnitude as the gradiometers @@ -2458,9 +2084,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): 59.5 for VectorView). """ -docdict[ - "mapping_rename_channels_duplicates" -] = """ +docdict["mapping_rename_channels_duplicates"] = """ mapping : dict | callable A dictionary mapping the old channel to a new channel name e.g. ``{'EEG061' : 'EEG161'}``. Can also be a callable function @@ -2490,9 +2114,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): "statistical test of the data reaches significance)", ) -docdict[ - "mask_params_topomap" -] = """ +docdict["mask_params_topomap"] = """ mask_params : dict | None Additional plotting parameters for plotting significant sensors. Default (None) equals:: @@ -2509,9 +2131,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): shape="(n_channels,)", shape_appendix="(s)", example="" ) -docdict[ - "match_alias" -] = """ +docdict["match_alias"] = """ match_alias : bool | dict Whether to use a lookup table to match unrecognized channel location names to their known aliases. If True, uses the mapping in @@ -2522,18 +2142,14 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): .. versionadded:: 0.23 """ -docdict[ - "match_case" -] = """ +docdict["match_case"] = """ match_case : bool If True (default), channel name matching will be case sensitive. .. versionadded:: 0.20 """ -docdict[ - "max_dist_ieeg" -] = """ +docdict["max_dist_ieeg"] = """ max_dist : float The maximum distance to project a sensor to the pial surface in meters. Sensors that are greater than this distance from the pial surface will @@ -2541,17 +2157,13 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): flat brain. """ -docdict[ - "max_iter_multitaper" -] = """ +docdict["max_iter_multitaper"] = """ max_iter : int Maximum number of iterations to reach convergence when combining the tapered spectra with adaptive weights (see argument ``adaptive``). This argument has not effect if ``adaptive`` is set to ``False``.""" -docdict[ - "max_step_clust" -] = """ +docdict["max_step_clust"] = """ max_step : int Maximum distance between samples along the second axis of ``X`` to be considered adjacent (typically the second axis is the "time" dimension). @@ -2562,9 +2174,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): :func:`mne.stats.combine_adjacency`). """ -docdict[ - "measure" -] = """ +docdict["measure"] = """ measure : 'zscore' | 'correlation' Which method to use for finding outliers among the components: @@ -2577,9 +2187,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): .. versionadded:: 0.21""" -docdict[ - "meg" -] = """ +docdict["meg"] = """ meg : str | list | dict | bool | None Can be "helmet", "sensors" or "ref" to show the MEG helmet, sensors or reference sensors respectively, or a combination like @@ -2591,9 +2199,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): Added support for specifying alpha values as a dict. """ -docdict[ - "metadata_epochs" -] = """ +docdict["metadata_epochs"] = """ metadata : instance of pandas.DataFrame | None A :class:`pandas.DataFrame` specifying metadata about each epoch. If given, ``len(metadata)`` must equal ``len(events)``. The DataFrame @@ -2607,17 +2213,13 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): .. versionadded:: 0.16 """ -docdict[ - "method_fir" -] = """ +docdict["method_fir"] = """ method : str ``'fir'`` will use overlap-add FIR filtering, ``'iir'`` will use IIR forward-backward filtering (via :func:`~scipy.signal.filtfilt`). """ -docdict[ - "method_kw_psd" -] = """\ +docdict["method_kw_psd"] = """\ **method_kw Additional keyword arguments passed to the spectral estimation function (e.g., ``n_fft, n_overlap, n_per_seg, average, window`` @@ -2643,16 +2245,12 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): docdict["method_psd"] = _method_psd.format("", "") docdict["method_psd_auto"] = _method_psd.format(" | ``'auto'``", "") -docdict[ - "mode_eltc" -] = """ +docdict["mode_eltc"] = """ mode : str Extraction mode, see Notes. """ -docdict[ - "mode_pctf" -] = """ +docdict["mode_pctf"] = """ mode : None | 'mean' | 'max' | 'svd' Compute summary of PSFs/CTFs across all indices specified in 'idx'. Can be: @@ -2666,9 +2264,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): n_comp first SVD components. """ -docdict[ - "montage" -] = """ +docdict["montage"] = """ montage : None | str | DigMontage A montage containing channel positions. If a string or :class:`~mne.channels.DigMontage` is @@ -2682,9 +2278,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): docdict["montage_types"] = """EEG/sEEG/ECoG/DBS/fNIRS""" -docdict[ - "montage_units" -] = """ +docdict["montage_units"] = """ montage_units : str Units that channel positions are represented in. Defaults to "mm" (millimeters), but can be any prefix + "m" combination (including just @@ -2693,22 +2287,16 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): .. versionadded:: 1.3 """ -docdict[ - "morlet_reference" -] = """ +docdict["morlet_reference"] = """ The Morlet wavelets follow the formulation in :footcite:t:`Tallon-BaudryEtAl1997`. """ -docdict[ - "moving" -] = """ +docdict["moving"] = """ moving : instance of SpatialImage The image to morph ("from" volume). """ -docdict[ - "mri_resolution_eltc" -] = """ +docdict["mri_resolution_eltc"] = """ mri_resolution : bool If True (default), the volume source space will be upsampled to the original MRI resolution via trilinear interpolation before the atlas values @@ -2722,17 +2310,13 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): # %% # N -docdict[ - "n_comp_pctf_n" -] = """ +docdict["n_comp_pctf_n"] = """ n_comp : int Number of PSF/CTF components to return for mode='max' or mode='svd'. Default n_comp=1. """ -docdict[ - "n_cycles_tfr" -] = """ +docdict["n_cycles_tfr"] = """ n_cycles : int | array of int, shape (n_freqs,) Number of cycles in the wavelet, either a fixed number or one per frequency. The number of cycles ``n_cycles`` and the frequencies of @@ -2741,9 +2325,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): and about time and frequency smoothing. """ -docdict[ - "n_jobs" -] = """\ +docdict["n_jobs"] = """\ n_jobs : int | None The number of jobs to run in parallel. If ``-1``, it is set to the number of CPU cores. Requires the :mod:`joblib` package. @@ -2753,25 +2335,19 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): value for ``n_jobs``. """ -docdict[ - "n_jobs_cuda" -] = """ +docdict["n_jobs_cuda"] = """ n_jobs : int | str Number of jobs to run in parallel. Can be ``'cuda'`` if ``cupy`` is installed properly. """ -docdict[ - "n_jobs_fir" -] = """ +docdict["n_jobs_fir"] = """ n_jobs : int | str Number of jobs to run in parallel. Can be ``'cuda'`` if ``cupy`` is installed properly and ``method='fir'``. """ -docdict[ - "n_pca_components_apply" -] = """ +docdict["n_pca_components_apply"] = """ n_pca_components : int | float | None The number of PCA components to be kept, either absolute (int) or fraction of the explained variance (float). If None (default), @@ -2779,24 +2355,18 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): in 0.23 all components will be used. """ -docdict[ - "n_permutations_clust_all" -] = """ +docdict["n_permutations_clust_all"] = """ n_permutations : int | 'all' The number of permutations to compute. Can be 'all' to perform an exact test. """ -docdict[ - "n_permutations_clust_int" -] = """ +docdict["n_permutations_clust_int"] = """ n_permutations : int The number of permutations to compute. """ -docdict[ - "n_proj_vectors" -] = """ +docdict["n_proj_vectors"] = """ n_grad : int | float between ``0`` and ``1`` Number of vectors for gradiometers. Either an integer or a float between 0 and 1 to select the number of vectors to explain the cumulative variance greater than @@ -2811,18 +2381,14 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): ``n_eeg``. """ -docdict[ - "names_topomap" -] = """\ +docdict["names_topomap"] = """\ names : None | list Labels for the sensors. If a :class:`list`, labels should correspond to the order of channels in ``data``. If ``None`` (default), no channel names are plotted. """ -docdict[ - "nirx_notes" -] = """ +docdict["nirx_notes"] = """ This function has only been tested with NIRScout and NIRSport devices, and with the NIRStar software version 15 and above and Aurora software 2021 and above. @@ -2836,9 +2402,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): saturated data. """ -docdict[ - "niter" -] = """ +docdict["niter"] = """ niter : dict | tuple | None For each phase of the volume registration, ``niter`` is the number of iterations per successive stage of optimization. If a tuple is @@ -2857,9 +2421,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): sdr=(5, 5, 3)) """ -docdict[ - "norm_pctf" -] = """ +docdict["norm_pctf"] = """ norm : None | 'max' | 'norm' Whether and how to normalise the PSFs and CTFs. This will be applied before computing summaries as specified in 'mode'. @@ -2870,24 +2432,18 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): * 'norm' : Normalize to maximum norm across all PSFs/CTFs. """ -docdict[ - "normalization" -] = """normalization : 'full' | 'length' +docdict["normalization"] = """normalization : 'full' | 'length' Normalization strategy. If "full", the PSD will be normalized by the sampling rate as well as the length of the signal (as in :ref:`Nitime `). Default is ``'length'``.""" -docdict[ - "normalize_psd_topo" -] = """ +docdict["normalize_psd_topo"] = """ normalize : bool If True, each band will be divided by the total power. Defaults to False. """ -docdict[ - "notes_2d_backend" -] = """\ +docdict["notes_2d_backend"] = """\ MNE-Python provides two different backends for browsing plots (i.e., :meth:`raw.plot()`, :meth:`epochs.plot()`, and :meth:`ica.plot_sources()`). One is @@ -2915,9 +2471,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): docdict["notes_plot_*_psd_func"] = _notes_plot_psd.format("function") docdict["notes_plot_psd_meth"] = _notes_plot_psd.format("method") -docdict[ - "notes_spectrum_array" -] = """ +docdict["notes_spectrum_array"] = """ It is assumed that the data passed in represent spectral *power* (not amplitude, phase, model coefficients, etc) and downstream methods (such as :meth:`~mne.time_frequency.SpectrumArray.plot`) assume power data. If you pass in @@ -2925,27 +2479,21 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): other things may also not work or be incorrect). """ -docdict[ - "notes_tmax_included_by_default" -] = """ +docdict["notes_tmax_included_by_default"] = """ Unlike Python slices, MNE time intervals by default include **both** their end points; ``crop(tmin, tmax)`` returns the interval ``tmin <= t <= tmax``. Pass ``include_tmax=False`` to specify the half-open interval ``tmin <= t < tmax`` instead. """ -docdict[ - "npad" -] = """ +docdict["npad"] = """ npad : int | str Amount to pad the start and end of the data. Can also be ``"auto"`` to use a padding that will result in a power-of-two size (can be much faster). """ -docdict[ - "nrows_ncols_ica_components" -] = """ +docdict["nrows_ncols_ica_components"] = """ nrows, ncols : int | 'auto' The number of rows and columns of topographies to plot. If both ``nrows`` and ``ncols`` are ``'auto'``, will plot up to 20 components in a 5×4 grid, @@ -2956,9 +2504,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): ``nrows='auto', ncols='auto'``. """ -docdict[ - "nrows_ncols_topomap" -] = """ +docdict["nrows_ncols_topomap"] = """ nrows, ncols : int | 'auto' The number of rows and columns of topographies to plot. If either ``nrows`` or ``ncols`` is ``'auto'``, the necessary number will be inferred. Defaults @@ -2968,9 +2514,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): # %% # O -docdict[ - "offset_decim" -] = """ +docdict["offset_decim"] = """ offset : int Apply an offset to where the decimation starts relative to the sample corresponding to t=0. The offset is in samples at the @@ -2979,9 +2523,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): .. versionadded:: 0.12 """ -docdict[ - "on_baseline_ica" -] = """ +docdict["on_baseline_ica"] = """ on_baseline : str How to handle baseline-corrected epochs or evoked data. Can be ``'raise'`` to raise an error, ``'warn'`` (default) to emit a @@ -2990,9 +2532,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): .. versionadded:: 1.2 """ -docdict[ - "on_defects" -] = """ +docdict["on_defects"] = """ on_defects : 'raise' | 'warn' | 'ignore' What to do if the surface is found to have topological defects. Can be ``'raise'`` (default) to raise an error, ``'warn'`` to emit a @@ -3003,9 +2543,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): fail irrespective of this parameter. """ -docdict[ - "on_header_missing" -] = """ +docdict["on_header_missing"] = """ on_header_missing : str Can be ``'raise'`` (default) to raise an error, ``'warn'`` to emit a warning, or ``'ignore'`` to ignore when the FastSCAN header is missing. @@ -3018,9 +2556,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): warning, or ``'ignore'`` to ignore when""" -docdict[ - "on_mismatch_info" -] = f""" +docdict["on_mismatch_info"] = f""" on_mismatch : 'raise' | 'warn' | 'ignore' {_on_missing_base} the device-to-head transformation differs between instances. @@ -3028,27 +2564,21 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): .. versionadded:: 0.24 """ -docdict[ - "on_missing_ch_names" -] = f""" +docdict["on_missing_ch_names"] = f""" on_missing : 'raise' | 'warn' | 'ignore' {_on_missing_base} entries in ch_names are not present in the raw instance. .. versionadded:: 0.23.0 """ -docdict[ - "on_missing_chpi" -] = f""" +docdict["on_missing_chpi"] = f""" on_missing : 'raise' | 'warn' | 'ignore' {_on_missing_base} no cHPI information can be found. If ``'ignore'`` or ``'warn'``, all return values will be empty arrays or ``None``. If ``'raise'``, an exception will be raised. """ -docdict[ - "on_missing_epochs" -] = """ +docdict["on_missing_epochs"] = """ on_missing : 'raise' | 'warn' | 'ignore' What to do if one or several event ids are not found in the recording. Valid keys are 'raise' | 'warn' | 'ignore' @@ -3060,9 +2590,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): automatically generated irrespective of this parameter. """ -docdict[ - "on_missing_events" -] = f""" +docdict["on_missing_events"] = f""" on_missing : 'raise' | 'warn' | 'ignore' {_on_missing_base} event numbers from ``event_id`` are missing from :term:`events`. When numbers from :term:`events` are missing from @@ -3072,32 +2600,24 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): .. versionadded:: 0.21 """ -docdict[ - "on_missing_fiducials" -] = f""" +docdict["on_missing_fiducials"] = f""" on_missing : 'raise' | 'warn' | 'ignore' {_on_missing_base} some necessary fiducial points are missing. """ -docdict[ - "on_missing_fwd" -] = f""" +docdict["on_missing_fwd"] = f""" on_missing : 'raise' | 'warn' | 'ignore' {_on_missing_base} ``stc`` has vertices that are not in ``fwd``. """ -docdict[ - "on_missing_montage" -] = f""" +docdict["on_missing_montage"] = f""" on_missing : 'raise' | 'warn' | 'ignore' {_on_missing_base} channels have missing coordinates. .. versionadded:: 0.20.1 """ -docdict[ - "on_rank_mismatch" -] = """ +docdict["on_rank_mismatch"] = """ on_rank_mismatch : str If an explicit MEG value is passed, what to do when it does not match an empirically computed rank (only used for covariances). @@ -3107,18 +2627,14 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): .. versionadded:: 0.23 """ -docdict[ - "on_split_missing" -] = f""" +docdict["on_split_missing"] = f""" on_split_missing : str {_on_missing_base} split file is missing. .. versionadded:: 0.22 """ -docdict[ - "ordered" -] = """ +docdict["ordered"] = """ ordered : bool If True (default False), ensure that the order of the channels in the modified instance matches the order of ``ch_names``. @@ -3128,9 +2644,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): The default changed from False in 1.4 to True in 1.5. """ -docdict[ - "origin_maxwell" -] = """ +docdict["origin_maxwell"] = """ origin : array-like, shape (3,) | str Origin of internal and external multipolar moment space in meters. The default is ``'auto'``, which means ``(0., 0., 0.)`` when @@ -3142,9 +2656,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): options or specifying the origin manually. """ -docdict[ - "out_type_clust" -] = """ +docdict["out_type_clust"] = """ out_type : 'mask' | 'indices' Output format of clusters within a list. If ``'mask'``, returns a list of boolean arrays, @@ -3157,9 +2669,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): Default is ``'indices'``. """ -docdict[ - "outlines_topomap" -] = """ +docdict["outlines_topomap"] = """ outlines : 'head' | dict | None The outlines to be drawn. If 'head', the default head scheme will be drawn. If dict, each key refers to a tuple of x and y positions, the values @@ -3170,9 +2680,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): Defaults to 'head'. """ -docdict[ - "overview_mode" -] = """ +docdict["overview_mode"] = """ overview_mode : str | None Can be "channels", "empty", or "hidden" to set the overview bar mode for the ``'qt'`` backend. If None (default), the config option @@ -3180,9 +2688,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): if it's not found. """ -docdict[ - "overwrite" -] = """ +docdict["overwrite"] = """ overwrite : bool If True (default False), overwrite the destination file if it exists. @@ -3208,26 +2714,20 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): """ ) -docdict[ - "pca_vars_pctf" -] = """ +docdict["pca_vars_pctf"] = """ pca_vars : array, shape (n_comp,) | list of array The explained variances of the first n_comp SVD components across the PSFs/CTFs for the specified vertices. Arrays for multiple labels are returned as list. Only returned if ``mode='svd'`` and ``return_pca_vars=True``. """ -docdict[ - "per_sample_metric" -] = """ +docdict["per_sample_metric"] = """ per_sample : bool If True the metric is computed for each sample separately. If False, the metric is spatio-temporal. """ -docdict[ - "phase" -] = """ +docdict["phase"] = """ phase : str Phase of the filter. When ``method='fir'``, symmetric linear-phase FIR filters are constructed, @@ -3247,9 +2747,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): .. versionadded:: 0.13 """ -docdict[ - "physical_range_export_params" -] = """ +docdict["physical_range_export_params"] = """ physical_range : str | tuple The physical range of the data. If 'auto' (default), then it will infer the physical min and max from the data itself, @@ -3283,9 +2781,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): """ ) -docdict[ - "pick_ori_bf" -] = """ +docdict["pick_ori_bf"] = """ pick_ori : None | str For forward solutions with fixed orientation, None (default) must be used and a scalar beamformer is computed. For free-orientation forward @@ -3307,9 +2803,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): + _pick_ori_novec ) -docdict[ - "pick_types_params" -] = """ +docdict["pick_types_params"] = """ meg : bool | str If True include MEG channels. If string it can be 'mag', 'grad', 'planar1' or 'planar2' to select only magnetometers, all @@ -3423,9 +2917,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): f"{picks_base} good data channels {noref}" ) docdict["picks_header"] = _picks_header -docdict[ - "picks_ica" -] = """ +docdict["picks_ica"] = """ picks : int | list of int | slice | None Indices of the independent components (ICs) to visualize. If an integer, represents the index of the IC to pick. @@ -3434,24 +2926,18 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): IC: ``ICA001``. ``None`` will pick all independent components in the order fitted. """ -docdict[ - "picks_nostr" -] = f"""picks : list | slice | None +docdict["picks_nostr"] = f"""picks : list | slice | None {_picks_desc} {_picks_int} None (default) will pick all channels. {reminder_nostr}""" -docdict[ - "picks_plot_projs_joint_trace" -] = f"""\ +docdict["picks_plot_projs_joint_trace"] = f"""\ picks_trace : {_picks_types} Channels to show alongside the projected time courses. Typically these are the ground-truth channels for an artifact (e.g., ``'eog'`` or ``'ecg'``). {_picks_int} {_picks_str} no channels. """ -docdict[ - "pipeline" -] = """ +docdict["pipeline"] = """ pipeline : str | tuple The volume registration steps to perform (a ``str`` for a single step, or ``tuple`` for a set of sequential steps). The following steps can be @@ -3490,9 +2976,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): the SDR step. """ -docdict[ - "plot_psd_doc" -] = """\ +docdict["plot_psd_doc"] = """\ Plot power or amplitude spectra. Separate plots are drawn for each channel type. When the data have been @@ -3512,16 +2996,12 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): docdict["pos_topomap"] = _pos_topomap.format(" | instance of Info") docdict["pos_topomap_psd"] = _pos_topomap.format("") -docdict[ - "position" -] = """ +docdict["position"] = """ position : int The position for the progress bar. """ -docdict[ - "precompute" -] = """ +docdict["precompute"] = """ precompute : bool | str Whether to load all data (not just the visible portion) into RAM and apply preprocessing (e.g., projectors) to the full data array in a separate @@ -3536,9 +3016,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): Support for the MNE_BROWSER_PRECOMPUTE config variable. """ -docdict[ - "preload" -] = """ +docdict["preload"] = """ preload : bool or str (default False) Preload data into memory for data manipulation and faster indexing. If True, the data will be preloaded into memory (fast, requires @@ -3546,9 +3024,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): file name of a memory-mapped file which is used to store the data on the hard drive (slower, requires less memory).""" -docdict[ - "preload_concatenate" -] = """ +docdict["preload_concatenate"] = """ preload : bool, str, or None (default None) Preload data into memory for data manipulation and faster indexing. If True, the data will be preloaded into memory (fast, requires @@ -3559,9 +3035,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): of the instances passed in. """ -docdict[ - "proj_epochs" -] = """ +docdict["proj_epochs"] = """ proj : bool | 'delayed' Apply SSP projection vectors. If proj is 'delayed' and reject is not None the single epochs will be projected before the rejection @@ -3575,9 +3049,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): recommended value if SSPs are not used for cleaning the data. """ -docdict[ - "proj_plot" -] = """ +docdict["proj_plot"] = """ proj : bool | 'interactive' | 'reconstruct' If true SSP projections are applied before display. If 'interactive', a check box for reversible selection of SSP projection vectors will @@ -3589,17 +3061,13 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): Support for 'reconstruct' was added. """ -docdict[ - "proj_psd" -] = """\ +docdict["proj_psd"] = """\ proj : bool Whether to apply SSP projection vectors before spectral estimation. Default is ``False``. """ -docdict[ - "projection_set_eeg_reference" -] = """ +docdict["projection_set_eeg_reference"] = """ projection : bool If ``ref_channels='average'`` this argument specifies if the average reference should be computed as a projection (True) or not @@ -3611,16 +3079,12 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): must be set to ``False`` (the default in this case). """ -docdict[ - "projs" -] = """ +docdict["projs"] = """ projs : list of Projection List of computed projection vectors. """ -docdict[ - "projs_report" -] = """ +docdict["projs_report"] = """ projs : bool | None Whether to add SSP projector plots if projectors are present in the data. If ``None``, use ``projs`` from `~mne.Report` creation. @@ -3629,9 +3093,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): # %% # R -docdict[ - "random_state" -] = """ +docdict["random_state"] = """ random_state : None | int | instance of ~numpy.random.RandomState A seed for the NumPy random number generator (RNG). If ``None`` (default), the seed will be obtained from the operating system @@ -3690,24 +3152,18 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): docdict["rank_info"] = _rank_base + "\n The default is ``'info'``." docdict["rank_none"] = _rank_base + "\n The default is ``None``." -docdict[ - "raw_epochs" -] = """ +docdict["raw_epochs"] = """ raw : Raw object An instance of `~mne.io.Raw`. """ -docdict[ - "raw_sfreq" -] = """ +docdict["raw_sfreq"] = """ raw_sfreq : float The original Raw object sampling rate. If None, then it is set to ``info['sfreq']``. """ -docdict[ - "reduce_rank" -] = """ +docdict["reduce_rank"] = """ reduce_rank : bool If True, the rank of the denominator of the beamformer formula (i.e., during pseudo-inversion) will be reduced by one for each spatial location. @@ -3719,18 +3175,14 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): ``pick='max_power'`` with weight normalization). """ -docdict[ - "ref_channels" -] = """ +docdict["ref_channels"] = """ ref_channels : str | list of str Name of the electrode(s) which served as the reference in the recording. If a name is provided, a corresponding channel is added and its data is set to 0. This is useful for later re-referencing. """ -docdict[ - "ref_channels_set_eeg_reference" -] = """ +docdict["ref_channels_set_eeg_reference"] = """ ref_channels : list of str | str Can be: @@ -3742,16 +3194,12 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): the data """ -docdict[ - "reg_affine" -] = """ +docdict["reg_affine"] = """ reg_affine : ndarray of float, shape (4, 4) The affine that registers one volume to another. """ -docdict[ - "regularize_maxwell_reg" -] = """ +docdict["regularize_maxwell_reg"] = """ regularize : str | None Basis regularization type, must be ``"in"`` or None. ``"in"`` is the same algorithm as the ``-regularize in`` option in @@ -3768,18 +3216,14 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): docdict["reject_by_annotation_all"] = _reject_by_annotation_base -docdict[ - "reject_by_annotation_epochs" -] = """ +docdict["reject_by_annotation_epochs"] = """ reject_by_annotation : bool Whether to reject based on annotations. If ``True`` (default), epochs overlapping with segments whose description begins with ``'bad'`` are rejected. If ``False``, no rejection based on annotations is performed. """ -docdict[ - "reject_by_annotation_psd" -] = """\ +docdict["reject_by_annotation_psd"] = """\ reject_by_annotation : bool Whether to omit bad spans of data before spectral estimation. If ``True``, spans with annotations whose description begins with @@ -3817,18 +3261,14 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): difference will be preserved. """ -docdict[ - "reject_drop_bad" -] = f""" +docdict["reject_drop_bad"] = f""" reject : dict | str | None {_reject_common} If ``reject`` is ``None``, no rejection is performed. If ``'existing'`` (default), then the rejection parameters set at instantiation are used. """ -docdict[ - "reject_epochs" -] = f""" +docdict["reject_epochs"] = f""" reject : dict | None {_reject_common} .. note:: To constrain the time period used for estimation of signal @@ -3837,17 +3277,13 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): If ``reject`` is ``None`` (default), no rejection is performed. """ -docdict[ - "remove_dc" -] = """ +docdict["remove_dc"] = """ remove_dc : bool If ``True``, the mean is subtracted from each segment before computing its spectrum. """ -docdict[ - "replace_report" -] = """ +docdict["replace_report"] = """ replace : bool If ``True``, content already present that has the same ``title`` and ``section`` will be replaced. Defaults to ``False``, which will cause @@ -3855,16 +3291,12 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): already exists. """ -docdict[ - "res_topomap" -] = """ +docdict["res_topomap"] = """ res : int The resolution of the topomap image (number of pixels along each side). """ -docdict[ - "return_pca_vars_pctf" -] = """ +docdict["return_pca_vars_pctf"] = """ return_pca_vars : bool Whether or not to return the explained variances across the specified vertices for individual SVD components. This is only valid if @@ -3872,9 +3304,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): Default return_pca_vars=False. """ -docdict[ - "roll" -] = """ +docdict["roll"] = """ roll : float | None The roll of the camera rendering the view in degrees. """ @@ -3882,9 +3312,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): # %% # S -docdict[ - "saturated" -] = """saturated : str +docdict["saturated"] = """saturated : str Replace saturated segments of data with NaNs, can be: ``"ignore"`` @@ -3903,9 +3331,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): .. versionadded:: 0.24 """ -docdict[ - "scalings" -] = """ +docdict["scalings"] = """ scalings : 'auto' | dict | None Scaling factors for the traces. If a dictionary where any value is ``'auto'``, the scaling factor is set to match the 99.5th @@ -3926,26 +3352,20 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): positive direction and 20 µV in the negative direction). """ -docdict[ - "scalings_df" -] = """ +docdict["scalings_df"] = """ scalings : dict | None Scaling factor applied to the channels picked. If ``None``, defaults to ``dict(eeg=1e6, mag=1e15, grad=1e13)`` — i.e., converts EEG to µV, magnetometers to fT, and gradiometers to fT/cm. """ -docdict[ - "scalings_topomap" -] = """ +docdict["scalings_topomap"] = """ scalings : dict | float | None The scalings of the channel types to be applied for plotting. If None, defaults to ``dict(eeg=1e6, grad=1e13, mag=1e15)``. """ -docdict[ - "scoring" -] = """ +docdict["scoring"] = """ scoring : callable | str | None Score function (or loss function) with signature ``score_func(y, y_pred, **kwargs)``. @@ -3955,17 +3375,13 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): ``scoring=sklearn.metrics.roc_auc_score``). """ -docdict[ - "sdr_morph" -] = """ +docdict["sdr_morph"] = """ sdr_morph : instance of dipy.align.DiffeomorphicMap The class that applies the the symmetric diffeomorphic registration (SDR) morph. """ -docdict[ - "section_report" -] = """ +docdict["section_report"] = """ section : str | None The name of the section (or content block) to add the content to. This feature is useful for grouping multiple related content elements @@ -3977,9 +3393,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): .. versionadded:: 1.1 """ -docdict[ - "seed" -] = """ +docdict["seed"] = """ seed : None | int | instance of ~numpy.random.RandomState A seed for the NumPy random number generator (RNG). If ``None`` (default), the seed will be obtained from the operating system @@ -3989,24 +3403,18 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): the RNG with a defined state. """ -docdict[ - "seeg" -] = """ +docdict["seeg"] = """ seeg : bool If True (default), show sEEG electrodes. """ -docdict[ - "selection" -] = """ +docdict["selection"] = """ selection : iterable | None Iterable of indices of selected epochs. If ``None``, will be automatically generated, corresponding to all non-zero events. """ -docdict[ - "sensor_colors" -] = """ +docdict["sensor_colors"] = """ sensor_colors : array-like of color | dict | None Colors to use for the sensor glyphs. Can be None (default) to use default colors. A dict should provide the colors (values) for each channel type (keys), e.g.:: @@ -4020,9 +3428,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): shape ``(n_eeg, 3)`` or ``(n_eeg, 4)``. """ -docdict[ - "sensors_topomap" -] = """ +docdict["sensors_topomap"] = """ sensors : bool | str Whether to add markers for sensor locations. If :class:`str`, should be a valid matplotlib format string (e.g., ``'r+'`` for red plusses, see the @@ -4030,9 +3436,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): default), black circles will be used. """ -docdict[ - "set_eeg_reference_see_also_notes" -] = """ +docdict["set_eeg_reference_see_also_notes"] = """ See Also -------- mne.set_bipolar_reference : Convenience function for creating bipolar @@ -4082,16 +3486,12 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): .. footbibliography:: """ -docdict[ - "show" -] = """\ +docdict["show"] = """\ show : bool Show the figure if ``True``. """ -docdict[ - "show_names_topomap" -] = """ +docdict["show_names_topomap"] = """ show_names : bool | callable If ``True``, show channel names next to each sensor marker. If callable, channel names will be formatted using the callable; e.g., to @@ -4100,18 +3500,14 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): non-masked sensor names will be shown. """ -docdict[ - "show_scalebars" -] = """ +docdict["show_scalebars"] = """ show_scalebars : bool Whether to show scale bars when the plot is initialized. Can be toggled after initialization by pressing :kbd:`s` while the plot window is focused. Default is ``True``. """ -docdict[ - "show_scrollbars" -] = """ +docdict["show_scrollbars"] = """ show_scrollbars : bool Whether to show scrollbars when the plot is initialized. Can be toggled after initialization by pressing :kbd:`z` ("zen mode") while the plot @@ -4120,9 +3516,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): .. versionadded:: 0.19.0 """ -docdict[ - "show_traces" -] = """ +docdict["show_traces"] = """ show_traces : bool | str | float If True, enable interactive picking of a point on the surface of the brain and plot its time course. @@ -4136,16 +3530,12 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): .. versionadded:: 0.20.0 """ -docdict[ - "size_topomap" -] = """ +docdict["size_topomap"] = """ size : float Side length of each subplot in inches. """ -docdict[ - "skip_by_annotation" -] = """ +docdict["skip_by_annotation"] = """ skip_by_annotation : str | list of str If a string (or list of str), any annotation segment that begins with the given string will not be included in filtering, and @@ -4157,9 +3547,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): To disable, provide an empty list. Only used if ``inst`` is raw. """ -docdict[ - "skip_by_annotation_maxwell" -] = """ +docdict["skip_by_annotation_maxwell"] = """ skip_by_annotation : str | list of str If a string (or list of str), any annotation segment that begins with the given string will not be included in filtering, and @@ -4171,24 +3559,18 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): To disable, provide an empty list. """ -docdict[ - "smooth" -] = """ +docdict["smooth"] = """ smooth : float in [0, 1) The smoothing factor to be applied. Default 0 is no smoothing. """ -docdict[ - "spatial_colors_psd" -] = """\ +docdict["spatial_colors_psd"] = """\ spatial_colors : bool Whether to color spectrum lines by channel location. Ignored if ``average=True``. """ -docdict[ - "sphere_topomap_auto" -] = f"""\ +docdict["sphere_topomap_auto"] = f"""\ sphere : float | array-like | instance of ConductorModel | None | 'auto' | 'eeglab' The sphere parameters to use for the head outline. Can be array-like of shape (4,) to give the X/Y/Z origin and radius in meters, or a single float @@ -4205,17 +3587,13 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): .. versionchanged:: 1.1 Added ``'eeglab'`` option. """ -docdict[ - "splash" -] = """ +docdict["splash"] = """ splash : bool If True (default), a splash screen is shown during the application startup. Only applicable to the ``qt`` backend. """ -docdict[ - "split_naming" -] = """ +docdict["split_naming"] = """ split_naming : 'neuromag' | 'bids' When splitting files, append a filename partition with the appropriate naming schema: for ``'neuromag'``, a split file ``fname.fif`` will be named @@ -4223,16 +3601,12 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): it will be named ``fname_split-01.fif``, ``fname_split-02.fif``, etc. """ -docdict[ - "src_eltc" -] = """ +docdict["src_eltc"] = """ src : instance of SourceSpaces The source spaces for the source time courses. """ -docdict[ - "src_volume_options" -] = """ +docdict["src_volume_options"] = """ src : instance of SourceSpaces | None The source space corresponding to the source estimate. Only necessary if the STC is a volume or mixed source estimate. @@ -4263,9 +3637,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): entry. """ -docdict[ - "st_fixed_maxwell_only" -] = """ +docdict["st_fixed_maxwell_only"] = """ st_fixed : bool If True (default), do tSSS using the median head position during the ``st_duration`` window. This is the default behavior of MaxFilter @@ -4287,9 +3659,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): .. versionadded:: 0.12 """ -docdict[ - "standardize_names" -] = """ +docdict["standardize_names"] = """ standardize_names : bool If True, standardize MEG and EEG channel names to be ``'MEG ###'`` and ``'EEG ###'``. If False (default), native @@ -4307,48 +3677,36 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): docdict["stat_fun_clust_t"] = _stat_fun_clust_base.format("ttest_1samp_no_p") -docdict[ - "static" -] = """ +docdict["static"] = """ static : instance of SpatialImage The image to align with ("to" volume). """ -docdict[ - "stc_est_metric" -] = """ +docdict["stc_est_metric"] = """ stc_est : instance of (Vol|Mixed)SourceEstimate The source estimates containing estimated values e.g. obtained with a source imaging method. """ -docdict[ - "stc_metric" -] = """ +docdict["stc_metric"] = """ metric : float | array, shape (n_times,) The metric. float if per_sample is False, else array with the values computed for each time point. """ -docdict[ - "stc_plot_kwargs_report" -] = """ +docdict["stc_plot_kwargs_report"] = """ stc_plot_kwargs : dict Dictionary of keyword arguments to pass to :class:`mne.SourceEstimate.plot`. Only used when plotting in 3D mode. """ -docdict[ - "stc_true_metric" -] = """ +docdict["stc_true_metric"] = """ stc_true : instance of (Vol|Mixed)SourceEstimate The source estimates containing correct values. """ -docdict[ - "stcs_pctf" -] = """ +docdict["stcs_pctf"] = """ stcs : instance of SourceEstimate | list of instances of SourceEstimate The PSFs or CTFs as STC objects. All PSFs/CTFs will be returned as successive samples in STC objects, in the order they are specified @@ -4364,9 +3722,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): a VectorSourceEstimate object. """ -docdict[ - "std_err_by_event_type_returns" -] = """ +docdict["std_err_by_event_type_returns"] = """ std_err : instance of Evoked | list of Evoked The standard error over epochs. When ``by_event_type=True`` was specified, a list is returned containing a @@ -4375,9 +3731,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): dictionary. """ -docdict[ - "step_down_p_clust" -] = """ +docdict["step_down_p_clust"] = """ step_down_p : float To perform a step-down-in-jumps test, pass a p-value for clusters to exclude from each successive iteration. Default is zero, perform no @@ -4386,48 +3740,36 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): but costs computation time. """ -docdict[ - "subject" -] = """ +docdict["subject"] = """ subject : str The FreeSurfer subject name. """ -docdict[ - "subject_label" -] = """ +docdict["subject_label"] = """ subject : str | None Subject which this label belongs to. Should only be specified if it is not specified in the label. """ -docdict[ - "subject_none" -] = """ +docdict["subject_none"] = """ subject : str | None The FreeSurfer subject name. """ -docdict[ - "subject_optional" -] = """ +docdict["subject_optional"] = """ subject : str The FreeSurfer subject name. While not necessary, it is safer to set the subject parameter to avoid analysis errors. """ -docdict[ - "subjects_dir" -] = """ +docdict["subjects_dir"] = """ subjects_dir : path-like | None The path to the directory containing the FreeSurfer subjects reconstructions. If ``None``, defaults to the ``SUBJECTS_DIR`` environment variable. """ -docdict[ - "surface" -] = """surface : str +docdict["surface"] = """surface : str The surface along which to do the computations, defaults to ``'white'`` (the gray-white matter boundary). """ @@ -4435,9 +3777,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): # %% # T -docdict[ - "t_power_clust" -] = """ +docdict["t_power_clust"] = """ t_power : float Power to raise the statistical values (usually t-values) by before summing (sign will be retained). Note that ``t_power=0`` will give a @@ -4445,24 +3785,18 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): by its statistical score. """ -docdict[ - "t_window_chpi_t" -] = """ +docdict["t_window_chpi_t"] = """ t_window : float Time window to use to estimate the amplitudes, default is 0.2 (200 ms). """ -docdict[ - "tags_report" -] = """ +docdict["tags_report"] = """ tags : array-like of str | str Tags to add for later interactive filtering. Must not contain spaces. """ -docdict[ - "tail_clust" -] = """ +docdict["tail_clust"] = """ tail : int If tail is 1, the statistic is thresholded above threshold. If tail is -1, the statistic is thresholded below threshold. @@ -4470,9 +3804,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): the distribution. """ -docdict[ - "temporal_window_tfr_intro" -] = """ +docdict["temporal_window_tfr_intro"] = """ In spectrotemporal analysis (as with traditional fourier methods), the temporal and spectral resolution are interrelated: longer temporal windows allow more precise frequency estimates; shorter temporal windows "smear" @@ -4492,9 +3824,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): multitapers and wavelets `_. """ # noqa: E501 -docdict[ - "temporal_window_tfr_morlet_notes" -] = r""" +docdict["temporal_window_tfr_morlet_notes"] = r""" In MNE-Python, the length of the Morlet wavelet is affected by the arguments ``freqs`` and ``n_cycles``, which define the frequencies of interest and the number of cycles, respectively. For the time-frequency representation, @@ -4512,9 +3842,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): For more information on the Morlet wavelet, see :func:`mne.time_frequency.morlet`. """ -docdict[ - "temporal_window_tfr_multitaper_notes" -] = r""" +docdict["temporal_window_tfr_multitaper_notes"] = r""" In MNE-Python, the multitaper temporal window length is defined by the arguments ``freqs`` and ``n_cycles``, respectively defining the frequencies of interest and the number of cycles: :math:`T = \frac{\mathtt{n\_cycles}}{\mathtt{freqs}}` @@ -4539,26 +3867,16 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): defaulting to "auto" if it's not found.\ """ -docdict[ - "theme_3d" -] = """ +docdict["theme_3d"] = """ {theme} -""".format( - theme=_theme.format(config_option="MNE_3D_OPTION_THEME") -) +""".format(theme=_theme.format(config_option="MNE_3D_OPTION_THEME")) -docdict[ - "theme_pg" -] = """ +docdict["theme_pg"] = """ {theme} Only supported by the ``'qt'`` backend. -""".format( - theme=_theme.format(config_option="MNE_BROWSER_THEME") -) +""".format(theme=_theme.format(config_option="MNE_BROWSER_THEME")) -docdict[ - "thresh" -] = """ +docdict["thresh"] = """ thresh : None or float Not supported yet. If not None, values below thresh will not be visible. @@ -4582,9 +3900,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): f_test = ("an F-threshold", "an F-statistic") docdict["threshold_clust_f"] = _threshold_clust_base.format(*f_test) -docdict[ - "threshold_clust_f_notes" -] = """ +docdict["threshold_clust_f_notes"] = """ For computing a ``threshold`` based on a p-value, use the conversion from :meth:`scipy.stats.rv_continuous.ppf`:: @@ -4597,9 +3913,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): t_test = ("a t-threshold", "a t-statistic") docdict["threshold_clust_t"] = _threshold_clust_base.format(*t_test) -docdict[ - "threshold_clust_t_notes" -] = """ +docdict["threshold_clust_t_notes"] = """ For computing a ``threshold`` based on a p-value, use the conversion from :meth:`scipy.stats.rv_continuous.ppf`:: @@ -4611,9 +3925,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): For testing the lower tail (``tail=-1``), don't subtract ``pval`` from 1. """ -docdict[ - "time_bandwidth_tfr" -] = """ +docdict["time_bandwidth_tfr"] = """ time_bandwidth : float ``≥ 2.0`` Product between the temporal window length (in seconds) and the *full* frequency bandwidth (in Hz). This product can be seen as the surface of the @@ -4621,9 +3933,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): (thus the frequency resolution) and the number of good tapers. See notes for additional information.""" -docdict[ - "time_bandwidth_tfr_notes" -] = r""" +docdict["time_bandwidth_tfr_notes"] = r""" In MNE-Python's multitaper functions, the frequency bandwidth is additionally affected by the parameter ``time_bandwidth``. The ``n_cycles`` parameter determines the temporal window length based on the @@ -4659,9 +3969,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): example above, the half-frequency bandwidth is 2 Hz. """ -docdict[ - "time_format" -] = """ +docdict["time_format"] = """ time_format : 'float' | 'clock' Style of time labels on the horizontal axis. If ``'float'``, labels will be number of seconds from the start of the recording. If ``'clock'``, @@ -4689,9 +3997,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): ) docdict["time_format_df_raw"] = _time_format_df_base.format(_raw_tf) -docdict[ - "time_label" -] = """ +docdict["time_label"] = """ time_label : str | callable | None Format of the time label (a format string, a function that maps floating point time values to strings, or None for no label). The @@ -4699,69 +4005,51 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): is more than one time point. """ -docdict[ - "time_unit" -] = """\ +docdict["time_unit"] = """\ time_unit : str The units for the time axis, can be "s" (default) or "ms". """ -docdict[ - "time_viewer_brain_screenshot" -] = """ +docdict["time_viewer_brain_screenshot"] = """ time_viewer : bool If True, include time viewer traces. Only used if ``time_viewer=True`` and ``separate_canvas=False``. """ -docdict[ - "title_none" -] = """ +docdict["title_none"] = """ title : str | None The title of the generated figure. If ``None`` (default), no title is displayed. """ -docdict[ - "tmax_raw" -] = """ +docdict["tmax_raw"] = """ tmax : float End time of the raw data to use in seconds (cannot exceed data duration). """ -docdict[ - "tmin" -] = """ +docdict["tmin"] = """ tmin : scalar Time point of the first sample in data. """ -docdict[ - "tmin_epochs" -] = """ +docdict["tmin_epochs"] = """ tmin : float Start time before event. If nothing provided, defaults to 0. """ -docdict[ - "tmin_raw" -] = """ +docdict["tmin_raw"] = """ tmin : float Start time of the raw data to use in seconds (must be >= 0). """ -docdict[ - "tmin_tmax_psd" -] = """\ +docdict["tmin_tmax_psd"] = """\ tmin, tmax : float | None First and last times to include, in seconds. ``None`` uses the first or last time present in the data. Default is ``tmin=None, tmax=None`` (all times). """ -docdict[ - "tol_kind_rank" -] = """ +docdict["tol_kind_rank"] = """ tol_kind : str Can be: "absolute" (default) or "relative". Only used if ``tol`` is a float, because when ``tol`` is a string the mode is implicitly relative. @@ -4780,9 +4068,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): .. versionadded:: 0.21.0 """ -docdict[ - "tol_rank" -] = """ +docdict["tol_rank"] = """ tol : float | 'auto' Tolerance for singular values to consider non-zero in calculating the rank. The singular values are calculated @@ -4791,9 +4077,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): same thresholding as :func:`scipy.linalg.orth`. """ -docdict[ - "topomap_kwargs" -] = """ +docdict["topomap_kwargs"] = """ topomap_kwargs : dict | None Keyword arguments to pass to the topomap-generating functions. """ @@ -4803,26 +4087,18 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): during coregistration. Can also be ``'fsaverage'`` to use the built-in fsaverage transformation.""" -docdict[ - "trans" -] = f""" +docdict["trans"] = f""" trans : path-like | dict | instance of Transform | ``"fsaverage"`` | None {_trans_base} If trans is None, an identity matrix is assumed. """ -docdict[ - "trans_not_none" -] = """ +docdict["trans_not_none"] = """ trans : str | dict | instance of Transform %s -""" % ( - _trans_base, -) +""" % (_trans_base,) -docdict[ - "transparent" -] = """ +docdict["transparent"] = """ transparent : bool | None If True: use a linear transparency between fmin and fmid and make values below fmin fully transparent (symmetrically for @@ -4830,17 +4106,13 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): type. """ -docdict[ - "tstart_ecg" -] = """ +docdict["tstart_ecg"] = """ tstart : float Start ECG detection after ``tstart`` seconds. Useful when the beginning of the run is noisy. """ -docdict[ - "tstep" -] = """ +docdict["tstep"] = """ tstep : scalar Time step between successive samples in data. """ @@ -4848,18 +4120,14 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): # %% # U -docdict[ - "ui_event_name_source" -] = """ +docdict["ui_event_name_source"] = """ name : str The name of the event (same as its class name but in snake_case). source : matplotlib.figure.Figure | Figure3D The figure that published the event. """ -docdict[ - "uint16_codec" -] = """ +docdict["uint16_codec"] = """ uint16_codec : str | None If your set file contains non-ascii characters, sometimes reading it may fail and give rise to error message stating that "buffer is @@ -4868,9 +4136,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): can therefore help you solve this problem. """ -docdict[ - "units" -] = """ +docdict["units"] = """ units : str | dict | None Specify the unit(s) that the data should be returned in. If ``None`` (default), the data is returned in the @@ -4889,9 +4155,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): channel-type-specific default unit. """ -docdict[ - "units_edf_bdf_io" -] = """ +docdict["units_edf_bdf_io"] = """ units : dict | str The units of the channels as stored in the file. This argument is useful only if the units are missing from the original file. @@ -4910,17 +4174,13 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): "dict | ", "and ``scalings=None`` the unit is automatically determined, otherwise " ) -docdict[ - "use_cps" -] = """ +docdict["use_cps"] = """ use_cps : bool Whether to use cortical patch statistics to define normal orientations for surfaces (default True). """ -docdict[ - "use_cps_restricted" -] = """ +docdict["use_cps_restricted"] = """ use_cps : bool Whether to use cortical patch statistics to define normal orientations for surfaces (default True). @@ -4929,9 +4189,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): not in surface orientation, and ``pick_ori='normal'``. """ -docdict[ - "use_opengl" -] = """ +docdict["use_opengl"] = """ use_opengl : bool | None Whether to use OpenGL when rendering the plot (requires ``pyopengl``). May increase performance, but effect is dependent on system CPU and @@ -4946,9 +4204,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): # %% # V -docdict[ - "vector_pctf" -] = """ +docdict["vector_pctf"] = """ vector : bool Whether to return PSF/CTF as vector source estimate (3 values per location) or source estimate object (1 intensity value per location). @@ -4958,9 +4214,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): .. versionadded:: 1.2 """ -docdict[ - "verbose" -] = """ +docdict["verbose"] = """ verbose : bool | str | int | None Control verbosity of the logging output. If ``None``, use the default verbosity level. See the :ref:`logging documentation ` and @@ -4968,17 +4222,13 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): argument. """ -docdict[ - "vertices_volume" -] = """ +docdict["vertices_volume"] = """ vertices : list of array of int The indices of the dipoles in the source space. Should be a single array of shape (n_dipoles,) unless there are subvolumes. """ -docdict[ - "view" -] = """ +docdict["view"] = """ view : str | None The name of the view to show (e.g. "lateral"). Other arguments take precedence and modify the camera starting from the ``view``. @@ -4986,17 +4236,13 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): string shortcut options. """ -docdict[ - "view_layout" -] = """ +docdict["view_layout"] = """ view_layout : str Can be "vertical" (default) or "horizontal". When using "horizontal" mode, the PyVista backend must be used and hemi cannot be "split". """ -docdict[ - "views" -] = """ +docdict["views"] = """ views : str | list View to use. Using multiple views (list) is not supported for mpl backend. See :meth:`Brain.show_view ` for @@ -5032,9 +4278,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): ) docdict["vlim_plot_topomap_psd"] = _vlim_joint.format("topomap", _vlim_callable, "") -docdict[ - "vmin_vmax_topomap" -] = """ +docdict["vmin_vmax_topomap"] = """ vmin, vmax : float | callable | None Lower and upper bounds of the colormap, in the same units as the data. If ``vmin`` and ``vmax`` are both ``None``, they are set at ± the @@ -5048,9 +4292,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): # %% # W -docdict[ - "weight_norm" -] = """ +docdict["weight_norm"] = """ weight_norm : str | None Can be: @@ -5082,16 +4324,12 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): solution. """ -docdict[ - "window_psd" -] = """\ +docdict["window_psd"] = """\ window : str | float | tuple Windowing function to use. See :func:`scipy.signal.get_window`. """ -docdict[ - "window_resample" -] = """ +docdict["window_resample"] = """ window : str | tuple Frequency-domain window to use in resampling. See :func:`scipy.signal.resample`. @@ -5100,9 +4338,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): # %% # X -docdict[ - "xscale_plot_psd" -] = """\ +docdict["xscale_plot_psd"] = """\ xscale : 'linear' | 'log' Scale of the frequency axis. Default is ``'linear'``. """ @@ -5505,7 +4741,7 @@ def open_docs(kind=None, version=None): class _decorator: """Inject code or modify the docstring of a class, method, or function.""" - def __init__(self, extra): # noqa: D102 + def __init__(self, extra): self.kind = self.__class__.__name__ self.extra = extra self.msg = f"NOTE: {{}}() is a {self.kind} {{}}. {self.extra}." @@ -5625,7 +4861,7 @@ class legacy(_decorator): and in a sphinx warning box in the docstring. """ - def __init__(self, alt, extra=""): # noqa: D102 + def __init__(self, alt, extra=""): period = ". " if len(extra) else "" extra = f"New code should use {alt}{period}{extra}" super().__init__(extra=extra) diff --git a/mne/utils/progressbar.py b/mne/utils/progressbar.py index 94f595dd441..14429cb9033 100644 --- a/mne/utils/progressbar.py +++ b/mne/utils/progressbar.py @@ -55,7 +55,7 @@ def __init__( *, which_tqdm=None, **kwargs, - ): # noqa: D102 + ): # The following mimics this, but with configurable module to use # from ..externals.tqdm import auto import tqdm diff --git a/mne/viz/_3d.py b/mne/viz/_3d.py index 5f68fd0a46e..b67b9f49096 100644 --- a/mne/viz/_3d.py +++ b/mne/viz/_3d.py @@ -2187,8 +2187,7 @@ def link_brains(brains, time=True, camera=False, colorbar=True, picking=False): if _get_3d_backend() != "pyvistaqt": raise NotImplementedError( - "Expected 3d backend is pyvistaqt but" - " {} was given.".format(_get_3d_backend()) + f"Expected 3d backend is pyvistaqt but {_get_3d_backend()} was given." ) from ._brain import Brain, _LinkViewer diff --git a/mne/viz/backends/_utils.py b/mne/viz/backends/_utils.py index d613a909f67..e9e4ea439aa 100644 --- a/mne/viz/backends/_utils.py +++ b/mne/viz/backends/_utils.py @@ -73,8 +73,7 @@ def _check_color(color): ) else: raise TypeError( - "Expected type is `str` or iterable but " - "{} was given.".format(type(color)) + f"Expected type is `str` or iterable but {type(color)} was given." ) return color @@ -327,9 +326,7 @@ def _qt_get_stylesheet(theme): height: 16px; image: url("%(icons_path)s/toolbar_move_vertical@2x.png"); } -""" % dict( - icons_path=icons_path - ) +""" % dict(icons_path=icons_path) else: # Here we are on non-macOS (or on macOS but our sys theme does not # match the requested theme) diff --git a/mne/viz/epochs.py b/mne/viz/epochs.py index 20dbeed142c..c830570d457 100644 --- a/mne/viz/epochs.py +++ b/mne/viz/epochs.py @@ -468,8 +468,7 @@ def _validate_fig_and_axes(fig, axes, group_by, evoked, colorbar, clear=False): # `plot_image`, be forgiving of presence/absence of sensor inset axis. if len(fig.axes) not in (n_axes, n_axes + 1): raise ValueError( - '{}"fig" must contain {} axes, got {}.' - "".format(prefix, n_axes, len(fig.axes)) + f'{prefix}"fig" must contain {n_axes} axes, got {len(fig.axes)}.' ) if len(list(group_by)) != 1: raise ValueError( @@ -498,8 +497,7 @@ def _validate_fig_and_axes(fig, axes, group_by, evoked, colorbar, clear=False): if isinstance(axes, list): if len(axes) != n_axes: raise ValueError( - '{}"axes" must be length {}, got {}.' - "".format(prefix, n_axes, len(axes)) + f'{prefix}"axes" must be length {n_axes}, got {len(axes)}.' ) # for list of axes to work, must be only one group if len(list(group_by)) != 1: diff --git a/mne/viz/evoked.py b/mne/viz/evoked.py index ccbe48eabd4..3db8d745368 100644 --- a/mne/viz/evoked.py +++ b/mne/viz/evoked.py @@ -2475,8 +2475,7 @@ def _draw_axes_pce( ybounds = _trim_ticks(ax.get_yticks(), ymin, ymax)[[0, -1]] else: raise ValueError( - '"truncate_yaxis" must be bool or ' - '"auto", got {}'.format(truncate_yaxis) + f'"truncate_yaxis" must be bool or "auto", got {truncate_yaxis}' ) _setup_ax_spines( ax, diff --git a/mne/viz/topomap.py b/mne/viz/topomap.py index f78d035e0ad..4c66d58b80e 100644 --- a/mne/viz/topomap.py +++ b/mne/viz/topomap.py @@ -1218,9 +1218,8 @@ def _plot_topomap( raise ValueError("Multiple channel types in Info structure. " + info_help) elif len(pos["chs"]) != data.shape[0]: raise ValueError( - "Number of channels in the Info object (%s) and " - "the data array (%s) do not match. " % (len(pos["chs"]), data.shape[0]) - + info_help + f"Number of channels in the Info object ({len(pos['chs'])}) and the " + f"data array ({data.shape[0]}) do not match." + info_help ) else: ch_type = ch_type.pop() diff --git a/mne/viz/utils.py b/mne/viz/utils.py index 15a43916dc4..dd8323a2f85 100644 --- a/mne/viz/utils.py +++ b/mne/viz/utils.py @@ -457,8 +457,7 @@ def _prepare_trellis( naxes = ncols * nrows if naxes < n_cells: raise ValueError( - "Cannot plot {} axes in a {} by {} " - "figure.".format(n_cells, nrows, ncols) + f"Cannot plot {n_cells} axes in a {nrows} by {ncols} figure." ) width = size * ncols diff --git a/pyproject.toml b/pyproject.toml index 1145bdafcde..d3caea4706f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -117,7 +117,6 @@ test = [ "twine", "wheel", "pre-commit", - "black", ] # Dependencies for being able to run additional tests (rare/CIs/advanced devs) @@ -264,9 +263,6 @@ addopts = """--durations=20 --doctest-modules -ra --cov-report= --tb=short \ --color=yes --capture=sys""" junit_family = "xunit2" -[tool.black] -exclude = "(dist/)|(build/)|(.*\\.ipynb)" - [tool.bandit.assert_used] skips = ["*/test_*.py"] # assert statements are good practice with pytest diff --git a/tutorials/epochs/20_visualize_epochs.py b/tutorials/epochs/20_visualize_epochs.py index 5fc1a454700..69864d19e26 100644 --- a/tutorials/epochs/20_visualize_epochs.py +++ b/tutorials/epochs/20_visualize_epochs.py @@ -245,7 +245,9 @@ # therefore mask smaller signal fluctuations of interest. reject_criteria = dict( - mag=3000e-15, grad=3000e-13, eeg=150e-6 # 3000 fT # 3000 fT/cm + mag=3000e-15, # 3000 fT + grad=3000e-13, # 3000 fT/cm + eeg=150e-6, ) # 150 µV epochs.drop_bad(reject=reject_criteria) diff --git a/tutorials/inverse/20_dipole_fit.py b/tutorials/inverse/20_dipole_fit.py index f12e5968546..bf40d55e4ea 100644 --- a/tutorials/inverse/20_dipole_fit.py +++ b/tutorials/inverse/20_dipole_fit.py @@ -117,8 +117,7 @@ plot_params["colorbar"] = True diff.plot_topomap(time_format="Difference", axes=axes[2:], **plot_params) fig.suptitle( - "Comparison of measured and predicted fields " - "at {:.0f} ms".format(best_time * 1000.0), + f"Comparison of measured and predicted fields at {best_time * 1000:.0f} ms", fontsize=16, ) diff --git a/tutorials/raw/10_raw_overview.py b/tutorials/raw/10_raw_overview.py index 31dfbf12325..7b777046afc 100644 --- a/tutorials/raw/10_raw_overview.py +++ b/tutorials/raw/10_raw_overview.py @@ -142,8 +142,8 @@ ch_names = raw.ch_names n_chan = len(ch_names) # note: there is no raw.n_channels attribute print( - "the (cropped) sample data object has {} time samples and {} channels." - "".format(n_time_samps, n_chan) + f"the (cropped) sample data object has {n_time_samps} time samples and " + f"{n_chan} channels." ) print("The last time sample is at {} seconds.".format(time_secs[-1])) print("The first few channel names are {}.".format(", ".join(ch_names[:3]))) diff --git a/tutorials/stats-source-space/60_cluster_rmANOVA_spatiotemporal.py b/tutorials/stats-source-space/60_cluster_rmANOVA_spatiotemporal.py index 0951280e6d6..24c0adc9d35 100644 --- a/tutorials/stats-source-space/60_cluster_rmANOVA_spatiotemporal.py +++ b/tutorials/stats-source-space/60_cluster_rmANOVA_spatiotemporal.py @@ -285,9 +285,7 @@ def stat_fun(*args): inds_t, inds_v = [ (clusters[cluster_ind]) for ii, cluster_ind in enumerate(good_cluster_inds) -][ - 0 -] # first cluster +][0] # first cluster times = np.arange(X[0].shape[1]) * tstep * 1e3 From 3d3633cadf17631ffe2a984b13af5e21c5bbbde0 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Tue, 5 Dec 2023 11:59:16 -0500 Subject: [PATCH 104/405] BUG: Move defusedxml to optional dependencies (#12264) --- doc/changes/devel.rst | 1 + mne/channels/_dig_montage_utils.py | 11 +++++------ mne/channels/_standard_montage_utils.py | 6 +++--- mne/channels/tests/test_montage.py | 7 +++++-- mne/export/tests/test_export.py | 2 ++ mne/io/egi/egimff.py | 9 +++++++-- mne/io/egi/events.py | 6 +++--- mne/io/egi/general.py | 15 +++++++++++++-- mne/io/egi/tests/test_egi.py | 11 +++++++++++ mne/io/nedf/nedf.py | 6 +++--- mne/io/nedf/tests/test_nedf.py | 2 ++ pyproject.toml | 2 +- 12 files changed, 56 insertions(+), 22 deletions(-) diff --git a/doc/changes/devel.rst b/doc/changes/devel.rst index 3f3e8036419..43aa481fce5 100644 --- a/doc/changes/devel.rst +++ b/doc/changes/devel.rst @@ -30,6 +30,7 @@ Bugs ~~~~ - Allow :func:`mne.viz.plot_compare_evokeds` to plot eyetracking channels, and improve error handling (:gh:`12190` by `Scott Huberty`_) - Fix bug with accessing the last data sample using ``raw[:, -1]`` where an empty array was returned (:gh:`12248` by `Eric Larson`_) +- ``defusedxml`` is now an optional (rather than required) dependency and needed when reading EGI-MFF data, NEDF data, and BrainVision montages (:gh:`12264` by `Eric Larson`_) - Fix bug with type hints in :func:`mne.io.read_raw_neuralynx` (:gh:`12236` by `Richard Höchenberger`_) API changes diff --git a/mne/channels/_dig_montage_utils.py b/mne/channels/_dig_montage_utils.py index 4d2e9e6af3f..0f34af975d2 100644 --- a/mne/channels/_dig_montage_utils.py +++ b/mne/channels/_dig_montage_utils.py @@ -13,9 +13,8 @@ # Copyright the MNE-Python contributors. import numpy as np -from defusedxml import ElementTree -from ..utils import Bunch, _check_fname, warn +from ..utils import Bunch, _check_fname, _soft_import, warn def _read_dig_montage_egi( @@ -28,8 +27,8 @@ def _read_dig_montage_egi( "hsp, hpi, elp, point_names, fif must all be " "None if egi is not None" ) _check_fname(fname, overwrite="read", must_exist=True) - - root = ElementTree.parse(fname).getroot() + defusedxml = _soft_import("defusedxml", "reading EGI montages") + root = defusedxml.ElementTree.parse(fname).getroot() ns = root.tag[root.tag.index("{") : root.tag.index("}") + 1] sensors = root.find("%ssensorLayout/%ssensors" % (ns, ns)) fids = dict() @@ -76,8 +75,8 @@ def _read_dig_montage_egi( def _parse_brainvision_dig_montage(fname, scale): FID_NAME_MAP = {"Nasion": "nasion", "RPA": "rpa", "LPA": "lpa"} - - root = ElementTree.parse(fname).getroot() + defusedxml = _soft_import("defusedxml", "reading BrainVision montages") + root = defusedxml.ElementTree.parse(fname).getroot() sensors = root.find("CapTrakElectrodeList") fids, dig_ch_pos = dict(), dict() diff --git a/mne/channels/_standard_montage_utils.py b/mne/channels/_standard_montage_utils.py index 43c8fa6aecd..7b70c57881b 100644 --- a/mne/channels/_standard_montage_utils.py +++ b/mne/channels/_standard_montage_utils.py @@ -9,11 +9,10 @@ from functools import partial import numpy as np -from defusedxml import ElementTree from .._freesurfer import get_mni_fiducials from ..transforms import _sph_to_cart -from ..utils import _pl, warn +from ..utils import _pl, _soft_import, warn from . import __file__ as _CHANNELS_INIT_FILE from .montage import make_dig_montage @@ -344,7 +343,8 @@ def _read_brainvision(fname, head_size): # standard electrode positions: X-axis from T7 to T8, Y-axis from Oz to # Fpz, Z-axis orthogonal from XY-plane through Cz, fit to a sphere if # idealized (when radius=1), specified in millimeters - root = ElementTree.parse(fname).getroot() + defusedxml = _soft_import("defusedxml", "reading BrainVision montages") + root = defusedxml.ElementTree.parse(fname).getroot() ch_names = [s.text for s in root.findall("./Electrode/Name")] theta = [float(s.text) for s in root.findall("./Electrode/Theta")] pol = np.deg2rad(np.array(theta)) diff --git a/mne/channels/tests/test_montage.py b/mne/channels/tests/test_montage.py index a5e09440896..31320ed951c 100644 --- a/mne/channels/tests/test_montage.py +++ b/mne/channels/tests/test_montage.py @@ -513,6 +513,8 @@ def test_documented(): ) def test_montage_readers(reader, file_content, expected_dig, ext, warning, tmp_path): """Test that we have an equivalent of read_montage for all file formats.""" + if file_content.startswith(" \x00""" +pytest.importorskip("defusedxml") + @pytest.mark.parametrize("nacc", (0, 3)) def test_nedf_header_parser(nacc): diff --git a/pyproject.toml b/pyproject.toml index d3caea4706f..e95e5c48345 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,7 +42,6 @@ dependencies = [ "packaging", "jinja2", "lazy_loader>=0.3", - "defusedxml", ] [project.optional-dependencies] @@ -100,6 +99,7 @@ full = [ "edfio>=0.2.1", "pybv", "snirf", + "defusedxml", ] # Dependencies for running the test infrastructure From 7bf1b4ab70404a9f0b3cf00783a06e1ce9e0c272 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20H=C3=B6chenberger?= Date: Tue, 5 Dec 2023 20:31:31 +0100 Subject: [PATCH 105/405] MRG: Add return type hints to `read_evokeds()`, `read_raw()` (#12250) Co-authored-by: Eric Larson --- .git-blame-ignore-revs | 1 + .pre-commit-config.yaml | 9 +++++++++ doc/changes/devel.rst | 15 +++++++++++++-- mne/evoked.py | 3 ++- mne/io/_read_raw.py | 3 ++- pyproject.toml | 31 +++++++++++++++++++++++++++++++ 6 files changed, 58 insertions(+), 4 deletions(-) diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index 3e511b1a194..c9248c01bb0 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -1,3 +1,4 @@ e81ec528a42ac687f3d961ed5cf8e25f236925b0 # black 12395f9d9cf6ea3c72b225b62e052dd0d17d9889 # YAML indentation d6d2f8c6a2ed4a0b27357da9ddf8e0cd14931b59 # isort +e7dd1588013179013a50d3f6b8e8f9ae0a185783 # ruff format diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 29c11f935ec..52a3d560fdc 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -51,5 +51,14 @@ repos: - tomli files: ^doc/.*\.(rst|inc)$ + # mypy + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.7.1 + 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"] + ci: autofix_prs: false diff --git a/doc/changes/devel.rst b/doc/changes/devel.rst index 43aa481fce5..eaf1cb881ad 100644 --- a/doc/changes/devel.rst +++ b/doc/changes/devel.rst @@ -21,17 +21,28 @@ Version 1.7.dev0 (development) ------------------------------ +In this version, we started adding type hints (also known as "type annotations") to select parts of the codebase. +This meta information will be used by development environments (IDEs) like VS Code and PyCharm automatically to provide +better assistance such as tab completion or error detection even before running your code. + +So far, we've only added return type hints to :func:`mne.read_evokeds` and :func:`mne.io.read_raw`. Now your editors will know: +these functions return evoked and raw data, respectively. We are planning add type hints to more functions after careful +evaluation in the future. + +You don't need to do anything to benefit from these changes – your editor will pick them up automatically and provide the +enhanced experience if it supports it! + Enhancements ~~~~~~~~~~~~ - Speed up export to .edf in :func:`mne.export.export_raw` by using ``edfio`` instead of ``EDFlib-Python`` (:gh:`12218` by :newcontrib:`Florian Hofer`) - +- We added type hints for the return values of :func:`mne.read_evokeds` and :func:`mne.io.read_raw`. Development environments like VS Code or PyCharm will now provide more help when using these functions in your code. (:gh:`12250` by `Richard Höchenberger`_ and `Eric Larson`_) Bugs ~~~~ - Allow :func:`mne.viz.plot_compare_evokeds` to plot eyetracking channels, and improve error handling (:gh:`12190` by `Scott Huberty`_) - Fix bug with accessing the last data sample using ``raw[:, -1]`` where an empty array was returned (:gh:`12248` by `Eric Larson`_) +- Remove incorrect type hints in :func:`mne.io.read_raw_neuralynx` (:gh:`12236` by `Richard Höchenberger`_) - ``defusedxml`` is now an optional (rather than required) dependency and needed when reading EGI-MFF data, NEDF data, and BrainVision montages (:gh:`12264` by `Eric Larson`_) -- Fix bug with type hints in :func:`mne.io.read_raw_neuralynx` (:gh:`12236` by `Richard Höchenberger`_) API changes ~~~~~~~~~~~ diff --git a/mne/evoked.py b/mne/evoked.py index 03923533fbb..93583edb004 100644 --- a/mne/evoked.py +++ b/mne/evoked.py @@ -9,6 +9,7 @@ # Copyright the MNE-Python contributors. from copy import deepcopy +from typing import List, Union import numpy as np @@ -1538,7 +1539,7 @@ def read_evokeds( proj=True, allow_maxshield=False, verbose=None, -): +) -> Union[List[Evoked], Evoked]: """Read evoked dataset(s). Parameters diff --git a/mne/io/_read_raw.py b/mne/io/_read_raw.py index f9e715be6b0..c226bf63285 100644 --- a/mne/io/_read_raw.py +++ b/mne/io/_read_raw.py @@ -10,6 +10,7 @@ from pathlib import Path from ..utils import fill_doc +from .base import BaseRaw def _read_unsupported(fname, **kwargs): @@ -110,7 +111,7 @@ def split_name_ext(fname): @fill_doc -def read_raw(fname, *, preload=False, verbose=None, **kwargs): +def read_raw(fname, *, preload=False, verbose=None, **kwargs) -> BaseRaw: """Read raw file. This function is a convenient wrapper for readers defined in `mne.io`. The diff --git a/pyproject.toml b/pyproject.toml index e95e5c48345..7bb17f07570 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -117,6 +117,7 @@ test = [ "twine", "wheel", "pre-commit", + "mypy", ] # Dependencies for being able to run additional tests (rare/CIs/advanced devs) @@ -309,3 +310,33 @@ ignore_directives = [ "tab-set", ] ignore_messages = "^.*(Unknown target name|Undefined substitution referenced)[^`]*$" + +[tool.mypy] +ignore_errors = true +scripts_are_modules = true +strict = true + +[[tool.mypy.overrides]] +module = ['mne.evoked', 'mne.io'] +ignore_errors = false +# Ignore "attr-defined" until we fix stuff like: +# - BunchConstNamed: '"BunchConstNamed" has no attribute "FIFFB_EVOKED"' +# - Missing __all__: 'Module "mne.io.snirf" does not explicitly export attribute "read_raw_snirf"' +# Ignore "no-untyped-call" until we fix stuff like: +# - 'Call to untyped function "end_block" in typed context' +# Ignore "no-untyped-def" until we fix stuff like: +# - 'Function is missing a type annotation' +# Ignore "misc" until we fix stuff like: +# - 'Cannot determine type of "_projector" in base class "ProjMixin"' +# Ignore "assignment" until we fix stuff like: +# - 'Incompatible types in assignment (expression has type "tuple[str, ...]", variable has type "str")' +# Ignore "operator" until we fix stuff like: +# - Unsupported operand types for - ("None" and "int") +disable_error_code = [ + 'attr-defined', + 'no-untyped-call', + 'no-untyped-def', + 'misc', + 'assignment', + 'operator', +] From 432249ee6304b07a8ecece2b310a65c594c59ae9 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Wed, 6 Dec 2023 12:53:49 -0500 Subject: [PATCH 106/405] ENH: Add polyphase resampling (#12268) Co-authored-by: Daniel McCloy --- doc/Makefile | 2 +- doc/changes/devel.rst | 1 + ...dataset_sgskip.py => spm_faces_dataset.py} | 107 +++-------- examples/decoding/receptive_field_mtrf.py | 11 +- .../source_power_spectrum_opm.py | 8 +- mne/cuda.py | 2 +- mne/filter.py | 166 ++++++++++++------ mne/io/base.py | 22 ++- mne/io/fiff/tests/test_raw_fiff.py | 66 +++---- mne/source_estimate.py | 27 ++- mne/tests/test_filter.py | 39 ++-- mne/tests/test_source_estimate.py | 116 ++++++------ mne/utils/docs.py | 58 ++++-- .../preprocessing/30_filtering_resampling.py | 53 +++++- 14 files changed, 409 insertions(+), 269 deletions(-) rename examples/datasets/{spm_faces_dataset_sgskip.py => spm_faces_dataset.py} (60%) diff --git a/doc/Makefile b/doc/Makefile index 70d7429f4ad..3c251069045 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -76,6 +76,6 @@ doctest: "results in _build/doctest/output.txt." view: - @python -c "import webbrowser; webbrowser.open_new_tab('file://$(PWD)/_build/html/index.html')" + @python -c "import webbrowser; webbrowser.open_new_tab('file://$(PWD)/_build/html/sg_execution_times.html')" show: view diff --git a/doc/changes/devel.rst b/doc/changes/devel.rst index eaf1cb881ad..fadd872e621 100644 --- a/doc/changes/devel.rst +++ b/doc/changes/devel.rst @@ -36,6 +36,7 @@ Enhancements ~~~~~~~~~~~~ - Speed up export to .edf in :func:`mne.export.export_raw` by using ``edfio`` instead of ``EDFlib-Python`` (:gh:`12218` by :newcontrib:`Florian Hofer`) - We added type hints for the return values of :func:`mne.read_evokeds` and :func:`mne.io.read_raw`. Development environments like VS Code or PyCharm will now provide more help when using these functions in your code. (:gh:`12250` by `Richard Höchenberger`_ and `Eric Larson`_) +- Add ``method="polyphase"`` to :meth:`mne.io.Raw.resample` and related functions to allow resampling using :func:`scipy.signal.upfirdn` (:gh:`12268` by `Eric Larson`_) Bugs ~~~~ diff --git a/examples/datasets/spm_faces_dataset_sgskip.py b/examples/datasets/spm_faces_dataset.py similarity index 60% rename from examples/datasets/spm_faces_dataset_sgskip.py rename to examples/datasets/spm_faces_dataset.py index 1357fc513b6..32df7d1a9ed 100644 --- a/examples/datasets/spm_faces_dataset_sgskip.py +++ b/examples/datasets/spm_faces_dataset.py @@ -5,15 +5,8 @@ From raw data to dSPM on SPM Faces dataset ========================================== -Runs a full pipeline using MNE-Python: - - - artifact removal - - averaging Epochs - - forward model computation - - source reconstruction using dSPM on the contrast : "faces - scrambled" - -.. note:: This example does quite a bit of processing, so even on a - fast machine it can take several minutes to complete. +Runs a full pipeline using MNE-Python. This example does quite a bit of processing, so +even on a fast machine it can take several minutes to complete. """ # Authors: Alexandre Gramfort # Denis Engemann @@ -21,12 +14,6 @@ # License: BSD-3-Clause # Copyright the MNE-Python contributors. -# %% - -# sphinx_gallery_thumbnail_number = 10 - -import matplotlib.pyplot as plt - import mne from mne import combine_evoked, io from mne.datasets import spm_face @@ -40,109 +27,72 @@ spm_path = data_path / "MEG" / "spm" # %% -# Load and filter data, set up epochs +# Load data, filter it, and fit ICA. raw_fname = spm_path / "SPM_CTF_MEG_example_faces1_3D.ds" - raw = io.read_raw_ctf(raw_fname, preload=True) # Take first run # Here to save memory and time we'll downsample heavily -- this is not # advised for real data as it can effectively jitter events! -raw.resample(120.0, npad="auto") - -picks = mne.pick_types(raw.info, meg=True, exclude="bads") -raw.filter(1, 30, method="fir", fir_design="firwin") +raw.resample(100) +raw.filter(1.0, None) # high-pass +reject = dict(mag=5e-12) +ica = ICA(n_components=0.95, max_iter="auto", random_state=0) +ica.fit(raw, reject=reject) +# compute correlation scores, get bad indices sorted by score +eog_epochs = create_eog_epochs(raw, ch_name="MRT31-2908", reject=reject) +eog_inds, eog_scores = ica.find_bads_eog(eog_epochs, ch_name="MRT31-2908") +ica.plot_scores(eog_scores, eog_inds) # see scores the selection is based on +ica.plot_components(eog_inds) # view topographic sensitivity of components +ica.exclude += eog_inds[:1] # we saw the 2nd ECG component looked too dipolar +ica.plot_overlay(eog_epochs.average()) # inspect artifact removal +# %% +# Epoch data and apply ICA. events = mne.find_events(raw, stim_channel="UPPT001") - -# plot the events to get an idea of the paradigm -mne.viz.plot_events(events, raw.info["sfreq"]) - event_ids = {"faces": 1, "scrambled": 2} - tmin, tmax = -0.2, 0.6 -baseline = None # no baseline as high-pass is applied -reject = dict(mag=5e-12) - epochs = mne.Epochs( raw, events, event_ids, tmin, tmax, - picks=picks, - baseline=baseline, + picks="meg", + baseline=None, preload=True, reject=reject, ) - -# Fit ICA, find and remove major artifacts -ica = ICA(n_components=0.95, max_iter="auto", random_state=0) -ica.fit(raw, decim=1, reject=reject) - -# compute correlation scores, get bad indices sorted by score -eog_epochs = create_eog_epochs(raw, ch_name="MRT31-2908", reject=reject) -eog_inds, eog_scores = ica.find_bads_eog(eog_epochs, ch_name="MRT31-2908") -ica.plot_scores(eog_scores, eog_inds) # see scores the selection is based on -ica.plot_components(eog_inds) # view topographic sensitivity of components -ica.exclude += eog_inds[:1] # we saw the 2nd ECG component looked too dipolar -ica.plot_overlay(eog_epochs.average()) # inspect artifact removal +del raw ica.apply(epochs) # clean data, default in place - evoked = [epochs[k].average() for k in event_ids] - contrast = combine_evoked(evoked, weights=[-1, 1]) # Faces - scrambled - evoked.append(contrast) - for e in evoked: e.plot(ylim=dict(mag=[-400, 400])) -plt.show() - -# estimate noise covarariance -noise_cov = mne.compute_covariance(epochs, tmax=0, method="shrunk", rank=None) - # %% -# Visualize fields on MEG helmet - -# The transformation here was aligned using the dig-montage. It's included in -# the spm_faces dataset and is named SPM_dig_montage.fif. -trans_fname = spm_path / "SPM_CTF_MEG_example_faces1_3D_raw-trans.fif" - -maps = mne.make_field_map( - evoked[0], trans_fname, subject="spm", subjects_dir=subjects_dir, n_jobs=None -) - -evoked[0].plot_field(maps, time=0.170, time_viewer=False) - -# %% -# Look at the whitened evoked daat +# Estimate noise covariance and look at the whitened evoked data +noise_cov = mne.compute_covariance(epochs, tmax=0, method="shrunk", rank=None) evoked[0].plot_white(noise_cov) # %% # Compute forward model +trans_fname = spm_path / "SPM_CTF_MEG_example_faces1_3D_raw-trans.fif" src = subjects_dir / "spm" / "bem" / "spm-oct-6-src.fif" bem = subjects_dir / "spm" / "bem" / "spm-5120-5120-5120-bem-sol.fif" forward = mne.make_forward_solution(contrast.info, trans_fname, src, bem) # %% -# Compute inverse solution +# Compute inverse solution and plot + +# sphinx_gallery_thumbnail_number = 8 snr = 3.0 lambda2 = 1.0 / snr**2 -method = "dSPM" - -inverse_operator = make_inverse_operator( - contrast.info, forward, noise_cov, loose=0.2, depth=0.8 -) - -# Compute inverse solution on contrast -stc = apply_inverse(contrast, inverse_operator, lambda2, method, pick_ori=None) -# stc.save('spm_%s_dSPM_inverse' % contrast.comment) - -# Plot contrast in 3D with mne.viz.Brain if available +inverse_operator = make_inverse_operator(contrast.info, forward, noise_cov) +stc = apply_inverse(contrast, inverse_operator, lambda2, method="dSPM", pick_ori=None) brain = stc.plot( hemi="both", subjects_dir=subjects_dir, @@ -150,4 +100,3 @@ views=["ven"], clim={"kind": "value", "lims": [3.0, 6.0, 9.0]}, ) -# brain.save_image('dSPM_map.png') diff --git a/examples/decoding/receptive_field_mtrf.py b/examples/decoding/receptive_field_mtrf.py index 24b459f192f..8dc04630753 100644 --- a/examples/decoding/receptive_field_mtrf.py +++ b/examples/decoding/receptive_field_mtrf.py @@ -17,7 +17,7 @@ .. _figure 1: https://www.frontiersin.org/articles/10.3389/fnhum.2016.00604/full#F1 .. _figure 2: https://www.frontiersin.org/articles/10.3389/fnhum.2016.00604/full#F2 .. _figure 5: https://www.frontiersin.org/articles/10.3389/fnhum.2016.00604/full#F5 -""" # noqa: E501 +""" # Authors: Chris Holdgraf # Eric Larson @@ -26,9 +26,6 @@ # License: BSD-3-Clause # Copyright the MNE-Python contributors. -# %% -# sphinx_gallery_thumbnail_number = 3 - from os.path import join import matplotlib.pyplot as plt @@ -58,8 +55,8 @@ speech = data["envelope"].T sfreq = float(data["Fs"].item()) sfreq /= decim -speech = mne.filter.resample(speech, down=decim, npad="auto") -raw = mne.filter.resample(raw, down=decim, npad="auto") +speech = mne.filter.resample(speech, down=decim, method="polyphase") +raw = mne.filter.resample(raw, down=decim, method="polyphase") # Read in channel positions and create our MNE objects from the raw data montage = mne.channels.make_standard_montage("biosemi128") @@ -131,6 +128,8 @@ # across the scalp. We will recreate `figure 1`_ and `figure 2`_ from # :footcite:`CrosseEtAl2016`. +# sphinx_gallery_thumbnail_number = 3 + # Print mean coefficients across all time delays / channels (see Fig 1) time_plot = 0.180 # For highlighting a specific time. fig, ax = plt.subplots(figsize=(4, 8), layout="constrained") diff --git a/examples/time_frequency/source_power_spectrum_opm.py b/examples/time_frequency/source_power_spectrum_opm.py index dd142138784..11168cc08a5 100644 --- a/examples/time_frequency/source_power_spectrum_opm.py +++ b/examples/time_frequency/source_power_spectrum_opm.py @@ -58,16 +58,16 @@ raw_erms = dict() new_sfreq = 60.0 # Nyquist frequency (30 Hz) < line noise freq (50 Hz) raws["vv"] = mne.io.read_raw_fif(vv_fname, verbose="error") # ignore naming -raws["vv"].load_data().resample(new_sfreq) +raws["vv"].load_data().resample(new_sfreq, method="polyphase") raws["vv"].info["bads"] = ["MEG2233", "MEG1842"] raw_erms["vv"] = mne.io.read_raw_fif(vv_erm_fname, verbose="error") -raw_erms["vv"].load_data().resample(new_sfreq) +raw_erms["vv"].load_data().resample(new_sfreq, method="polyphase") raw_erms["vv"].info["bads"] = ["MEG2233", "MEG1842"] raws["opm"] = mne.io.read_raw_fif(opm_fname) -raws["opm"].load_data().resample(new_sfreq) +raws["opm"].load_data().resample(new_sfreq, method="polyphase") raw_erms["opm"] = mne.io.read_raw_fif(opm_erm_fname) -raw_erms["opm"].load_data().resample(new_sfreq) +raw_erms["opm"].load_data().resample(new_sfreq, method="polyphase") # Make sure our assumptions later hold assert raws["opm"].info["sfreq"] == raws["vv"].info["sfreq"] diff --git a/mne/cuda.py b/mne/cuda.py index b4aa7c37bf3..7d7634a6e4e 100644 --- a/mne/cuda.py +++ b/mne/cuda.py @@ -330,7 +330,7 @@ def _fft_resample(x, new_len, npads, to_removes, cuda_dict=None, pad="reflect_li Number of samples to remove after resampling. cuda_dict : dict Dictionary constructed using setup_cuda_multiply_repeated(). - %(pad)s + %(pad_resample)s The default is ``'reflect_limited'``. .. versionadded:: 0.15 diff --git a/mne/filter.py b/mne/filter.py index 528128822b8..3d9b3ecc7da 100644 --- a/mne/filter.py +++ b/mne/filter.py @@ -5,6 +5,7 @@ from collections import Counter from copy import deepcopy from functools import partial +from math import gcd import numpy as np from scipy import fft, signal @@ -1898,12 +1899,13 @@ def resample( x, up=1.0, down=1.0, - npad=100, + *, axis=-1, - window="boxcar", + window="auto", n_jobs=None, - pad="reflect_limited", - *, + pad="auto", + npad=100, + method="fft", verbose=None, ): """Resample an array. @@ -1918,15 +1920,18 @@ def resample( Factor to upsample by. down : float Factor to downsample by. - %(npad)s axis : int Axis along which to resample (default is the last axis). %(window_resample)s %(n_jobs_cuda)s - %(pad)s - The default is ``'reflect_limited'``. + ``n_jobs='cuda'`` is only supported when ``method="fft"``. + %(pad_resample_auto)s .. versionadded:: 0.15 + %(npad_resample)s + %(method_resample)s + + .. versionadded:: 1.7 %(verbose)s Returns @@ -1936,26 +1941,16 @@ def resample( Notes ----- - This uses (hopefully) intelligent edge padding and frequency-domain - windowing improve scipy.signal.resample's resampling method, which + When using ``method="fft"`` (default), + this uses (hopefully) intelligent edge padding and frequency-domain + windowing improve :func:`scipy.signal.resample`'s resampling method, which we have adapted for our use here. Choices of npad and window have important consequences, and the default choices should work well for most natural signals. - - Resampling arguments are broken into "up" and "down" components for future - compatibility in case we decide to use an upfirdn implementation. The - current implementation is functionally equivalent to passing - up=up/down and down=1. """ - # check explicitly for backwards compatibility - if not isinstance(axis, int): - err = ( - "The axis parameter needs to be an integer (got %s). " - "The axis parameter was missing from this function for a " - "period of time, you might be intending to specify the " - "subsequent window parameter." % repr(axis) - ) - raise TypeError(err) + _validate_type(method, str, "method") + _validate_type(pad, str, "pad") + _check_option("method", method, ("fft", "polyphase")) # make sure our arithmetic will work x = _check_filterable(x, "resampled", "resample") @@ -1963,31 +1958,88 @@ def resample( del up, down if axis < 0: axis = x.ndim + axis - orig_last_axis = x.ndim - 1 - if axis != orig_last_axis: - x = x.swapaxes(axis, orig_last_axis) - orig_shape = x.shape - x_len = orig_shape[-1] - if x_len == 0: - warn("x has zero length along last axis, returning a copy of x") + if x.shape[axis] == 0: + warn(f"x has zero length along axis={axis}, returning a copy of x") return x.copy() - bad_msg = 'npad must be "auto" or an integer' + + # prep for resampling along the last axis (swap axis with last then reshape) + out_shape = list(x.shape) + out_shape.pop(axis) + out_shape.append(final_len) + x = np.atleast_2d(x.swapaxes(axis, -1).reshape((-1, x.shape[axis]))) + + # do the resampling using FFT or polyphase methods + kwargs = dict(pad=pad, window=window, n_jobs=n_jobs) + if method == "fft": + y = _resample_fft(x, npad=npad, ratio=ratio, final_len=final_len, **kwargs) + else: + up, down, kwargs["window"] = _prep_polyphase( + ratio, x.shape[-1], final_len, window + ) + half_len = len(window) // 2 + logger.info( + f"Polyphase resampling locality: ±{half_len} input sample{_pl(half_len)}" + ) + y = _resample_polyphase(x, up=up, down=down, **kwargs) + assert y.shape[-1] == final_len + + # restore dimensions (reshape then swap axis with last) + y = y.reshape(out_shape).swapaxes(axis, -1) + + return y + + +def _prep_polyphase(ratio, x_len, final_len, window): + if isinstance(window, str) and window == "auto": + window = ("kaiser", 5.0) # SciPy default + up = final_len + down = x_len + g_ = gcd(up, down) + up = up // g_ + down = down // g_ + # Figure out our signal locality and design window (adapted from SciPy) + if not isinstance(window, (list, np.ndarray)): + # Design a linear-phase low-pass FIR filter + max_rate = max(up, down) + f_c = 1.0 / max_rate # cutoff of FIR filter (rel. to Nyquist) + half_len = 10 * max_rate # reasonable cutoff for sinc-like function + window = signal.firwin(2 * half_len + 1, f_c, window=window) + return up, down, window + + +def _resample_polyphase(x, *, up, down, pad, window, n_jobs): + if pad == "auto": + pad = "reflect" + kwargs = dict(padtype=pad, window=window, up=up, down=down) + _validate_type( + n_jobs, (None, "int-like"), "n_jobs", extra="when method='polyphase'" + ) + parallel, p_fun, n_jobs = parallel_func(signal.resample_poly, n_jobs) + if n_jobs == 1: + y = signal.resample_poly(x, axis=-1, **kwargs) + else: + y = np.array(parallel(p_fun(x_, **kwargs) for x_ in x)) + return y + + +def _resample_fft(x_flat, *, ratio, final_len, pad, window, npad, n_jobs): + x_len = x_flat.shape[-1] + pad = "reflect_limited" if pad == "auto" else pad + if (isinstance(window, str) and window == "auto") or window is None: + window = "boxcar" if isinstance(npad, str): - if npad != "auto": - raise ValueError(bad_msg) + _check_option("npad", npad, ("auto",), extra="when a string") # Figure out reasonable pad that gets us to a power of 2 min_add = min(x_len // 8, 100) * 2 npad = 2 ** int(np.ceil(np.log2(x_len + min_add))) - x_len npad, extra = divmod(npad, 2) npads = np.array([npad, npad + extra], int) else: - if npad != int(npad): - raise ValueError(bad_msg) + npad = _ensure_int(npad, "npad", extra="or 'auto'") npads = np.array([npad, npad], int) del npad # prep for resampling now - x_flat = x.reshape((-1, x_len)) orig_len = x_len + npads.sum() # length after padding new_len = max(int(round(ratio * orig_len)), 1) # length after resampling to_removes = [int(round(ratio * npads[0]))] @@ -1997,15 +2049,12 @@ def resample( # assert np.abs(to_removes[1] - to_removes[0]) <= int(np.ceil(ratio)) # figure out windowing function - if window is not None: - if callable(window): - W = window(fft.fftfreq(orig_len)) - elif isinstance(window, np.ndarray) and window.shape == (orig_len,): - W = window - else: - W = fft.ifftshift(signal.get_window(window, orig_len)) + if callable(window): + W = window(fft.fftfreq(orig_len)) + elif isinstance(window, np.ndarray) and window.shape == (orig_len,): + W = window else: - W = np.ones(orig_len) + W = fft.ifftshift(signal.get_window(window, orig_len)) W *= float(new_len) / float(orig_len) # figure out if we should use CUDA @@ -2015,7 +2064,7 @@ def resample( # use of the 'flat' window is recommended for minimal ringing parallel, p_fun, n_jobs = parallel_func(_fft_resample, n_jobs) if n_jobs == 1: - y = np.zeros((len(x_flat), new_len - to_removes.sum()), dtype=x.dtype) + y = np.zeros((len(x_flat), new_len - to_removes.sum()), dtype=x_flat.dtype) for xi, x_ in enumerate(x_flat): y[xi] = _fft_resample(x_, new_len, npads, to_removes, cuda_dict, pad) else: @@ -2024,12 +2073,6 @@ def resample( ) y = np.array(y) - # Restore the original array shape (modified for resampling) - y.shape = orig_shape[:-1] + (y.shape[1],) - if axis != orig_last_axis: - y = y.swapaxes(axis, orig_last_axis) - assert y.shape[axis] == final_len - return y @@ -2635,11 +2678,12 @@ def filter( def resample( self, sfreq, + *, npad="auto", - window="boxcar", + window="auto", n_jobs=None, pad="edge", - *, + method="fft", verbose=None, ): """Resample data. @@ -2656,11 +2700,12 @@ def resample( %(npad)s %(window_resample)s %(n_jobs_cuda)s - %(pad)s - The default is ``'edge'``, which pads with the edge values of each - vector. + %(pad_resample)s .. versionadded:: 0.15 + %(method_resample)s + + .. versionadded:: 1.7 %(verbose)s Returns @@ -2691,7 +2736,14 @@ def resample( _check_preload(self, "inst.resample") self._data = resample( - self._data, sfreq, o_sfreq, npad, window=window, n_jobs=n_jobs, pad=pad + self._data, + sfreq, + o_sfreq, + npad=npad, + window=window, + n_jobs=n_jobs, + pad=pad, + method=method, ) lowpass = self.info.get("lowpass") lowpass = np.inf if lowpass is None else lowpass diff --git a/mne/io/base.py b/mne/io/base.py index 95ba7038865..652b747a8ac 100644 --- a/mne/io/base.py +++ b/mne/io/base.py @@ -1260,12 +1260,14 @@ def notch_filter( def resample( self, sfreq, + *, npad="auto", - window="boxcar", + window="auto", stim_picks=None, n_jobs=None, events=None, - pad="reflect_limited", + pad="auto", + method="fft", verbose=None, ): """Resample all channels. @@ -1294,7 +1296,7 @@ def resample( ---------- sfreq : float New sample rate to use. - %(npad)s + %(npad_resample)s %(window_resample)s stim_picks : list of int | None Stim channels. These channels are simply subsampled or @@ -1307,10 +1309,12 @@ def resample( An optional event matrix. When specified, the onsets of the events are resampled jointly with the data. NB: The input events are not modified, but a new array is returned with the raw instead. - %(pad)s - The default is ``'reflect_limited'``. + %(pad_resample_auto)s .. versionadded:: 0.15 + %(method_resample)s + + .. versionadded:: 1.7 %(verbose)s Returns @@ -1364,7 +1368,13 @@ def resample( ) kwargs = dict( - up=sfreq, down=o_sfreq, npad=npad, window=window, n_jobs=n_jobs, pad=pad + up=sfreq, + down=o_sfreq, + npad=npad, + window=window, + n_jobs=n_jobs, + pad=pad, + method=method, ) ratio, n_news = zip( *( diff --git a/mne/io/fiff/tests/test_raw_fiff.py b/mne/io/fiff/tests/test_raw_fiff.py index 5c760735800..2c302eac3ad 100644 --- a/mne/io/fiff/tests/test_raw_fiff.py +++ b/mne/io/fiff/tests/test_raw_fiff.py @@ -42,6 +42,7 @@ _record_warnings, assert_and_remove_boundary_annot, assert_object_equal, + catch_logging, requires_mne, run_subprocess, ) @@ -1290,23 +1291,28 @@ def test_resample_equiv(): @pytest.mark.slowtest @testing.requires_testing_data @pytest.mark.parametrize( - "preload, n, npad", + "preload, n, npad, method", [ - (True, 512, "auto"), - (False, 512, 0), + (True, 512, "auto", "fft"), + (True, 512, "auto", "polyphase"), + (False, 512, 0, "fft"), # only test one with non-preload because it's slow ], ) -def test_resample(tmp_path, preload, n, npad): +def test_resample(tmp_path, preload, n, npad, method): """Test resample (with I/O and multiple files).""" + kwargs = dict(npad=npad, method=method) raw = read_raw_fif(fif_fname) raw.crop(0, raw.times[n - 1]) + # Reduce to a few MEG channels and a few stim channels to speed up + n_meg = 5 + raw.pick(raw.ch_names[:n_meg] + raw.ch_names[312:320]) # 10 MEG + 3 STIM + 5 EEG assert len(raw.times) == n if preload: raw.load_data() raw_resamp = raw.copy() sfreq = raw.info["sfreq"] # test parallel on upsample - raw_resamp.resample(sfreq * 2, n_jobs=2, npad=npad) + raw_resamp.resample(sfreq * 2, n_jobs=2, **kwargs) assert raw_resamp.n_times == len(raw_resamp.times) raw_resamp.save(tmp_path / "raw_resamp-raw.fif") raw_resamp = read_raw_fif(tmp_path / "raw_resamp-raw.fif", preload=True) @@ -1315,7 +1321,13 @@ def test_resample(tmp_path, preload, n, npad): assert raw_resamp.get_data().shape[1] == raw_resamp.n_times assert raw.get_data().shape[0] == raw_resamp._data.shape[0] # test non-parallel on downsample - raw_resamp.resample(sfreq, n_jobs=None, npad=npad) + with catch_logging() as log: + raw_resamp.resample(sfreq, n_jobs=None, verbose=True, **kwargs) + log = log.getvalue() + if method == "fft": + assert "locality" not in log + else: + assert "locality" in log assert raw_resamp.info["sfreq"] == sfreq assert raw.get_data().shape == raw_resamp._data.shape assert raw.first_samp == raw_resamp.first_samp @@ -1324,18 +1336,12 @@ def test_resample(tmp_path, preload, n, npad): # works (hooray). Note that the stim channels had to be sub-sampled # without filtering to be accurately preserved # note we have to treat MEG and EEG+STIM channels differently (tols) - assert_allclose( - raw.get_data()[:306, 200:-200], - raw_resamp._data[:306, 200:-200], - rtol=1e-2, - atol=1e-12, - ) - assert_allclose( - raw.get_data()[306:, 200:-200], - raw_resamp._data[306:, 200:-200], - rtol=1e-2, - atol=1e-7, - ) + want_meg = raw.get_data()[:n_meg, 200:-200] + got_meg = raw_resamp._data[:n_meg, 200:-200] + want_non_meg = raw.get_data()[n_meg:, 200:-200] + got_non_meg = raw_resamp._data[n_meg:, 200:-200] + assert_allclose(got_meg, want_meg, rtol=1e-2, atol=1e-12) + assert_allclose(want_non_meg, got_non_meg, rtol=1e-2, atol=1e-7) # now check multiple file support w/resampling, as order of operations # (concat, resample) should not affect our data @@ -1344,9 +1350,9 @@ def test_resample(tmp_path, preload, n, npad): raw3 = raw.copy() raw4 = raw.copy() raw1 = concatenate_raws([raw1, raw2]) - raw1.resample(10.0, npad=npad) - raw3.resample(10.0, npad=npad) - raw4.resample(10.0, npad=npad) + raw1.resample(10.0, **kwargs) + raw3.resample(10.0, **kwargs) + raw4.resample(10.0, **kwargs) raw3 = concatenate_raws([raw3, raw4]) assert_array_equal(raw1._data, raw3._data) assert_array_equal(raw1._first_samps, raw3._first_samps) @@ -1364,12 +1370,12 @@ def test_resample(tmp_path, preload, n, npad): # basic decimation stim = [1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0] raw = RawArray([stim], create_info(1, len(stim), ["stim"])) - assert_allclose(raw.resample(8.0, npad=npad)._data, [[1, 1, 0, 0, 1, 1, 0, 0]]) + assert_allclose(raw.resample(8.0, **kwargs)._data, [[1, 1, 0, 0, 1, 1, 0, 0]]) # decimation of multiple stim channels raw = RawArray(2 * [stim], create_info(2, len(stim), 2 * ["stim"])) assert_allclose( - raw.resample(8.0, npad=npad, verbose="error")._data, + raw.resample(8.0, **kwargs, verbose="error")._data, [[1, 1, 0, 0, 1, 1, 0, 0], [1, 1, 0, 0, 1, 1, 0, 0]], ) @@ -1377,19 +1383,19 @@ def test_resample(tmp_path, preload, n, npad): # done naively stim = [0, 0, 0, 1, 1, 0, 0, 0] raw = RawArray([stim], create_info(1, len(stim), ["stim"])) - assert_allclose(raw.resample(4.0, npad=npad)._data, [[0, 1, 1, 0]]) + assert_allclose(raw.resample(4.0, **kwargs)._data, [[0, 1, 1, 0]]) # two events are merged in this case (warning) stim = [0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0] raw = RawArray([stim], create_info(1, len(stim), ["stim"])) with pytest.warns(RuntimeWarning, match="become unreliable"): - raw.resample(8.0, npad=npad) + raw.resample(8.0, **kwargs) # events are dropped in this case (warning) stim = [0, 1, 1, 0, 0, 1, 1, 0] raw = RawArray([stim], create_info(1, len(stim), ["stim"])) with pytest.warns(RuntimeWarning, match="become unreliable"): - raw.resample(4.0, npad=npad) + raw.resample(4.0, **kwargs) # test resampling events: this should no longer give a warning # we often have first_samp != 0, include it here too @@ -1400,7 +1406,7 @@ def test_resample(tmp_path, preload, n, npad): first_samp = len(stim) // 2 raw = RawArray([stim], create_info(1, o_sfreq, ["stim"]), first_samp=first_samp) events = find_events(raw) - raw, events = raw.resample(n_sfreq, events=events, npad=npad) + raw, events = raw.resample(n_sfreq, events=events, **kwargs) # Try index into raw.times with resampled events: raw.times[events[:, 0] - raw.first_samp] n_fsamp = int(first_samp * sfreq_ratio) # how it's calc'd in base.py @@ -1425,16 +1431,16 @@ def test_resample(tmp_path, preload, n, npad): # test copy flag stim = [1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0] raw = RawArray([stim], create_info(1, len(stim), ["stim"])) - raw_resampled = raw.copy().resample(4.0, npad=npad) + raw_resampled = raw.copy().resample(4.0, **kwargs) assert raw_resampled is not raw - raw_resampled = raw.resample(4.0, npad=npad) + raw_resampled = raw.resample(4.0, **kwargs) assert raw_resampled is raw # resample should still work even when no stim channel is present raw = RawArray(np.random.randn(1, 100), create_info(1, 100, ["eeg"])) with raw.info._unlock(): raw.info["lowpass"] = 50.0 - raw.resample(10, npad=npad) + raw.resample(10, **kwargs) assert raw.info["lowpass"] == 5.0 assert len(raw) == 10 diff --git a/mne/source_estimate.py b/mne/source_estimate.py index 213d00e5baa..50734817431 100644 --- a/mne/source_estimate.py +++ b/mne/source_estimate.py @@ -819,7 +819,17 @@ def crop(self, tmin=None, tmax=None, include_tmax=True): return self # return self for chaining methods @verbose - def resample(self, sfreq, npad="auto", window="boxcar", n_jobs=None, verbose=None): + def resample( + self, + sfreq, + *, + npad=100, + method="fft", + window="auto", + pad="auto", + n_jobs=None, + verbose=None, + ): """Resample data. If appropriate, an anti-aliasing filter is applied before resampling. @@ -833,8 +843,15 @@ def resample(self, sfreq, npad="auto", window="boxcar", n_jobs=None, verbose=Non Amount to pad the start and end of the data. Can also be "auto" to use a padding that will result in a power-of-two size (can be much faster). - window : str | tuple - Window to use in resampling. See :func:`scipy.signal.resample`. + %(method_resample)s + + .. versionadded:: 1.7 + %(window_resample)s + + .. versionadded:: 1.7 + %(pad_resample_auto)s + + .. versionadded:: 1.7 %(n_jobs)s %(verbose)s @@ -863,7 +880,9 @@ def resample(self, sfreq, npad="auto", window="boxcar", n_jobs=None, verbose=Non data = self.data if data.dtype == np.float32: data = data.astype(np.float64) - self.data = resample(data, sfreq, o_sfreq, npad, n_jobs=n_jobs) + self.data = resample( + data, sfreq, o_sfreq, npad=npad, window=window, n_jobs=n_jobs, method=method + ) # adjust indirectly affected variables self.tstep = 1.0 / sfreq diff --git a/mne/tests/test_filter.py b/mne/tests/test_filter.py index 110a8f136c3..3ab60dba055 100644 --- a/mne/tests/test_filter.py +++ b/mne/tests/test_filter.py @@ -32,6 +32,8 @@ from mne.io import RawArray, read_raw_fif from mne.utils import catch_logging, requires_mne, run_subprocess, sum_squared +resample_method_parametrize = pytest.mark.parametrize("method", ("fft", "polyphase")) + def test_filter_array(): """Test filtering an array.""" @@ -372,20 +374,27 @@ def test_notch_filters(method, filter_length, line_freq, tol): assert_almost_equal(new_power, orig_power, tol) -def test_resample(): +@resample_method_parametrize +def test_resample(method): """Test resampling.""" rng = np.random.RandomState(0) x = rng.normal(0, 1, (10, 10, 10)) - x_rs = resample(x, 1, 2, 10) + with catch_logging() as log: + x_rs = resample(x, 1, 2, npad=10, method=method, verbose=True) + log = log.getvalue() + if method == "fft": + assert "locality" not in log + else: + assert "locality" in log assert x.shape == (10, 10, 10) assert x_rs.shape == (10, 10, 5) x_2 = x.swapaxes(0, 1) - x_2_rs = resample(x_2, 1, 2, 10) + x_2_rs = resample(x_2, 1, 2, npad=10, method=method) assert_array_equal(x_2_rs.swapaxes(0, 1), x_rs) x_3 = x.swapaxes(0, 2) - x_3_rs = resample(x_3, 1, 2, 10, 0) + x_3_rs = resample(x_3, 1, 2, npad=10, axis=0, method=method) assert_array_equal(x_3_rs.swapaxes(0, 2), x_rs) # make sure we cast to array if necessary @@ -401,12 +410,12 @@ def test_resample_scipy(): err_msg = "%s: %s" % (N, window) x_2_sp = sp_resample(x, 2 * N, window=window) for n_jobs in n_jobs_test: - x_2 = resample(x, 2, 1, 0, window=window, n_jobs=n_jobs) + x_2 = resample(x, 2, 1, npad=0, window=window, n_jobs=n_jobs) assert_allclose(x_2, x_2_sp, atol=1e-12, err_msg=err_msg) new_len = int(round(len(x) * (1.0 / 2.0))) x_p5_sp = sp_resample(x, new_len, window=window) for n_jobs in n_jobs_test: - x_p5 = resample(x, 1, 2, 0, window=window, n_jobs=n_jobs) + x_p5 = resample(x, 1, 2, npad=0, window=window, n_jobs=n_jobs) assert_allclose(x_p5, x_p5_sp, atol=1e-12, err_msg=err_msg) @@ -450,23 +459,25 @@ def test_resamp_stim_channel(): assert new_data.shape[1] == new_data_len -def test_resample_raw(): +@resample_method_parametrize +def test_resample_raw(method): """Test resampling using RawArray.""" x = np.zeros((1, 1001)) sfreq = 2048.0 raw = RawArray(x, create_info(1, sfreq, "eeg")) - raw.resample(128, npad=10) + raw.resample(128, npad=10, method=method) data = raw.get_data() assert data.shape == (1, 63) -def test_resample_below_1_sample(): +@resample_method_parametrize +def test_resample_below_1_sample(method): """Test resampling doesn't yield datapoints.""" # Raw x = np.zeros((1, 100)) sfreq = 1000.0 raw = RawArray(x, create_info(1, sfreq, "eeg")) - raw.resample(5) + raw.resample(5, method=method) assert len(raw.times) == 1 assert raw.get_data().shape[1] == 1 @@ -487,7 +498,13 @@ def test_resample_below_1_sample(): preload=True, verbose=False, ) - epochs.resample(1) + with catch_logging() as log: + epochs.resample(1, method=method, verbose=True) + log = log.getvalue() + if method == "fft": + assert "locality" not in log + else: + assert "locality" in log assert len(epochs.times) == 1 assert epochs.get_data(copy=False).shape[2] == 1 diff --git a/mne/tests/test_source_estimate.py b/mne/tests/test_source_estimate.py index 8c9e7df9389..be31fd1501b 100644 --- a/mne/tests/test_source_estimate.py +++ b/mne/tests/test_source_estimate.py @@ -558,61 +558,73 @@ def test_stc_arithmetic(): @pytest.mark.slowtest @testing.requires_testing_data -def test_stc_methods(): +@pytest.mark.parametrize("kind", ("scalar", "vector")) +@pytest.mark.parametrize("method", ("fft", "polyphase")) +def test_stc_methods(kind, method): """Test stc methods lh_data, rh_data, bin(), resample().""" - stc_ = read_source_estimate(fname_stc) + stc = read_source_estimate(fname_stc) - # Make a vector version of the above source estimate - x = stc_.data[:, np.newaxis, :] - yz = np.zeros((x.shape[0], 2, x.shape[2])) - vec_stc_ = VectorSourceEstimate( - np.concatenate((x, yz), 1), stc_.vertices, stc_.tmin, stc_.tstep, stc_.subject - ) + if kind == "vector": + # Make a vector version of the above source estimate + x = stc.data[:, np.newaxis, :] + yz = np.zeros((x.shape[0], 2, x.shape[2])) + stc = VectorSourceEstimate( + np.concatenate((x, yz), 1), + stc.vertices, + stc.tmin, + stc.tstep, + stc.subject, + ) - for stc in [stc_, vec_stc_]: - # lh_data / rh_data - assert_array_equal(stc.lh_data, stc.data[: len(stc.lh_vertno)]) - assert_array_equal(stc.rh_data, stc.data[len(stc.lh_vertno) :]) - - # bin - binned = stc.bin(0.12) - a = np.mean(stc.data[..., : np.searchsorted(stc.times, 0.12)], axis=-1) - assert_array_equal(a, binned.data[..., 0]) - - stc = read_source_estimate(fname_stc) - stc.subject = "sample" - label_lh = read_labels_from_annot( - "sample", "aparc", "lh", subjects_dir=subjects_dir - )[0] - label_rh = read_labels_from_annot( - "sample", "aparc", "rh", subjects_dir=subjects_dir - )[0] - label_both = label_lh + label_rh - for label in (label_lh, label_rh, label_both): - assert isinstance(stc.shape, tuple) and len(stc.shape) == 2 - stc_label = stc.in_label(label) - if label.hemi != "both": - if label.hemi == "lh": - verts = stc_label.vertices[0] - else: # label.hemi == 'rh': - verts = stc_label.vertices[1] - n_vertices_used = len(label.get_vertices_used(verts)) - assert_equal(len(stc_label.data), n_vertices_used) - stc_lh = stc.in_label(label_lh) - pytest.raises(ValueError, stc_lh.in_label, label_rh) - label_lh.subject = "foo" - pytest.raises(RuntimeError, stc.in_label, label_lh) - - stc_new = deepcopy(stc) - o_sfreq = 1.0 / stc.tstep - # note that using no padding for this STC reduces edge ringing... - stc_new.resample(2 * o_sfreq, npad=0) - assert stc_new.data.shape[1] == 2 * stc.data.shape[1] - assert stc_new.tstep == stc.tstep / 2 - stc_new.resample(o_sfreq, npad=0) - assert stc_new.data.shape[1] == stc.data.shape[1] - assert stc_new.tstep == stc.tstep - assert_array_almost_equal(stc_new.data, stc.data, 5) + # lh_data / rh_data + assert_array_equal(stc.lh_data, stc.data[: len(stc.lh_vertno)]) + assert_array_equal(stc.rh_data, stc.data[len(stc.lh_vertno) :]) + + # bin + binned = stc.bin(0.12) + a = np.mean(stc.data[..., : np.searchsorted(stc.times, 0.12)], axis=-1) + assert_array_equal(a, binned.data[..., 0]) + + stc = read_source_estimate(fname_stc) + stc.subject = "sample" + label_lh = read_labels_from_annot( + "sample", "aparc", "lh", subjects_dir=subjects_dir + )[0] + label_rh = read_labels_from_annot( + "sample", "aparc", "rh", subjects_dir=subjects_dir + )[0] + label_both = label_lh + label_rh + for label in (label_lh, label_rh, label_both): + assert isinstance(stc.shape, tuple) and len(stc.shape) == 2 + stc_label = stc.in_label(label) + if label.hemi != "both": + if label.hemi == "lh": + verts = stc_label.vertices[0] + else: # label.hemi == 'rh': + verts = stc_label.vertices[1] + n_vertices_used = len(label.get_vertices_used(verts)) + assert_equal(len(stc_label.data), n_vertices_used) + stc_lh = stc.in_label(label_lh) + pytest.raises(ValueError, stc_lh.in_label, label_rh) + label_lh.subject = "foo" + pytest.raises(RuntimeError, stc.in_label, label_lh) + + stc_new = deepcopy(stc) + o_sfreq = 1.0 / stc.tstep + # note that using no padding for this STC reduces edge ringing... + stc_new.resample(2 * o_sfreq, npad=0, method=method) + assert stc_new.data.shape[1] == 2 * stc.data.shape[1] + assert stc_new.tstep == stc.tstep / 2 + stc_new.resample(o_sfreq, npad=0, method=method) + assert stc_new.data.shape[1] == stc.data.shape[1] + assert stc_new.tstep == stc.tstep + if method == "fft": + # no low-passing so survives round-trip + assert_allclose(stc_new.data, stc.data, atol=1e-5) + else: + # low-passing means we need something more flexible + corr = np.corrcoef(stc_new.data.ravel(), stc.data.ravel())[0, 1] + assert 0.99 < corr < 1 @testing.requires_testing_data diff --git a/mne/utils/docs.py b/mne/utils/docs.py index 806d774f221..6d26d01dc40 100644 --- a/mne/utils/docs.py +++ b/mne/utils/docs.py @@ -2245,6 +2245,13 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): docdict["method_psd"] = _method_psd.format("", "") docdict["method_psd_auto"] = _method_psd.format(" | ``'auto'``", "") +docdict["method_resample"] = """ +method : str + Resampling method to use. Can be ``"fft"`` (default) or ``"polyphase"`` + to use FFT-based on polyphase FIR resampling, respectively. These wrap to + :func:`scipy.signal.resample` and :func:`scipy.signal.resample_poly`, respectively. +""" + docdict["mode_eltc"] = """ mode : str Extraction mode, see Notes. @@ -2488,11 +2495,16 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): docdict["npad"] = """ npad : int | str - Amount to pad the start and end of the data. - Can also be ``"auto"`` to use a padding that will result in - a power-of-two size (can be much faster). + Amount to pad the start and end of the data. Can also be ``"auto"`` to use a padding + that will result in a power-of-two size (can be much faster). """ +docdict["npad_resample"] = ( + docdict["npad"] + + """ + Only used when ``method="fft"``. +""" +) docdict["nrows_ncols_ica_components"] = """ nrows, ncols : int | 'auto' The number of rows and columns of topographies to plot. If both ``nrows`` @@ -2698,22 +2710,38 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): # P _pad_base = """ -pad : str - The type of padding to use. Supports all :func:`numpy.pad` ``mode`` - options. Can also be ``"reflect_limited"``, which pads with a - reflected version of each vector mirrored on the first and last values + all :func:`numpy.pad` ``mode`` options. Can also be ``"reflect_limited"``, which + pads with a reflected version of each vector mirrored on the first and last values of the vector, followed by zeros. """ -docdict["pad"] = _pad_base - docdict["pad_fir"] = ( - _pad_base - + """ + """ +pad : str + The type of padding to use. Supports """ + + _pad_base + + """\ Only used for ``method='fir'``. """ ) +docdict["pad_resample"] = ( # used when default is not "auto" + """ +pad : str + The type of padding to use. When ``method="fft"``, supports """ + + _pad_base + + """\ + When ``method="polyphase"``, supports all modes of :func:`scipy.signal.upfirdn`. +""" +) + +docdict["pad_resample_auto"] = ( # used when default is "auto" + docdict["pad_resample"] + + """\ + The default ("auto") means ``'reflect_limited'`` for ``method='fft'`` and + ``'reflect'`` for ``method='polyphase'``. +""" +) docdict["pca_vars_pctf"] = """ pca_vars : array, shape (n_comp,) | list of array The explained variances of the first n_comp SVD components across the @@ -4331,8 +4359,12 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): docdict["window_resample"] = """ window : str | tuple - Frequency-domain window to use in resampling. - See :func:`scipy.signal.resample`. + When ``method="fft"``, this is the *frequency-domain* window to use in resampling, + and should be the same length as the signal; see :func:`scipy.signal.resample` + for details. When ``method="polyphase"``, this is the *time-domain* linear-phase + window to use after upsampling the signal; see :func:`scipy.signal.resample_poly` + for details. The default ``"auto"`` will use ``"boxcar"`` for ``method="fft"`` and + ``("kaiser", 5.0)`` for ``method="polyphase"``. """ # %% diff --git a/tutorials/preprocessing/30_filtering_resampling.py b/tutorials/preprocessing/30_filtering_resampling.py index 530b92741f6..6c118c99180 100644 --- a/tutorials/preprocessing/30_filtering_resampling.py +++ b/tutorials/preprocessing/30_filtering_resampling.py @@ -206,16 +206,59 @@ def add_arrows(axes): # frequency`_ of the desired new sampling rate. This can be clearly seen in the # PSD plot, where a dashed vertical line indicates the filter cutoff; the # original data had an existing lowpass at around 172 Hz (see -# ``raw.info['lowpass']``), and the data resampled from 600 Hz to 200 Hz gets +# ``raw.info['lowpass']``), and the data resampled from ~600 Hz to 200 Hz gets # automatically lowpass filtered at 100 Hz (the `Nyquist frequency`_ for a # target rate of 200 Hz): raw_downsampled = raw.copy().resample(sfreq=200) +# choose n_fft for Welch PSD to make frequency axes similar resolution +n_ffts = [4096, int(round(4096 * 200 / raw.info["sfreq"]))] +fig, axes = plt.subplots(2, 1, sharey=True, layout="constrained", figsize=(10, 6)) +for ax, data, title, n_fft in zip( + axes, [raw, raw_downsampled], ["Original", "Downsampled"], n_ffts +): + fig = data.compute_psd(n_fft=n_fft).plot( + average=True, picks="data", exclude="bads", axes=ax + ) + ax.set(title=title, xlim=(0, 300)) -for data, title in zip([raw, raw_downsampled], ["Original", "Downsampled"]): - fig = data.compute_psd().plot(average=True, picks="data", exclude="bads") - fig.suptitle(title) - plt.setp(fig.axes, xlim=(0, 300)) +# %% +# By default, MNE-Python resamples using ``method="fft"``, which performs FFT-based +# resampling via :func:`scipy.signal.resample`. While efficient and good for most +# biological signals, it has two main potential drawbacks: +# +# 1. It assumes periodicity of the signal. We try to overcome this with appropriate +# signal padding, but some signal leakage may still occur. +# 2. It treats the entire signal as a single block. This means that in general effects +# are not guaranteed to be localized in time, though in practice they often are. +# +# Alternatively, resampling can be performed using ``method="polyphase"`` instead. +# This uses :func:`scipy.signal.resample_poly` under the hood, which in turn utilizes +# a three-step process to resample signals (see :func:`scipy.signal.upfirdn` for +# details). This process guarantees that each resampled output value is only affected by +# input values within a limited range. In other words, output values are guaranteed to +# be a result of a specific set of input values. +# +# In general, using ``method="polyphase"`` can also be faster than ``method="fft"`` in +# cases where the desired sampling rate is an integer factor different from the input +# sampling rate. For example: + +# sphinx_gallery_thumbnail_number = 11 + +n_ffts = [4096, 2048] # factor of 2 smaller n_fft +raw_downsampled_poly = raw.copy().resample( + sfreq=raw.info["sfreq"] / 2.0, + method="polyphase", + verbose=True, +) +fig, axes = plt.subplots(2, 1, sharey=True, layout="constrained", figsize=(10, 6)) +for ax, data, title, n_fft in zip( + axes, [raw, raw_downsampled_poly], ["Original", "Downsampled (polyphase)"], n_ffts +): + data.compute_psd(n_fft=n_fft).plot( + average=True, picks="data", exclude="bads", axes=ax + ) + ax.set(title=title, xlim=(0, 300)) # %% # Because resampling involves filtering, there are some pitfalls to resampling From 854c0eb018beafa2841663bcbbdec3af1b35e73a Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Thu, 7 Dec 2023 15:34:20 -0500 Subject: [PATCH 107/405] MAINT: Fix for latest PyVista (#12275) --- mne/filter.py | 5 +++-- mne/io/fiff/tests/test_raw_fiff.py | 4 ++-- mne/tests/test_filter.py | 8 ++++---- mne/viz/backends/_pyvista.py | 9 +++++---- 4 files changed, 14 insertions(+), 12 deletions(-) diff --git a/mne/filter.py b/mne/filter.py index 3d9b3ecc7da..b9bc92aa9ce 100644 --- a/mne/filter.py +++ b/mne/filter.py @@ -1978,7 +1978,8 @@ def resample( ) half_len = len(window) // 2 logger.info( - f"Polyphase resampling locality: ±{half_len} input sample{_pl(half_len)}" + f"Polyphase resampling neighborhood: ±{half_len} " + f"input sample{_pl(half_len)}" ) y = _resample_polyphase(x, up=up, down=down, **kwargs) assert y.shape[-1] == final_len @@ -1997,7 +1998,7 @@ def _prep_polyphase(ratio, x_len, final_len, window): g_ = gcd(up, down) up = up // g_ down = down // g_ - # Figure out our signal locality and design window (adapted from SciPy) + # Figure out our signal neighborhood and design window (adapted from SciPy) if not isinstance(window, (list, np.ndarray)): # Design a linear-phase low-pass FIR filter max_rate = max(up, down) diff --git a/mne/io/fiff/tests/test_raw_fiff.py b/mne/io/fiff/tests/test_raw_fiff.py index 2c302eac3ad..bb249809f19 100644 --- a/mne/io/fiff/tests/test_raw_fiff.py +++ b/mne/io/fiff/tests/test_raw_fiff.py @@ -1325,9 +1325,9 @@ def test_resample(tmp_path, preload, n, npad, method): raw_resamp.resample(sfreq, n_jobs=None, verbose=True, **kwargs) log = log.getvalue() if method == "fft": - assert "locality" not in log + assert "neighborhood" not in log else: - assert "locality" in log + assert "neighborhood" in log assert raw_resamp.info["sfreq"] == sfreq assert raw.get_data().shape == raw_resamp._data.shape assert raw.first_samp == raw_resamp.first_samp diff --git a/mne/tests/test_filter.py b/mne/tests/test_filter.py index 3ab60dba055..36f2da736c3 100644 --- a/mne/tests/test_filter.py +++ b/mne/tests/test_filter.py @@ -383,9 +383,9 @@ def test_resample(method): x_rs = resample(x, 1, 2, npad=10, method=method, verbose=True) log = log.getvalue() if method == "fft": - assert "locality" not in log + assert "neighborhood" not in log else: - assert "locality" in log + assert "neighborhood" in log assert x.shape == (10, 10, 10) assert x_rs.shape == (10, 10, 5) @@ -502,9 +502,9 @@ def test_resample_below_1_sample(method): epochs.resample(1, method=method, verbose=True) log = log.getvalue() if method == "fft": - assert "locality" not in log + assert "neighborhood" not in log else: - assert "locality" in log + assert "neighborhood" in log assert len(epochs.times) == 1 assert epochs.get_data(copy=False).shape[2] == 1 diff --git a/mne/viz/backends/_pyvista.py b/mne/viz/backends/_pyvista.py index c1fb06eb8ff..b5d921f3968 100644 --- a/mne/viz/backends/_pyvista.py +++ b/mne/viz/backends/_pyvista.py @@ -108,7 +108,6 @@ def _init( off_screen=False, notebook=False, splash=False, - multi_samples=None, ): self._plotter = plotter self.display = None @@ -123,7 +122,6 @@ def _init( self.store["shape"] = shape self.store["off_screen"] = off_screen self.store["border"] = False - self.store["multi_samples"] = multi_samples self.store["line_smoothing"] = True self.store["polygon_smoothing"] = True self.store["point_smoothing"] = True @@ -234,12 +232,12 @@ def __init__( notebook=notebook, smooth_shading=smooth_shading, splash=splash, - multi_samples=multi_samples, ) self.font_family = "arial" self.tube_n_sides = 20 self.antialias = _get_3d_option("antialias") self.depth_peeling = _get_3d_option("depth_peeling") + self.multi_samples = multi_samples self.smooth_shading = smooth_shading if isinstance(fig, int): saved_fig = _FIGURES.get(fig) @@ -880,7 +878,10 @@ def _toggle_antialias(self): plotter.disable_anti_aliasing() else: if not bad_system: - plotter.enable_anti_aliasing(aa_type="msaa") + plotter.enable_anti_aliasing( + aa_type="msaa", + multi_samples=self.multi_samples, + ) def remove_mesh(self, mesh_data): actor, _ = mesh_data From 06c90a7982eee3b4747bbf6e6afca71014c3e5bf Mon Sep 17 00:00:00 2001 From: Nikolai Kapralov <4dvlup@gmail.com> Date: Thu, 7 Dec 2023 23:27:16 +0100 Subject: [PATCH 108/405] [MRG] DOC: inform about channel discrepancy in make_lcmv (#12238) --- doc/changes/devel.rst | 1 + doc/changes/names.inc | 2 ++ mne/utils/check.py | 24 ++++++++++++++++++++++-- mne/utils/tests/test_check.py | 19 +++++++++++++------ 4 files changed, 38 insertions(+), 8 deletions(-) diff --git a/doc/changes/devel.rst b/doc/changes/devel.rst index fadd872e621..ddd70ab22be 100644 --- a/doc/changes/devel.rst +++ b/doc/changes/devel.rst @@ -35,6 +35,7 @@ enhanced experience if it supports it! Enhancements ~~~~~~~~~~~~ - Speed up export to .edf in :func:`mne.export.export_raw` by using ``edfio`` instead of ``EDFlib-Python`` (:gh:`12218` by :newcontrib:`Florian Hofer`) +- Inform the user about channel discrepancy between provided info, forward operator, and/or covariance matrices in :func:`mne.beamformer.make_lcmv` (:gh:`12238` by :newcontrib:`Nikolai Kapralov`) - We added type hints for the return values of :func:`mne.read_evokeds` and :func:`mne.io.read_raw`. Development environments like VS Code or PyCharm will now provide more help when using these functions in your code. (:gh:`12250` by `Richard Höchenberger`_ and `Eric Larson`_) - Add ``method="polyphase"`` to :meth:`mne.io.Raw.resample` and related functions to allow resampling using :func:`scipy.signal.upfirdn` (:gh:`12268` by `Eric Larson`_) diff --git a/doc/changes/names.inc b/doc/changes/names.inc index 1085716a697..0d62d247dd3 100644 --- a/doc/changes/names.inc +++ b/doc/changes/names.inc @@ -402,6 +402,8 @@ .. _Nikolai Chapochnikov: https://github.com/chapochn +.. _Nikolai Kapralov: https://github.com/ctrltz + .. _Nikolas Chalas: https://github.com/Nichalas .. _Okba Bekhelifi: https://github.com/okbalefthanded diff --git a/mne/utils/check.py b/mne/utils/check.py index 8c2bc5f919d..467bd14e952 100644 --- a/mne/utils/check.py +++ b/mne/utils/check.py @@ -652,7 +652,8 @@ def _check_if_nan(data, msg=" to be plotted"): raise ValueError("Some of the values {} are NaN.".format(msg)) -def _check_info_inv(info, forward, data_cov=None, noise_cov=None): +@verbose +def _check_info_inv(info, forward, data_cov=None, noise_cov=None, verbose=None): """Return good channels common to forward model and covariance matrices.""" from .._fiff.pick import pick_types @@ -696,6 +697,19 @@ def _check_info_inv(info, forward, data_cov=None, noise_cov=None): if noise_cov is not None: ch_names = _compare_ch_names(ch_names, noise_cov.ch_names, noise_cov["bads"]) + # inform about excluding any channels apart from bads and reference + all_bads = info["bads"] + ref_chs + if data_cov is not None: + all_bads += data_cov["bads"] + if noise_cov is not None: + all_bads += noise_cov["bads"] + dropped_nonbads = set(info["ch_names"]) - set(ch_names) - set(all_bads) + if dropped_nonbads: + logger.info( + f"Excluding {len(dropped_nonbads)} channel(s) missing from the " + "provided forward operator and/or covariance matrices" + ) + picks = [info["ch_names"].index(k) for k in ch_names if k in info["ch_names"]] return picks @@ -750,7 +764,13 @@ def _check_one_ch_type(method, info, forward, data_cov=None, noise_cov=None): info_pick = info else: _validate_type(noise_cov, [None, Covariance], "noise_cov") - picks = _check_info_inv(info, forward, data_cov=data_cov, noise_cov=noise_cov) + picks = _check_info_inv( + info, + forward, + data_cov=data_cov, + noise_cov=noise_cov, + verbose=_verbose_safe_false(), + ) info_pick = pick_info(info, picks) ch_types = [_contains_ch_type(info_pick, tt) for tt in ("mag", "grad", "eeg")] if sum(ch_types) > 1: diff --git a/mne/utils/tests/test_check.py b/mne/utils/tests/test_check.py index 4f5f6d5416b..48017b79ae2 100644 --- a/mne/utils/tests/test_check.py +++ b/mne/utils/tests/test_check.py @@ -30,6 +30,7 @@ _safe_input, _suggest, _validate_type, + catch_logging, check_fname, check_random_state, check_version, @@ -141,12 +142,12 @@ def test_check_info_inv(): assert [1, 2] not in picks # covariance matrix data_cov_bads = data_cov.copy() - data_cov_bads["bads"] = data_cov_bads.ch_names[0] + data_cov_bads["bads"] = [data_cov_bads.ch_names[0]] picks = _check_info_inv(epochs.info, forward, data_cov=data_cov_bads) assert 0 not in picks # noise covariance matrix noise_cov_bads = noise_cov.copy() - noise_cov_bads["bads"] = noise_cov_bads.ch_names[1] + noise_cov_bads["bads"] = [noise_cov_bads.ch_names[1]] picks = _check_info_inv(epochs.info, forward, noise_cov=noise_cov_bads) assert 1 not in picks @@ -164,10 +165,16 @@ def test_check_info_inv(): noise_cov = pick_channels_cov( noise_cov, include=[noise_cov.ch_names[ii] for ii in range(7, 12)] ) - picks = _check_info_inv( - epochs.info, forward, noise_cov=noise_cov, data_cov=data_cov - ) - assert list(range(7, 10)) == picks + with catch_logging() as log: + picks = _check_info_inv( + epochs.info, forward, noise_cov=noise_cov, data_cov=data_cov, verbose=True + ) + assert list(range(7, 10)) == picks + + # make sure to inform the user that 7 channels were dropped + # (there are 10 channels in epochs but only 3 were picked) + log = log.getvalue() + assert "Excluding 7 channel(s) missing" in log def test_check_option(): From d00cbb12b9b6070a713ac67fcba19e7443c71ef7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20H=C3=B6chenberger?= Date: Fri, 8 Dec 2023 13:42:32 +0100 Subject: [PATCH 109/405] Use hatchling as build backend (#12269) --- MANIFEST.in | 86 ------------------------------------------- Makefile | 5 +-- azure-pipelines.yml | 4 -- doc/changes/devel.rst | 1 + pyproject.toml | 71 +++++++++++++---------------------- 5 files changed, 28 insertions(+), 139 deletions(-) delete mode 100644 MANIFEST.in diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 5a06c9c814b..00000000000 --- a/MANIFEST.in +++ /dev/null @@ -1,86 +0,0 @@ -include *.rst -include LICENSE.txt -include SECURITY.md -include mne/__init__.py - -recursive-include examples *.py -recursive-include examples *.txt -recursive-include tutorials *.py -recursive-include tutorials *.txt - -recursive-include mne *.py -recursive-include mne *.pyi -recursive-include mne/data * -recursive-include mne/icons * -recursive-include mne/data/helmets * -recursive-include mne/data/image * -recursive-include mne/data/fsaverage * -include mne/datasets/_fsaverage/root.txt -include mne/datasets/_fsaverage/bem.txt -include mne/datasets/_infant/*.txt -include mne/datasets/_phantom/*.txt -include mne/data/dataset_checksums.txt -include mne/data/eegbci_checksums.txt - -recursive-include mne/html_templates *.html.jinja - -recursive-include mne/channels/data/layouts * -recursive-include mne/channels/data/montages * -recursive-include mne/channels/data/neighbors * - -recursive-include mne/gui/help *.json - -recursive-include mne/html *.js -recursive-include mne/html *.css - -recursive-include mne/report * - -recursive-include mne/io/artemis123/resources * - -recursive-include mne mne/datasets *.csv -include mne/io/edf/gdf_encodes.txt -include mne/datasets/sleep_physionet/SHA1SUMS - -### Exclude - -recursive-exclude examples/MNE-sample-data * -recursive-exclude examples/MNE-testing-data * -recursive-exclude examples/MNE-spm-face * -recursive-exclude examples/MNE-somato-data * -recursive-exclude tools * -exclude tools -exclude Makefile -exclude .coveragerc -exclude *.yml -exclude *.yaml -exclude .git-blame-ignore-revs -exclude ignore_words.txt -exclude .mailmap -exclude codemeta.json -exclude CITATION.cff -recursive-exclude mne *.pyc - -recursive-exclude doc * -recursive-exclude logo * - -exclude CONTRIBUTING.md -exclude CODE_OF_CONDUCT.md -exclude .github -exclude .github/CONTRIBUTING.md -exclude .github/ISSUE_TEMPLATE -exclude .github/ISSUE_TEMPLATE/blank.md -exclude .github/ISSUE_TEMPLATE/bug_report.md -exclude .github/ISSUE_TEMPLATE/feature_request.md -exclude .github/PULL_REQUEST_TEMPLATE.md - -# Test files - -recursive-exclude mne/io/tests/data * -recursive-exclude mne/io/besa/tests/data * -recursive-exclude mne/io/bti/tests/data * -recursive-exclude mne/io/edf/tests/data * -recursive-exclude mne/io/kit/tests/data * -recursive-exclude mne/io/brainvision/tests/data * -recursive-exclude mne/io/egi/tests/data * -recursive-exclude mne/io/nicolet/tests/data * -recursive-exclude mne/preprocessing/tests/data * diff --git a/Makefile b/Makefile index 7d5488258d8..8a79bf966c5 100644 --- a/Makefile +++ b/Makefile @@ -25,7 +25,7 @@ clean-cache: clean: clean-build clean-pyc clean-so clean-ctags clean-cache wheel: - $(PYTHON) -m build + $(PYTHON) -m build -w sample_data: @python -c "import mne; mne.datasets.sample.data_path(verbose=True);" @@ -54,9 +54,6 @@ pep: pre-commit codespell: # running manually @codespell --builtin clear,rare,informal,names,usage -w -i 3 -q 3 -S $(CODESPELL_SKIPS) --ignore-words=ignore_words.txt --uri-ignore-words-list=bu $(CODESPELL_DIRS) -check-manifest: - check-manifest -q --ignore .circleci/config.yml,doc,logo,mne/io/*/tests/data*,mne/io/tests/data,mne/preprocessing/tests/data,.DS_Store,.git_archival.txt - check-readme: clean wheel twine check dist/* diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 2bfce3b4378..6cac2d5990f 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -64,10 +64,6 @@ stages: make nesting displayName: make nesting condition: always() - - bash: | - make check-manifest - displayName: make check-manifest - condition: always() - bash: | make check-readme displayName: make check-readme diff --git a/doc/changes/devel.rst b/doc/changes/devel.rst index ddd70ab22be..da82c6cfc4d 100644 --- a/doc/changes/devel.rst +++ b/doc/changes/devel.rst @@ -38,6 +38,7 @@ Enhancements - Inform the user about channel discrepancy between provided info, forward operator, and/or covariance matrices in :func:`mne.beamformer.make_lcmv` (:gh:`12238` by :newcontrib:`Nikolai Kapralov`) - We added type hints for the return values of :func:`mne.read_evokeds` and :func:`mne.io.read_raw`. Development environments like VS Code or PyCharm will now provide more help when using these functions in your code. (:gh:`12250` by `Richard Höchenberger`_ and `Eric Larson`_) - Add ``method="polyphase"`` to :meth:`mne.io.Raw.resample` and related functions to allow resampling using :func:`scipy.signal.upfirdn` (:gh:`12268` by `Eric Larson`_) +- The package build backend was switched from ``setuptools`` to ``hatchling``. This will only affect users who build and install MNE-Python from source. (:gh:`12269` by `Richard Höchenberger`_) Bugs ~~~~ diff --git a/pyproject.toml b/pyproject.toml index 7bb17f07570..d401cdca370 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,7 @@ +[build-system] +requires = ["hatchling", "hatch-vcs"] +build-backend = "hatchling.build" + [project] name = "mne" description = "MNE-Python project for MEG and EEG data analysis." @@ -112,7 +116,6 @@ test = [ "ruff", "numpydoc", "codespell", - "check-manifest", "tomli; python_version<'3.11'", "twine", "wheel", @@ -168,52 +171,30 @@ Documentation = "https://mne.tools/" Forum = "https://mne.discourse.group/" "Source Code" = "https://github.com/mne-tools/mne-python/" -[build-system] -requires = ["setuptools>=45", "setuptools_scm[toml]>=6.2", "wheel"] -build-backend = "setuptools.build_meta" - -[tool.setuptools.packages.find] -where = ["."] -include = ["mne*"] -namespaces = false - -[tool.setuptools_scm] -version_scheme = "release-branch-semver" - -[tool.setuptools] -include-package-data = true - -[tool.setuptools.package-data] -"mne" = [ - "data/eegbci_checksums.txt", - "data/*.sel", - "data/icos.fif.gz", - "data/coil_def*.dat", - "data/helmets/*.fif.gz", - "data/FreeSurferColorLUT.txt", - "data/image/*gif", - "data/image/*lout", - "data/fsaverage/*.fif", - "channels/data/layouts/*.lout", - "channels/data/layouts/*.lay", - "channels/data/montages/*.sfp", - "channels/data/montages/*.txt", - "channels/data/montages/*.elc", - "channels/data/neighbors/*.mat", - "datasets/sleep_physionet/SHA1SUMS", - "datasets/_fsaverage/*.txt", - "datasets/_infant/*.txt", - "datasets/_phantom/*.txt", - "html/*.js", - "html/*.css", - "html_templates/repr/*.jinja", - "html_templates/report/*.jinja", - "icons/*.svg", - "icons/*.png", - "io/artemis123/resources/*.csv", - "io/edf/gdf_encodes.txt", +[tool.hatch.build] +exclude = [ + "/.*", + "/*.yml", + "/*.yaml", + "/*.toml", + "/*.txt", + "/mne/**/tests", + "/logo", + "/doc", + "/tools", + "/tutorials", + "/examples", + "/CITATION.cff", + "/codemeta.json", + "/ignore_words.txt", + "/Makefile", + "/CONTRIBUTING.md", ] +[tool.hatch.version] +source = "vcs" +raw-options = { version_scheme = "release-branch-semver" } + [tool.codespell] ignore-words = "ignore_words.txt" builtin = "clear,rare,informal,names,usage" From 59e50247c8fc4d6d5c968b9d9b6207b7ff5d6b24 Mon Sep 17 00:00:00 2001 From: Clemens Brunner Date: Fri, 8 Dec 2023 19:09:20 +0100 Subject: [PATCH 110/405] Update year and use "official" text (#12278) --- LICENSE.txt | 31 +++++++++---------------------- 1 file changed, 9 insertions(+), 22 deletions(-) diff --git a/LICENSE.txt b/LICENSE.txt index 6d98ee83925..c9197c42f20 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,24 +1,11 @@ -Copyright © 2011-2022, authors of MNE-Python -All rights reserved. +Copyright 2011-2023 MNE-Python authors -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - * Neither the name of the copyright holder nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY -DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file From fd5008a48a1819034f8cf94dfc0e31f7f1a74ba5 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Fri, 8 Dec 2023 15:32:29 -0500 Subject: [PATCH 111/405] BUG: Fix bug with parent dir check (#12282) --- doc/changes/devel.rst | 1 + mne/io/base.py | 7 +++++++ mne/io/fiff/tests/test_raw_fiff.py | 4 ++++ 3 files changed, 12 insertions(+) diff --git a/doc/changes/devel.rst b/doc/changes/devel.rst index da82c6cfc4d..422754ba4a5 100644 --- a/doc/changes/devel.rst +++ b/doc/changes/devel.rst @@ -45,6 +45,7 @@ Bugs - Allow :func:`mne.viz.plot_compare_evokeds` to plot eyetracking channels, and improve error handling (:gh:`12190` by `Scott Huberty`_) - Fix bug with accessing the last data sample using ``raw[:, -1]`` where an empty array was returned (:gh:`12248` by `Eric Larson`_) - Remove incorrect type hints in :func:`mne.io.read_raw_neuralynx` (:gh:`12236` by `Richard Höchenberger`_) +- Fix bug where parent directory existence was not checked properly in :meth:`mne.io.Raw.save` (:gh:`12282` by `Eric Larson`_) - ``defusedxml`` is now an optional (rather than required) dependency and needed when reading EGI-MFF data, NEDF data, and BrainVision montages (:gh:`12264` by `Eric Larson`_) API changes diff --git a/mne/io/base.py b/mne/io/base.py index 652b747a8ac..6bd92607eb2 100644 --- a/mne/io/base.py +++ b/mne/io/base.py @@ -2563,6 +2563,13 @@ def set_annotations(self, annotations): def _write_raw(raw_fid_writer, fpath, split_naming, overwrite): """Write raw file with splitting.""" dir_path = fpath.parent + _check_fname( + dir_path, + overwrite="read", + must_exist=True, + name="parent directory", + need_dir=True, + ) # We have to create one extra filename here to make the for loop below happy, # but it will raise an error if it actually gets used split_fnames = _make_split_fnames( diff --git a/mne/io/fiff/tests/test_raw_fiff.py b/mne/io/fiff/tests/test_raw_fiff.py index bb249809f19..329d205e8d3 100644 --- a/mne/io/fiff/tests/test_raw_fiff.py +++ b/mne/io/fiff/tests/test_raw_fiff.py @@ -771,6 +771,10 @@ def test_io_raw(tmp_path): sl = slice(inds[0], inds[1]) assert_allclose(data[:, sl], raw[:, sl][0], rtol=1e-6, atol=1e-20) + # missing dir raises informative error + with pytest.raises(FileNotFoundError, match="parent directory does not exist"): + raw.save(tmp_path / "foo" / "test_raw.fif", split_size="1MB") + @pytest.mark.parametrize( "fname_in, fname_out", From 8af33df490f94c3dd628cfc23beafed1a6cc6361 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20H=C3=B6chenberger?= Date: Fri, 8 Dec 2023 22:57:13 +0100 Subject: [PATCH 112/405] Clean up .gitignore a bit and fix packaging (#12281) Co-authored-by: Eric Larson --- .github/workflows/tests.yml | 1 - .gitignore | 1 - doc/changes/devel.rst | 2 +- examples/visualization/3d_to_2d.py | 9 +- mne/conftest.py | 5 + mne/data/image/custom_layout.lout | 257 --------------------------- mne/data/image/mni_brain.gif | Bin 12051 -> 0 bytes mne/datasets/config.py | 4 +- pyproject.toml | 10 +- tools/github_actions_dependencies.sh | 6 +- tools/github_actions_install.sh | 5 - tools/github_actions_test.sh | 13 +- 12 files changed, 36 insertions(+), 277 deletions(-) delete mode 100644 mne/data/image/custom_layout.lout delete mode 100644 mne/data/image/mni_brain.gif delete mode 100755 tools/github_actions_install.sh diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 1f3f0eb7ea8..3a0517d59e1 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -102,7 +102,6 @@ jobs: # Minimal commands on Linux (macOS stalls) - run: ./tools/get_minimal_commands.sh if: ${{ startswith(matrix.os, 'ubuntu') }} - - run: ./tools/github_actions_install.sh - run: ./tools/github_actions_infos.sh # Check Qt - run: ./tools/check_qt_import.sh $MNE_QT_BACKEND diff --git a/.gitignore b/.gitignore index 564599c864a..51707aa39e0 100644 --- a/.gitignore +++ b/.gitignore @@ -41,7 +41,6 @@ MNE-brainstorm-data* physionet-sleep-data* MEGSIM* build -mne/_version.py coverage htmlcov .cache/ diff --git a/doc/changes/devel.rst b/doc/changes/devel.rst index 422754ba4a5..3fd579ad4be 100644 --- a/doc/changes/devel.rst +++ b/doc/changes/devel.rst @@ -38,7 +38,7 @@ Enhancements - Inform the user about channel discrepancy between provided info, forward operator, and/or covariance matrices in :func:`mne.beamformer.make_lcmv` (:gh:`12238` by :newcontrib:`Nikolai Kapralov`) - We added type hints for the return values of :func:`mne.read_evokeds` and :func:`mne.io.read_raw`. Development environments like VS Code or PyCharm will now provide more help when using these functions in your code. (:gh:`12250` by `Richard Höchenberger`_ and `Eric Larson`_) - Add ``method="polyphase"`` to :meth:`mne.io.Raw.resample` and related functions to allow resampling using :func:`scipy.signal.upfirdn` (:gh:`12268` by `Eric Larson`_) -- The package build backend was switched from ``setuptools`` to ``hatchling``. This will only affect users who build and install MNE-Python from source. (:gh:`12269` by `Richard Höchenberger`_) +- The package build backend was switched from ``setuptools`` to ``hatchling``. This will only affect users who build and install MNE-Python from source. (:gh:`12269`, :gh:`12281` by `Richard Höchenberger`_) Bugs ~~~~ diff --git a/examples/visualization/3d_to_2d.py b/examples/visualization/3d_to_2d.py index 6d8e8674fa3..47b223e8396 100644 --- a/examples/visualization/3d_to_2d.py +++ b/examples/visualization/3d_to_2d.py @@ -23,8 +23,6 @@ # Copyright the MNE-Python contributors. # %% -from os.path import dirname -from pathlib import Path import numpy as np from matplotlib import pyplot as plt @@ -43,8 +41,7 @@ ecog_data_fname = subjects_dir / "sample_ecog_ieeg.fif" # We've already clicked and exported -layout_path = Path(dirname(mne.__file__)) / "data" / "image" -layout_name = "custom_layout.lout" +layout_name = subjects_dir / "custom_layout.lout" # %% # Load data @@ -128,10 +125,10 @@ # # Generate a layout from our clicks and normalize by the image # print('Generating and saving layout...') # lt = click.to_layout() -# lt.save(layout_path / layout_name) # save if we want +# lt.save(layout_name) # save if we want # # We've already got the layout, load it -lt = mne.channels.read_layout(layout_path / layout_name, scale=False) +lt = mne.channels.read_layout(layout_name, scale=False) x = lt.pos[:, 0] * float(im.shape[1]) y = (1 - lt.pos[:, 1]) * float(im.shape[0]) # Flip the y-position fig, ax = plt.subplots(layout="constrained") diff --git a/mne/conftest.py b/mne/conftest.py index 40a317b7da9..ba2bfd51dfa 100644 --- a/mne/conftest.py +++ b/mne/conftest.py @@ -984,6 +984,11 @@ def pytest_terminal_summary(terminalreporter, exitstatus, config): writer.line(f"{timing.ljust(15)}{name}") +def pytest_report_header(config, startdir): + """Add information to the pytest run header.""" + return f"MNE {mne.__version__} -- {str(Path(mne.__file__).parent)}" + + @pytest.fixture(scope="function", params=("Numba", "NumPy")) def numba_conditional(monkeypatch, request): """Test both code paths on machines that have Numba.""" diff --git a/mne/data/image/custom_layout.lout b/mne/data/image/custom_layout.lout deleted file mode 100644 index ab5b81408cb..00000000000 --- a/mne/data/image/custom_layout.lout +++ /dev/null @@ -1,257 +0,0 @@ - 0.00 0.00 0.01 0.02 -000 0.79 0.46 0.07 0.05 0 -001 0.78 0.48 0.07 0.05 1 -002 0.76 0.51 0.07 0.05 2 -003 0.74 0.53 0.07 0.05 3 -004 0.72 0.55 0.07 0.05 4 -005 0.71 0.57 0.07 0.05 5 -006 0.69 0.59 0.07 0.05 6 -007 0.67 0.62 0.07 0.05 7 -008 0.66 0.64 0.07 0.05 8 -009 0.64 0.66 0.07 0.05 9 -010 0.62 0.68 0.07 0.05 10 -011 0.61 0.69 0.07 0.05 11 -012 0.59 0.71 0.07 0.05 12 -013 0.58 0.73 0.07 0.05 13 -014 0.56 0.75 0.07 0.05 14 -015 0.54 0.77 0.07 0.05 15 -016 0.77 0.44 0.07 0.05 16 -017 0.75 0.46 0.07 0.05 17 -018 0.73 0.49 0.07 0.05 18 -019 0.72 0.51 0.07 0.05 19 -020 0.70 0.54 0.07 0.05 20 -021 0.68 0.56 0.07 0.05 21 -022 0.66 0.58 0.07 0.05 22 -023 0.65 0.60 0.07 0.05 23 -024 0.63 0.62 0.07 0.05 24 -025 0.62 0.64 0.07 0.05 25 -026 0.60 0.66 0.07 0.05 26 -027 0.58 0.68 0.07 0.05 27 -028 0.57 0.70 0.07 0.05 28 -029 0.55 0.71 0.07 0.05 29 -030 0.53 0.73 0.07 0.05 30 -031 0.52 0.75 0.07 0.05 31 -032 0.75 0.42 0.07 0.05 32 -033 0.73 0.45 0.07 0.05 33 -034 0.71 0.47 0.07 0.05 34 -035 0.69 0.50 0.07 0.05 35 -036 0.68 0.52 0.07 0.05 36 -037 0.66 0.54 0.07 0.05 37 -038 0.64 0.57 0.07 0.05 38 -039 0.62 0.58 0.07 0.05 39 -040 0.61 0.61 0.07 0.05 40 -041 0.59 0.62 0.07 0.05 41 -042 0.58 0.64 0.07 0.05 42 -043 0.56 0.66 0.07 0.05 43 -044 0.54 0.68 0.07 0.05 44 -045 0.53 0.70 0.07 0.05 45 -046 0.51 0.72 0.07 0.05 46 -047 0.50 0.74 0.07 0.05 47 -048 0.72 0.41 0.07 0.05 48 -049 0.71 0.43 0.07 0.05 49 -050 0.69 0.46 0.07 0.05 50 -051 0.67 0.48 0.07 0.05 51 -052 0.65 0.50 0.07 0.05 52 -053 0.63 0.52 0.07 0.05 53 -054 0.62 0.55 0.07 0.05 54 -055 0.60 0.57 0.07 0.05 55 -056 0.58 0.59 0.07 0.05 56 -057 0.57 0.61 0.07 0.05 57 -058 0.55 0.63 0.07 0.05 58 -059 0.54 0.65 0.07 0.05 59 -060 0.52 0.67 0.07 0.05 60 -061 0.51 0.69 0.07 0.05 61 -062 0.49 0.71 0.07 0.05 62 -063 0.47 0.73 0.07 0.05 63 -064 0.70 0.39 0.07 0.05 64 -065 0.68 0.41 0.07 0.05 65 -066 0.66 0.44 0.07 0.05 66 -067 0.65 0.46 0.07 0.05 67 -068 0.63 0.49 0.07 0.05 68 -069 0.61 0.51 0.07 0.05 69 -070 0.59 0.53 0.07 0.05 70 -071 0.58 0.55 0.07 0.05 71 -072 0.56 0.57 0.07 0.05 72 -073 0.55 0.59 0.07 0.05 73 -074 0.53 0.61 0.07 0.05 74 -075 0.51 0.64 0.07 0.05 75 -076 0.50 0.66 0.07 0.05 76 -077 0.48 0.68 0.07 0.05 77 -078 0.47 0.69 0.07 0.05 78 -079 0.45 0.72 0.07 0.05 79 -080 0.68 0.38 0.07 0.05 80 -081 0.66 0.40 0.07 0.05 81 -082 0.64 0.42 0.07 0.05 82 -083 0.62 0.44 0.07 0.05 83 -084 0.60 0.47 0.07 0.05 84 -085 0.59 0.49 0.07 0.05 85 -086 0.57 0.51 0.07 0.05 86 -087 0.55 0.54 0.07 0.05 87 -088 0.54 0.56 0.07 0.05 88 -089 0.52 0.58 0.07 0.05 89 -090 0.50 0.60 0.07 0.05 90 -091 0.49 0.62 0.07 0.05 91 -092 0.47 0.64 0.07 0.05 92 -093 0.46 0.66 0.07 0.05 93 -094 0.44 0.68 0.07 0.05 94 -095 0.42 0.70 0.07 0.05 95 -096 0.65 0.36 0.07 0.05 96 -097 0.63 0.38 0.07 0.05 97 -098 0.61 0.41 0.07 0.05 98 -099 0.60 0.43 0.07 0.05 99 -100 0.58 0.45 0.07 0.05 100 -101 0.56 0.47 0.07 0.05 101 -102 0.55 0.50 0.07 0.05 102 -103 0.53 0.52 0.07 0.05 103 -104 0.51 0.54 0.07 0.05 104 -105 0.50 0.56 0.07 0.05 105 -106 0.48 0.58 0.07 0.05 106 -107 0.47 0.61 0.07 0.05 107 -108 0.45 0.63 0.07 0.05 108 -109 0.44 0.65 0.07 0.05 109 -110 0.42 0.67 0.07 0.05 110 -111 0.41 0.69 0.07 0.05 111 -112 0.63 0.34 0.07 0.05 112 -113 0.61 0.36 0.07 0.05 113 -114 0.59 0.39 0.07 0.05 114 -115 0.58 0.41 0.07 0.05 115 -116 0.56 0.43 0.07 0.05 116 -117 0.54 0.46 0.07 0.05 117 -118 0.52 0.48 0.07 0.05 118 -119 0.51 0.51 0.07 0.05 119 -120 0.49 0.52 0.07 0.05 120 -121 0.47 0.55 0.07 0.05 121 -122 0.46 0.57 0.07 0.05 122 -123 0.44 0.59 0.07 0.05 123 -124 0.43 0.61 0.07 0.05 124 -125 0.41 0.63 0.07 0.05 125 -126 0.40 0.65 0.07 0.05 126 -127 0.38 0.67 0.07 0.05 127 -128 0.60 0.32 0.07 0.05 128 -129 0.59 0.35 0.07 0.05 129 -130 0.56 0.37 0.07 0.05 130 -131 0.55 0.39 0.07 0.05 131 -132 0.53 0.42 0.07 0.05 132 -133 0.52 0.44 0.07 0.05 133 -134 0.50 0.46 0.07 0.05 134 -135 0.48 0.49 0.07 0.05 135 -136 0.47 0.51 0.07 0.05 136 -137 0.45 0.53 0.07 0.05 137 -138 0.43 0.56 0.07 0.05 138 -139 0.42 0.57 0.07 0.05 139 -140 0.40 0.60 0.07 0.05 140 -141 0.39 0.61 0.07 0.05 141 -142 0.37 0.63 0.07 0.05 142 -143 0.36 0.66 0.07 0.05 143 -144 0.58 0.31 0.07 0.05 144 -145 0.56 0.33 0.07 0.05 145 -146 0.54 0.35 0.07 0.05 146 -147 0.53 0.38 0.07 0.05 147 -148 0.51 0.40 0.07 0.05 148 -149 0.49 0.42 0.07 0.05 149 -150 0.48 0.45 0.07 0.05 150 -151 0.46 0.47 0.07 0.05 151 -152 0.44 0.49 0.07 0.05 152 -153 0.42 0.51 0.07 0.05 153 -154 0.41 0.53 0.07 0.05 154 -155 0.39 0.56 0.07 0.05 155 -156 0.38 0.58 0.07 0.05 156 -157 0.36 0.60 0.07 0.05 157 -158 0.35 0.62 0.07 0.05 158 -159 0.33 0.64 0.07 0.05 159 -160 0.55 0.29 0.07 0.05 160 -161 0.54 0.32 0.07 0.05 161 -162 0.52 0.34 0.07 0.05 162 -163 0.50 0.36 0.07 0.05 163 -164 0.49 0.38 0.07 0.05 164 -165 0.47 0.41 0.07 0.05 165 -166 0.45 0.43 0.07 0.05 166 -167 0.43 0.45 0.07 0.05 167 -168 0.42 0.48 0.07 0.05 168 -169 0.40 0.50 0.07 0.05 169 -170 0.39 0.52 0.07 0.05 170 -171 0.37 0.54 0.07 0.05 171 -172 0.36 0.56 0.07 0.05 172 -173 0.34 0.58 0.07 0.05 173 -174 0.33 0.60 0.07 0.05 174 -175 0.31 0.62 0.07 0.05 175 -176 0.53 0.27 0.07 0.05 176 -177 0.52 0.30 0.07 0.05 177 -178 0.50 0.32 0.07 0.05 178 -179 0.48 0.34 0.07 0.05 179 -180 0.46 0.37 0.07 0.05 180 -181 0.45 0.39 0.07 0.05 181 -182 0.43 0.41 0.07 0.05 182 -183 0.41 0.43 0.07 0.05 183 -184 0.40 0.46 0.07 0.05 184 -185 0.38 0.48 0.07 0.05 185 -186 0.36 0.50 0.07 0.05 186 -187 0.35 0.53 0.07 0.05 187 -188 0.33 0.55 0.07 0.05 188 -189 0.32 0.57 0.07 0.05 189 -190 0.30 0.59 0.07 0.05 190 -191 0.29 0.61 0.07 0.05 191 -192 0.51 0.26 0.07 0.05 192 -193 0.49 0.28 0.07 0.05 193 -194 0.47 0.31 0.07 0.05 194 -195 0.46 0.33 0.07 0.05 195 -196 0.44 0.35 0.07 0.05 196 -197 0.42 0.37 0.07 0.05 197 -198 0.41 0.40 0.07 0.05 198 -199 0.39 0.42 0.07 0.05 199 -200 0.37 0.44 0.07 0.05 200 -201 0.36 0.46 0.07 0.05 201 -202 0.34 0.49 0.07 0.05 202 -203 0.32 0.51 0.07 0.05 203 -204 0.31 0.53 0.07 0.05 204 -205 0.29 0.55 0.07 0.05 205 -206 0.28 0.57 0.07 0.05 206 -207 0.27 0.59 0.07 0.05 207 -208 0.48 0.24 0.07 0.05 208 -209 0.47 0.26 0.07 0.05 209 -210 0.45 0.28 0.07 0.05 210 -211 0.43 0.31 0.07 0.05 211 -212 0.41 0.33 0.07 0.05 212 -213 0.40 0.35 0.07 0.05 213 -214 0.38 0.38 0.07 0.05 214 -215 0.37 0.40 0.07 0.05 215 -216 0.35 0.42 0.07 0.05 216 -217 0.33 0.45 0.07 0.05 217 -218 0.32 0.47 0.07 0.05 218 -219 0.30 0.49 0.07 0.05 219 -220 0.28 0.51 0.07 0.05 220 -221 0.27 0.53 0.07 0.05 221 -222 0.25 0.55 0.07 0.05 222 -223 0.24 0.58 0.07 0.05 223 -224 0.46 0.23 0.07 0.05 224 -225 0.45 0.25 0.07 0.05 225 -226 0.43 0.27 0.07 0.05 226 -227 0.41 0.29 0.07 0.05 227 -228 0.39 0.31 0.07 0.05 228 -229 0.38 0.34 0.07 0.05 229 -230 0.36 0.36 0.07 0.05 230 -231 0.34 0.38 0.07 0.05 231 -232 0.33 0.41 0.07 0.05 232 -233 0.31 0.43 0.07 0.05 233 -234 0.29 0.45 0.07 0.05 234 -235 0.28 0.47 0.07 0.05 235 -236 0.26 0.50 0.07 0.05 236 -237 0.25 0.52 0.07 0.05 237 -238 0.24 0.54 0.07 0.05 238 -239 0.22 0.56 0.07 0.05 239 -240 0.44 0.21 0.07 0.05 240 -241 0.42 0.23 0.07 0.05 241 -242 0.41 0.25 0.07 0.05 242 -243 0.39 0.27 0.07 0.05 243 -244 0.37 0.30 0.07 0.05 244 -245 0.35 0.32 0.07 0.05 245 -246 0.33 0.34 0.07 0.05 246 -247 0.32 0.37 0.07 0.05 247 -248 0.30 0.39 0.07 0.05 248 -249 0.28 0.41 0.07 0.05 249 -250 0.27 0.43 0.07 0.05 250 -251 0.25 0.46 0.07 0.05 251 -252 0.24 0.48 0.07 0.05 252 -253 0.23 0.50 0.07 0.05 253 -254 0.21 0.52 0.07 0.05 254 -255 0.20 0.54 0.07 0.05 255 diff --git a/mne/data/image/mni_brain.gif b/mne/data/image/mni_brain.gif deleted file mode 100644 index 3d6cc08edbde8d9b2b83cfa9bc687e640efaed51..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12051 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 Nz8TW-_fD(m{{oK&3vK`a diff --git a/mne/datasets/config.py b/mne/datasets/config.py index 6778a1e7cc9..b548f5273f2 100644 --- a/mne/datasets/config.py +++ b/mne/datasets/config.py @@ -88,7 +88,7 @@ # respective repos, and make a new release of the dataset on GitHub. Then # update the checksum in the MNE_DATASETS dict below, and change version # here: ↓↓↓↓↓ ↓↓↓ -RELEASES = dict(testing="0.150", misc="0.26") +RELEASES = dict(testing="0.150", misc="0.27") TESTING_VERSIONED = f'mne-testing-data-{RELEASES["testing"]}' MISC_VERSIONED = f'mne-misc-data-{RELEASES["misc"]}' @@ -126,7 +126,7 @@ ) MNE_DATASETS["misc"] = dict( archive_name=f"{MISC_VERSIONED}.tar.gz", # 'mne-misc-data', - hash="md5:868b484fadd73b1d1a3535b7194a0d03", + hash="md5:e343d3a00cb49f8a2f719d14f4758afe", url=( "https://codeload.github.com/mne-tools/mne-misc-data/tar.gz/" f'{RELEASES["misc"]}' diff --git a/pyproject.toml b/pyproject.toml index d401cdca370..fb8757150ac 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -172,6 +172,12 @@ Forum = "https://mne.discourse.group/" "Source Code" = "https://github.com/mne-tools/mne-python/" [tool.hatch.build] +artifacts = [ + "/mne/data/**/*.dat", + "/mne/data/**/*.fif", + "/mne/data/**/*.fif.gz", + "/mne/icons/**/*.png", +] # excluded via .gitignore, but we do want to ship those files exclude = [ "/.*", "/*.yml", @@ -184,12 +190,12 @@ exclude = [ "/tools", "/tutorials", "/examples", - "/CITATION.cff", "/codemeta.json", "/ignore_words.txt", "/Makefile", + "/CITATION.cff", "/CONTRIBUTING.md", -] +] # tracked by git, but we don't want to ship those files [tool.hatch.version] source = "vcs" diff --git a/tools/github_actions_dependencies.sh b/tools/github_actions_dependencies.sh index 69cf6413fb2..9489a95f397 100755 --- a/tools/github_actions_dependencies.sh +++ b/tools/github_actions_dependencies.sh @@ -3,11 +3,15 @@ set -o pipefail STD_ARGS="--progress-bar off --upgrade" +INSTALL_ARGS="-e" INSTALL_KIND="test_extra,hdf5" if [ ! -z "$CONDA_ENV" ]; then echo "Uninstalling MNE for CONDA_ENV=${CONDA_ENV}" conda remove -c conda-forge --force -yq mne python -m pip uninstall -y mne + if [[ "${RUNNER_OS}" != "Windows" ]]; then + INSTALL_ARGS="" + fi elif [ ! -z "$CONDA_DEPENDENCIES" ]; then echo "Using Mamba to install CONDA_DEPENDENCIES=${CONDA_DEPENDENCIES}" mamba install -y $CONDA_DEPENDENCIES @@ -59,4 +63,4 @@ fi echo "" echo "Installing test dependencies using pip" -python -m pip install $STD_ARGS -e .[$INSTALL_KIND] +python -m pip install $STD_ARGS $INSTALL_ARGS .[$INSTALL_KIND] diff --git a/tools/github_actions_install.sh b/tools/github_actions_install.sh deleted file mode 100755 index f52c193d773..00000000000 --- a/tools/github_actions_install.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash - -set -eo pipefail - -pip install -ve . diff --git a/tools/github_actions_test.sh b/tools/github_actions_test.sh index f218197bda6..78cc063d016 100755 --- a/tools/github_actions_test.sh +++ b/tools/github_actions_test.sh @@ -12,6 +12,17 @@ if [ "${MNE_CI_KIND}" == "notebook" ]; then else USE_DIRS="mne/" fi +JUNIT_PATH="junit-results.xml" +if [[ ! -z "$CONDA_ENV" ]] && [[ "${RUNNER_OS}" != "Windows" ]]; then + JUNIT_PATH="$(pwd)/${JUNIT_PATH}" + # Use the installed version after adding all (excluded) test files + cd .. + INSTALL_PATH=$(python -c "import mne, pathlib; print(str(pathlib.Path(mne.__file__).parents[1]))") + echo "Copying tests from $(pwd)/mne-python/mne/ to ${INSTALL_PATH}/mne/" + rsync -a --partial --progress --prune-empty-dirs --exclude="*.pyc" --include="**/" --include="**/tests/*" --include="**/tests/data/**" --exclude="**" ./mne-python/mne/ ${INSTALL_PATH}/mne/ + cd $INSTALL_PATH + echo "Executing from $(pwd)" +fi set -x -pytest -m "${CONDITION}" --tb=short --cov=mne --cov-report xml -vv ${USE_DIRS} +pytest -m "${CONDITION}" --tb=short --cov=mne --cov-report xml --color=yes --junit-xml=$JUNIT_PATH -vv ${USE_DIRS} set +x From 8e500a3f4c8e37136c72e13e060f819b711198f1 Mon Sep 17 00:00:00 2001 From: Clemens Brunner Date: Sun, 10 Dec 2023 12:46:55 +0100 Subject: [PATCH 113/405] Remove license text in README (#12284) --- README.rst | 35 +---------------------------------- 1 file changed, 1 insertion(+), 34 deletions(-) diff --git a/README.rst b/README.rst index ca4e08becba..433c6a1d82f 100644 --- a/README.rst +++ b/README.rst @@ -134,40 +134,7 @@ About License ^^^^^^^ -MNE-Python is **BSD-licensed** (BSD-3-Clause): - - This software is OSI Certified Open Source Software. - OSI Certified is a certification mark of the Open Source Initiative. - - Copyright (c) 2011-2022, authors of MNE-Python. - All rights reserved. - - Redistribution and use in source and binary forms, with or without - modification, are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright notice, - this list of conditions and the following disclaimer. - - * Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - - * Neither the names of MNE-Python authors nor the names of any - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - - **This software is provided by the copyright holders and contributors - "as is" and any express or implied warranties, including, but not - limited to, the implied warranties of merchantability and fitness for - a particular purpose are disclaimed. In no event shall the copyright - owner or contributors be liable for any direct, indirect, incidental, - special, exemplary, or consequential damages (including, but not - limited to, procurement of substitute goods or services; loss of use, - data, or profits; or business interruption) however caused and on any - theory of liability, whether in contract, strict liability, or tort - (including negligence or otherwise) arising in any way out of the use - of this software, even if advised of the possibility of such - damage.** +MNE-Python is licensed under the BSD-3-Clause license. .. _Documentation: https://mne.tools/dev/ From bf03a03c91bc64b8a9cf72d75a399cfcb89c0662 Mon Sep 17 00:00:00 2001 From: Clemens Brunner Date: Sun, 10 Dec 2023 22:40:51 +0100 Subject: [PATCH 114/405] Simplify spectrum.plot() parameters (#12270) Co-authored-by: Daniel McCloy --- examples/datasets/brainstorm_data.py | 4 +- examples/preprocessing/eeg_csd.py | 4 +- examples/preprocessing/find_ref_artifacts.py | 6 +- mne/io/array/tests/test_array.py | 6 +- mne/report/report.py | 6 +- mne/time_frequency/spectrum.py | 63 +++++++++++-------- mne/time_frequency/tests/test_spectrum.py | 6 +- mne/viz/tests/test_epochs.py | 8 +-- mne/viz/tests/test_raw.py | 31 ++++----- mne/viz/utils.py | 33 +++------- tutorials/clinical/60_sleep.py | 1 + tutorials/epochs/20_visualize_epochs.py | 2 +- tutorials/epochs/30_epochs_metadata.py | 2 +- tutorials/intro/10_overview.py | 14 ++--- .../inverse/80_brainstorm_phantom_elekta.py | 4 +- tutorials/inverse/95_phantom_KIT.py | 2 +- tutorials/io/60_ctf_bst_auditory.py | 8 ++- .../10_preprocessing_overview.py | 2 +- .../preprocessing/30_filtering_resampling.py | 18 ++++-- .../50_artifact_correction_ssp.py | 9 ++- tutorials/preprocessing/59_head_positions.py | 2 +- .../preprocessing/70_fnirs_processing.py | 4 +- tutorials/raw/40_visualize_raw.py | 14 ++--- tutorials/simulation/10_array_objs.py | 2 +- tutorials/time-freq/10_spectrum_class.py | 8 +-- .../time-freq/20_sensors_time_frequency.py | 4 +- 26 files changed, 146 insertions(+), 117 deletions(-) diff --git a/examples/datasets/brainstorm_data.py b/examples/datasets/brainstorm_data.py index 0f32c704284..6331c9f1b29 100644 --- a/examples/datasets/brainstorm_data.py +++ b/examples/datasets/brainstorm_data.py @@ -41,7 +41,9 @@ raw.set_eeg_reference("average", projection=True) # show power line interference and remove it -raw.compute_psd(tmax=60).plot(average=False, picks="data", exclude="bads") +raw.compute_psd(tmax=60).plot( + average=False, amplitude=False, picks="data", exclude="bads" +) raw.notch_filter(np.arange(60, 181, 60), fir_design="firwin") events = mne.find_events(raw, stim_channel="UPPT001") diff --git a/examples/preprocessing/eeg_csd.py b/examples/preprocessing/eeg_csd.py index 73515e1f043..35ba959c34d 100644 --- a/examples/preprocessing/eeg_csd.py +++ b/examples/preprocessing/eeg_csd.py @@ -49,8 +49,8 @@ # %% # Also look at the power spectral densities: -raw.compute_psd().plot(picks="data", exclude="bads") -raw_csd.compute_psd().plot(picks="data", exclude="bads") +raw.compute_psd().plot(picks="data", exclude="bads", amplitude=False) +raw_csd.compute_psd().plot(picks="data", exclude="bads", amplitude=False) # %% # CSD can also be computed on Evoked (averaged) data. diff --git a/examples/preprocessing/find_ref_artifacts.py b/examples/preprocessing/find_ref_artifacts.py index 93b96e89e9c..90e3d1fb0da 100644 --- a/examples/preprocessing/find_ref_artifacts.py +++ b/examples/preprocessing/find_ref_artifacts.py @@ -70,7 +70,7 @@ # %% # The PSD of these data show the noise as clear peaks. -raw.compute_psd(fmax=30).plot(picks="data", exclude="bads") +raw.compute_psd(fmax=30).plot(picks="data", exclude="bads", amplitude=False) # %% # Run the "together" algorithm. @@ -99,7 +99,7 @@ # %% # Cleaned data: -raw_tog.compute_psd(fmax=30).plot(picks="data", exclude="bads") +raw_tog.compute_psd(fmax=30).plot(picks="data", exclude="bads", amplitude=False) # %% # Now try the "separate" algorithm. @@ -143,7 +143,7 @@ # %% # Cleaned raw data PSD: -raw_sep.compute_psd(fmax=30).plot(picks="data", exclude="bads") +raw_sep.compute_psd(fmax=30).plot(picks="data", exclude="bads", amplitude=False) ############################################################################## # References diff --git a/mne/io/array/tests/test_array.py b/mne/io/array/tests/test_array.py index e8013d631aa..10b7c834d98 100644 --- a/mne/io/array/tests/test_array.py +++ b/mne/io/array/tests/test_array.py @@ -151,7 +151,9 @@ def test_array_raw(): # plotting raw2.plot() - raw2.compute_psd(tmax=2.0, n_fft=1024).plot(average=True, spatial_colors=False) + raw2.compute_psd(tmax=2.0, n_fft=1024).plot( + average=True, amplitude=False, spatial_colors=False + ) plt.close("all") # epoching @@ -184,5 +186,5 @@ def test_array_raw(): raw = RawArray(data, info) raw.set_montage(montage) spectrum = raw.compute_psd() - spectrum.plot(average=False) # looking for nonexistent layout + spectrum.plot(average=False, amplitude=False) # looking for nonexistent layout spectrum.plot_topo() diff --git a/mne/report/report.py b/mne/report/report.py index 6a37f095c2f..9a547d4f7b6 100644 --- a/mne/report/report.py +++ b/mne/report/report.py @@ -3218,7 +3218,9 @@ def _add_raw( init_kwargs, plot_kwargs = _split_psd_kwargs(kwargs=add_psd) init_kwargs.setdefault("fmax", fmax) plot_kwargs.setdefault("show", False) - fig = raw.compute_psd(**init_kwargs).plot(**plot_kwargs) + with warnings.catch_warnings(): + warnings.simplefilter(action="ignore", category=FutureWarning) + fig = raw.compute_psd(**init_kwargs).plot(**plot_kwargs) _constrain_fig_resolution(fig, max_width=MAX_IMG_WIDTH, max_res=MAX_IMG_RES) self._add_figure( fig=fig, @@ -3785,7 +3787,7 @@ def _add_epochs_psd(self, *, epochs, psd, image_format, tags, section, replace): if fmax > 0.5 * epochs.info["sfreq"]: fmax = np.inf - fig = epochs_for_psd.compute_psd(fmax=fmax).plot(show=False) + fig = epochs_for_psd.compute_psd(fmax=fmax).plot(amplitude=False, show=False) _constrain_fig_resolution(fig, max_width=MAX_IMG_WIDTH, max_res=MAX_IMG_RES) duration = round(epoch_duration * len(epochs_for_psd), 1) caption = ( diff --git a/mne/time_frequency/spectrum.py b/mne/time_frequency/spectrum.py index 2b31ca46340..a7a2a753932 100644 --- a/mne/time_frequency/spectrum.py +++ b/mne/time_frequency/spectrum.py @@ -573,7 +573,7 @@ def plot( picks=None, average=False, dB=True, - amplitude="auto", + amplitude=None, xscale="linear", ci="sd", ci_alpha=0.3, @@ -593,52 +593,52 @@ def plot( .. versionchanged:: 1.5 In version 1.5, the default behavior changed so that all - :term:`data channels` (not just "good" data channels) are shown - by default. + :term:`data channels` (not just "good" data channels) are shown by + default. average : bool - Whether to average across channels before plotting. If ``True``, - interactive plotting of scalp topography is disabled, and - parameters ``ci`` and ``ci_alpha`` control the style of the - confidence band around the mean. Default is ``False``. + Whether to average across channels before plotting. If ``True``, interactive + plotting of scalp topography is disabled, and parameters ``ci`` and + ``ci_alpha`` control the style of the confidence band around the mean. + Default is ``False``. %(dB_spectrum_plot)s amplitude : bool | 'auto' 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``). If ``'auto'``, will plot a power spectrum when ``dB=True`` and + an amplitude spectrum otherwise. Default is ``'auto'``. + + .. versionchanged:: 1.8 + In version 1.8, the value ``amplitude="auto"`` will be removed. The + default value will change to ``amplitude=False``. %(xscale_plot_psd)s ci : float | 'sd' | 'range' | None - Type of confidence band drawn around the mean when - ``average=True``. If ``'sd'`` the band spans ±1 standard deviation - across channels. If ``'range'`` the band spans the range across - channels at each frequency. If a :class:`float`, it indicates the - (bootstrapped) confidence interval to display, and must satisfy - ``0 < ci <= 100``. If ``None``, no band is drawn. Default is - ``sd``. + Type of confidence band drawn around the mean when ``average=True``. If + ``'sd'`` the band spans ±1 standard deviation across channels. If + ``'range'`` the band spans the range across channels at each frequency. If a + :class:`float`, it indicates the (bootstrapped) confidence interval to + display, and must satisfy ``0 < ci <= 100``. If ``None``, no band is drawn. + Default is ``sd``. ci_alpha : float - Opacity of the confidence band. Must satisfy - ``0 <= ci_alpha <= 1``. Default is 0.3. + Opacity of the confidence band. Must satisfy ``0 <= ci_alpha <= 1``. Default + is 0.3. %(color_plot_psd)s alpha : float | None Opacity of the spectrum line(s). If :class:`float`, must satisfy ``0 <= alpha <= 1``. If ``None``, opacity will be ``1`` when - ``average=True`` and ``0.1`` when ``average=False``. Default is - ``None``. + ``average=True`` and ``0.1`` when ``average=False``. Default is ``None``. %(spatial_colors_psd)s %(sphere_topomap_auto)s %(exclude_spectrum_plot)s .. versionchanged:: 1.5 - In version 1.5, the default behavior changed from - ``exclude='bads'`` to ``exclude=()``. + In version 1.5, the default behavior changed from ``exclude='bads'`` to + ``exclude=()``. %(axes_spectrum_plot_topomap)s %(show)s Returns ------- fig : instance of matplotlib.figure.Figure - Figure with spectra plotted in separate subplots for each channel - type. + Figure with spectra plotted in separate subplots for each channel type. """ # Must nest this _mpl_figure import because of the BACKEND global # stuff @@ -652,10 +652,19 @@ def plot( scalings = _handle_default("scalings", None) titles = _handle_default("titles", None) units = _handle_default("units", None) - if amplitude == "auto": + + 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: # amplitude is boolean + else: estimate = "amplitude" if amplitude else "power" + + logger.info(f"Plotting {estimate} spectral density ({dB=}).") + # split picks by channel type picks = _picks_to_idx( self.info, picks, "data", exclude=exclude, with_ref_meg=False diff --git a/mne/time_frequency/tests/test_spectrum.py b/mne/time_frequency/tests/test_spectrum.py index 653f0ab1411..26c18529143 100644 --- a/mne/time_frequency/tests/test_spectrum.py +++ b/mne/time_frequency/tests/test_spectrum.py @@ -1,5 +1,6 @@ # License: BSD-3-Clause # Copyright the MNE-Python contributors. + from functools import partial import numpy as np @@ -270,12 +271,13 @@ def test_spectrum_kwarg_triaging(raw): import matplotlib.pyplot as plt regex = r"legacy plot_psd\(\) method.*unexpected keyword.*'axes'.*Try rewriting" - fig, axes = plt.subplots(1, 2) + _, axes = plt.subplots(1, 2) # `axes` is the new param name: technically only valid for Spectrum.plot() with pytest.warns(RuntimeWarning, match=regex): raw.plot_psd(axes=axes) # `ax` is the correct legacy param name - raw.plot_psd(ax=axes) + with pytest.warns(FutureWarning, match="amplitude='auto'"): + raw.plot_psd(ax=axes) def _check_spectrum_equivalent(spect1, spect2, tmp_path): diff --git a/mne/viz/tests/test_epochs.py b/mne/viz/tests/test_epochs.py index cf1c07b7d85..6dcfdb57bdf 100644 --- a/mne/viz/tests/test_epochs.py +++ b/mne/viz/tests/test_epochs.py @@ -396,9 +396,9 @@ def test_plot_psd_epochs(epochs): """Test plotting epochs psd (+topomap).""" spectrum = epochs.compute_psd() old_defaults = dict(picks="data", exclude="bads") - spectrum.plot(average=True, spatial_colors=False, **old_defaults) - spectrum.plot(average=False, spatial_colors=True, **old_defaults) - spectrum.plot(average=False, spatial_colors=False, **old_defaults) + spectrum.plot(average=True, amplitude=False, spatial_colors=False, **old_defaults) + spectrum.plot(average=False, amplitude=False, spatial_colors=True, **old_defaults) + spectrum.plot(average=False, amplitude=False, spatial_colors=False, **old_defaults) # test plot_psd_topomap errors with pytest.raises(RuntimeError, match="No frequencies in band"): spectrum.plot_topomap(bands=dict(foo=(0, 0.01))) @@ -497,7 +497,7 @@ def test_plot_psd_epochs_ctf(raw_ctf): for dB in [True, False]: spectrum.plot(dB=dB) spectrum.drop_channels(["EEG060"]) - spectrum.plot(spatial_colors=False, average=False, **old_defaults) + spectrum.plot(spatial_colors=False, average=False, amplitude=False, **old_defaults) with pytest.raises(RuntimeError, match="No frequencies in band"): spectrum.plot_topomap(bands=[(0, 0.01, "foo")]) spectrum.plot_topomap() diff --git a/mne/viz/tests/test_raw.py b/mne/viz/tests/test_raw.py index 89619d36e2f..619c79a7111 100644 --- a/mne/viz/tests/test_raw.py +++ b/mne/viz/tests/test_raw.py @@ -938,29 +938,33 @@ def test_plot_raw_psd(raw, raw_orig): spectrum = raw.compute_psd() # deprecation change handler old_defaults = dict(picks="data", exclude="bads") - fig = spectrum.plot(average=False) + fig = spectrum.plot(average=False, amplitude=False) # normal mode - fig = spectrum.plot(average=False, **old_defaults) + fig = spectrum.plot(average=False, amplitude=False, **old_defaults) fig.canvas.callbacks.process( "resize_event", backend_bases.ResizeEvent("resize_event", fig.canvas) ) # specific mode picks = pick_types(spectrum.info, meg="mag", eeg=False)[:4] - spectrum.plot(picks=picks, ci="range", spatial_colors=True, exclude="bads") - raw.compute_psd(tmax=20.0).plot(color="yellow", dB=False, alpha=0.4, **old_defaults) + spectrum.plot( + picks=picks, ci="range", spatial_colors=True, exclude="bads", amplitude=False + ) + raw.compute_psd(tmax=20.0).plot( + color="yellow", dB=False, alpha=0.4, amplitude=True, **old_defaults + ) plt.close("all") # one axes supplied ax = plt.axes() - spectrum.plot(picks=picks, axes=ax, average=True, exclude="bads") + spectrum.plot(picks=picks, axes=ax, average=True, exclude="bads", amplitude=False) plt.close("all") # two axes supplied _, axs = plt.subplots(2) - spectrum.plot(axes=axs, average=True, **old_defaults) + spectrum.plot(axes=axs, average=True, amplitude=False, **old_defaults) plt.close("all") # need 2, got 1 ax = plt.axes() with pytest.raises(ValueError, match="of length 2.*the length is 1"): - spectrum.plot(axes=ax, average=True, **old_defaults) + spectrum.plot(axes=ax, average=True, amplitude=False, **old_defaults) plt.close("all") # topo psd ax = plt.subplot() @@ -981,14 +985,13 @@ def test_plot_raw_psd(raw, raw_orig): # check grad axes title = fig.axes[0].get_title() ylabel = fig.axes[0].get_ylabel() - ends_dB = ylabel.endswith("mathrm{(dB)}$") unit = r"fT/cm/\sqrt{Hz}" if amplitude else "(fT/cm)²/Hz" assert title == "Gradiometers", title assert unit in ylabel, ylabel if dB: - assert ends_dB, ylabel + assert "dB" in ylabel else: - assert not ends_dB, ylabel + assert "dB" not in ylabel # check mag axes title = fig.axes[1].get_title() ylabel = fig.axes[1].get_ylabel() @@ -1006,8 +1009,8 @@ def test_plot_raw_psd(raw, raw_orig): raw = raw_orig.crop(0, 1) picks = pick_types(raw.info, meg=True) spectrum = raw.compute_psd(picks=picks) - spectrum.plot(average=False, **old_defaults) - spectrum.plot(average=True, **old_defaults) + spectrum.plot(average=False, amplitude=False, **old_defaults) + spectrum.plot(average=True, amplitude=False, **old_defaults) plt.close("all") raw.set_channel_types( { @@ -1018,7 +1021,7 @@ def test_plot_raw_psd(raw, raw_orig): }, verbose="error", ) - fig = raw.compute_psd().plot(**old_defaults) + fig = raw.compute_psd().plot(amplitude=False, **old_defaults) assert len(fig.axes) == 10 plt.close("all") @@ -1029,7 +1032,7 @@ def test_plot_raw_psd(raw, raw_orig): raw = RawArray(data, info) picks = pick_types(raw.info, misc=True) spectrum = raw.compute_psd(picks=picks, n_fft=n_fft) - spectrum.plot(spatial_colors=False, picks=picks, exclude="bads") + spectrum.plot(spatial_colors=False, picks=picks, exclude="bads", amplitude=False) plt.close("all") diff --git a/mne/viz/utils.py b/mne/viz/utils.py index dd8323a2f85..180d2b37595 100644 --- a/mne/viz/utils.py +++ b/mne/viz/utils.py @@ -2360,29 +2360,12 @@ def _make_combine_callable(combine): def _convert_psds( psds, dB, estimate, scaling, unit, ch_names=None, first_dim="channel" ): - """Convert PSDs to dB (if necessary) and appropriate units. - - The following table summarizes the relationship between the value of - parameters ``dB`` and ``estimate``, and the type of plot and corresponding - units. - - | dB | estimate | plot | units | - |-------+-------------+------+-------------------| - | True | 'power' | PSD | amp**2/Hz (dB) | - | True | 'amplitude' | ASD | amp/sqrt(Hz) (dB) | - | True | 'auto' | PSD | amp**2/Hz (dB) | - | False | 'power' | PSD | amp**2/Hz | - | False | 'amplitude' | ASD | amp/sqrt(Hz) | - | False | 'auto' | ASD | amp/sqrt(Hz) | - - where amp are the units corresponding to the variable, as specified by - ``unit``. - """ + """Convert PSDs to dB (if necessary) and appropriate units.""" _check_option("first_dim", first_dim, ["channel", "epoch"]) where = np.where(psds.min(1) <= 0)[0] if len(where) > 0: - # Construct a helpful error message, depending on whether the first - # dimension of `psds` are channels or epochs. + # Construct a helpful error message, depending on whether the first dimension of + # `psds` corresponds to channels or epochs. if dB: bad_value = "Infinite" else: @@ -2404,16 +2387,18 @@ def _convert_psds( if estimate == "amplitude": np.sqrt(psds, out=psds) psds *= scaling - ylabel = r"$\mathrm{%s/\sqrt{Hz}}$" % unit + ylabel = rf"$\mathrm{{{unit}/\sqrt{{Hz}}}}$" else: psds *= scaling * scaling if "/" in unit: - unit = "(%s)" % unit - ylabel = r"$\mathrm{%s²/Hz}$" % unit + unit = f"({unit})" + ylabel = rf"$\mathrm{{{unit}²/Hz}}$" if dB: np.log10(np.maximum(psds, np.finfo(float).tiny), out=psds) psds *= 10 - ylabel += r"$\ \mathrm{(dB)}$" + ylabel = r"$\mathrm{dB}\ $" + ylabel + ylabel = "Power (" + ylabel if estimate == "power" else "Amplitude (" + ylabel + ylabel += ")" return ylabel diff --git a/tutorials/clinical/60_sleep.py b/tutorials/clinical/60_sleep.py index 020d00bab7e..25273a0ff2f 100644 --- a/tutorials/clinical/60_sleep.py +++ b/tutorials/clinical/60_sleep.py @@ -219,6 +219,7 @@ axes=ax, show=False, average=True, + amplitude=False, spatial_colors=False, picks="data", exclude="bads", diff --git a/tutorials/epochs/20_visualize_epochs.py b/tutorials/epochs/20_visualize_epochs.py index 69864d19e26..e311b324ee8 100644 --- a/tutorials/epochs/20_visualize_epochs.py +++ b/tutorials/epochs/20_visualize_epochs.py @@ -144,7 +144,7 @@ # :class:`~mne.time_frequency.EpochsSpectrum`'s # :meth:`~mne.time_frequency.EpochsSpectrum.plot` method. -epochs["auditory"].compute_psd().plot(picks="eeg", exclude="bads") +epochs["auditory"].compute_psd().plot(picks="eeg", exclude="bads", amplitude=False) # %% # It is also possible to plot spectral power estimates across sensors as a diff --git a/tutorials/epochs/30_epochs_metadata.py b/tutorials/epochs/30_epochs_metadata.py index 7d5c06871ad..51d551090d4 100644 --- a/tutorials/epochs/30_epochs_metadata.py +++ b/tutorials/epochs/30_epochs_metadata.py @@ -116,7 +116,7 @@ # MNE-Python will try the traditional method first before falling back on rich # metadata querying. -epochs["solenoid"].compute_psd().plot(picks="data", exclude="bads") +epochs["solenoid"].compute_psd().plot(picks="data", exclude="bads", amplitude=False) # %% # One use of the Pandas query string approach is to select specific words for diff --git a/tutorials/intro/10_overview.py b/tutorials/intro/10_overview.py index 20dc532f65a..94b659444b3 100644 --- a/tutorials/intro/10_overview.py +++ b/tutorials/intro/10_overview.py @@ -5,12 +5,12 @@ Overview of MEG/EEG analysis with MNE-Python ============================================ -This tutorial covers the basic EEG/MEG pipeline for event-related analysis: -loading data, epoching, averaging, plotting, and estimating cortical activity -from sensor data. It introduces the core MNE-Python data structures -`~mne.io.Raw`, `~mne.Epochs`, `~mne.Evoked`, and `~mne.SourceEstimate`, and -covers a lot of ground fairly quickly (at the expense of depth). Subsequent -tutorials address each of these topics in greater detail. +This tutorial covers the basic EEG/MEG pipeline for event-related analysis: loading +data, epoching, averaging, plotting, and estimating cortical activity from sensor data. +It introduces the core MNE-Python data structures `~mne.io.Raw`, `~mne.Epochs`, +`~mne.Evoked`, and `~mne.SourceEstimate`, and covers a lot of ground fairly quickly (at +the expense of depth). Subsequent tutorials address each of these topics in greater +detail. We begin by importing the necessary Python modules: """ @@ -79,7 +79,7 @@ # sessions, `~mne.io.Raw.plot` is interactive and allows scrolling, scaling, # bad channel marking, annotations, projector toggling, etc. -raw.compute_psd(fmax=50).plot(picks="data", exclude="bads") +raw.compute_psd(fmax=50).plot(picks="data", exclude="bads", amplitude=False) raw.plot(duration=5, n_channels=30) # %% diff --git a/tutorials/inverse/80_brainstorm_phantom_elekta.py b/tutorials/inverse/80_brainstorm_phantom_elekta.py index 8184badeda3..303be4260d1 100644 --- a/tutorials/inverse/80_brainstorm_phantom_elekta.py +++ b/tutorials/inverse/80_brainstorm_phantom_elekta.py @@ -53,7 +53,9 @@ # noise (five peaks around 300 Hz). Here, we use only the first 30 seconds # to save memory: -raw.compute_psd(tmax=30).plot(average=False, picks="data", exclude="bads") +raw.compute_psd(tmax=30).plot( + average=False, amplitude=False, picks="data", exclude="bads" +) # %% # Our phantom produces sinusoidal bursts at 20 Hz: diff --git a/tutorials/inverse/95_phantom_KIT.py b/tutorials/inverse/95_phantom_KIT.py index 444ae4635fd..6a07658e13a 100644 --- a/tutorials/inverse/95_phantom_KIT.py +++ b/tutorials/inverse/95_phantom_KIT.py @@ -40,7 +40,7 @@ # boxcar windowing of the 11 Hz sinusoid. spectrum = raw.copy().crop(0, 60).compute_psd(n_fft=10000) -fig = spectrum.plot() +fig = spectrum.plot(amplitude=False) fig.axes[0].set_xlim(0, 50) dip_freq = 11.0 fig.axes[0].axvline(dip_freq, color="r", ls="--", lw=2, zorder=4) diff --git a/tutorials/io/60_ctf_bst_auditory.py b/tutorials/io/60_ctf_bst_auditory.py index dd8d9abadf5..450b8237db4 100644 --- a/tutorials/io/60_ctf_bst_auditory.py +++ b/tutorials/io/60_ctf_bst_auditory.py @@ -165,10 +165,14 @@ # saving mode we do the filtering at evoked stage, which is not something you # usually would do. if not use_precomputed: - raw.compute_psd(tmax=np.inf, picks="meg").plot(picks="data", exclude="bads") + raw.compute_psd(tmax=np.inf, picks="meg").plot( + picks="data", exclude="bads", amplitude=False + ) notches = np.arange(60, 181, 60) raw.notch_filter(notches, phase="zero-double", fir_design="firwin2") - raw.compute_psd(tmax=np.inf, picks="meg").plot(picks="data", exclude="bads") + raw.compute_psd(tmax=np.inf, picks="meg").plot( + picks="data", exclude="bads", amplitude=False + ) # %% # We also lowpass filter the data at 100 Hz to remove the hf components. diff --git a/tutorials/preprocessing/10_preprocessing_overview.py b/tutorials/preprocessing/10_preprocessing_overview.py index 483ac653767..d70fa4b4811 100644 --- a/tutorials/preprocessing/10_preprocessing_overview.py +++ b/tutorials/preprocessing/10_preprocessing_overview.py @@ -141,7 +141,7 @@ # use :meth:`~mne.io.Raw.compute_psd` to illustrate. fig = raw.compute_psd(tmax=np.inf, fmax=250).plot( - average=True, picks="data", exclude="bads" + average=True, amplitude=False, picks="data", exclude="bads" ) # add some arrows at 60 Hz and its harmonics: for ax in fig.axes[1:]: diff --git a/tutorials/preprocessing/30_filtering_resampling.py b/tutorials/preprocessing/30_filtering_resampling.py index 6c118c99180..a3be45e1ec2 100644 --- a/tutorials/preprocessing/30_filtering_resampling.py +++ b/tutorials/preprocessing/30_filtering_resampling.py @@ -123,7 +123,7 @@ def add_arrows(axes): - # add some arrows at 60 Hz and its harmonics + """Add some arrows at 60 Hz and its harmonics.""" for ax in axes: freqs = ax.lines[-1].get_xdata() psds = ax.lines[-1].get_ydata() @@ -143,7 +143,9 @@ def add_arrows(axes): ) -fig = raw.compute_psd(fmax=250).plot(average=True, picks="data", exclude="bads") +fig = raw.compute_psd(fmax=250).plot( + average=True, amplitude=False, picks="data", exclude="bads" +) add_arrows(fig.axes[:2]) # %% @@ -159,7 +161,9 @@ def add_arrows(axes): freqs = (60, 120, 180, 240) raw_notch = raw.copy().notch_filter(freqs=freqs, picks=meg_picks) for title, data in zip(["Un", "Notch "], [raw, raw_notch]): - fig = data.compute_psd(fmax=250).plot(average=True, picks="data", exclude="bads") + fig = data.compute_psd(fmax=250).plot( + average=True, amplitude=False, picks="data", exclude="bads" + ) fig.suptitle("{}filtered".format(title), size="xx-large", weight="bold") add_arrows(fig.axes[:2]) @@ -178,7 +182,9 @@ def add_arrows(axes): freqs=freqs, picks=meg_picks, method="spectrum_fit", filter_length="10s" ) for title, data in zip(["Un", "spectrum_fit "], [raw, raw_notch_fit]): - fig = data.compute_psd(fmax=250).plot(average=True, picks="data", exclude="bads") + fig = data.compute_psd(fmax=250).plot( + average=True, amplitude=False, picks="data", exclude="bads" + ) fig.suptitle("{}filtered".format(title), size="xx-large", weight="bold") add_arrows(fig.axes[:2]) @@ -218,7 +224,7 @@ def add_arrows(axes): axes, [raw, raw_downsampled], ["Original", "Downsampled"], n_ffts ): fig = data.compute_psd(n_fft=n_fft).plot( - average=True, picks="data", exclude="bads", axes=ax + average=True, amplitude=False, picks="data", exclude="bads", axes=ax ) ax.set(title=title, xlim=(0, 300)) @@ -256,7 +262,7 @@ def add_arrows(axes): axes, [raw, raw_downsampled_poly], ["Original", "Downsampled (polyphase)"], n_ffts ): data.compute_psd(n_fft=n_fft).plot( - average=True, picks="data", exclude="bads", axes=ax + average=True, amplitude=False, picks="data", exclude="bads", axes=ax ) ax.set(title=title, xlim=(0, 300)) diff --git a/tutorials/preprocessing/50_artifact_correction_ssp.py b/tutorials/preprocessing/50_artifact_correction_ssp.py index 2f5af536a3d..b99d068430b 100644 --- a/tutorials/preprocessing/50_artifact_correction_ssp.py +++ b/tutorials/preprocessing/50_artifact_correction_ssp.py @@ -116,7 +116,14 @@ raw.info["bads"] = ["MEG 2443"] spectrum = empty_room_raw.compute_psd() for average in (False, True): - spectrum.plot(average=average, dB=False, xscale="log", picks="data", exclude="bads") + spectrum.plot( + average=average, + dB=False, + amplitude=True, + xscale="log", + picks="data", + exclude="bads", + ) # %% # Creating the empty-room projectors diff --git a/tutorials/preprocessing/59_head_positions.py b/tutorials/preprocessing/59_head_positions.py index cd1a454fd7b..37ed574132b 100644 --- a/tutorials/preprocessing/59_head_positions.py +++ b/tutorials/preprocessing/59_head_positions.py @@ -37,7 +37,7 @@ data_path = op.join(mne.datasets.testing.data_path(verbose=True), "SSS") fname_raw = op.join(data_path, "test_move_anon_raw.fif") raw = mne.io.read_raw_fif(fname_raw, allow_maxshield="yes").load_data() -raw.compute_psd().plot(picks="data", exclude="bads") +raw.compute_psd().plot(picks="data", exclude="bads", amplitude=False) # %% # We can use `mne.chpi.get_chpi_info` to retrieve the coil frequencies, diff --git a/tutorials/preprocessing/70_fnirs_processing.py b/tutorials/preprocessing/70_fnirs_processing.py index 8b59c6a31ff..cf0b63da311 100644 --- a/tutorials/preprocessing/70_fnirs_processing.py +++ b/tutorials/preprocessing/70_fnirs_processing.py @@ -157,7 +157,9 @@ raw_haemo_unfiltered = raw_haemo.copy() raw_haemo.filter(0.05, 0.7, h_trans_bandwidth=0.2, l_trans_bandwidth=0.02) for when, _raw in dict(Before=raw_haemo_unfiltered, After=raw_haemo).items(): - fig = _raw.compute_psd().plot(average=True, picks="data", exclude="bads") + fig = _raw.compute_psd().plot( + average=True, amplitude=False, picks="data", exclude="bads" + ) fig.suptitle(f"{when} filtering", weight="bold", size="x-large") # %% diff --git a/tutorials/raw/40_visualize_raw.py b/tutorials/raw/40_visualize_raw.py index 0056d90e413..091f44a1493 100644 --- a/tutorials/raw/40_visualize_raw.py +++ b/tutorials/raw/40_visualize_raw.py @@ -5,13 +5,13 @@ Built-in plotting methods for Raw objects ========================================= -This tutorial shows how to plot continuous data as a time series, how to plot -the spectral density of continuous data, and how to plot the sensor locations -and projectors stored in `~mne.io.Raw` objects. +This tutorial shows how to plot continuous data as a time series, how to plot the +spectral density of continuous data, and how to plot the sensor locations and projectors +stored in `~mne.io.Raw` objects. As usual we'll start by importing the modules we need, loading some -:ref:`example data `, and cropping the `~mne.io.Raw` -object to just 60 seconds before loading it into RAM to save memory: +:ref:`example data `, and cropping the `~mne.io.Raw` object to just 60 +seconds before loading it into RAM to save memory: """ # License: BSD-3-Clause # Copyright the MNE-Python contributors. @@ -120,7 +120,7 @@ # object has a :meth:`~mne.time_frequency.Spectrum.plot` method: spectrum = raw.compute_psd() -spectrum.plot(average=True, picks="data", exclude="bads") +spectrum.plot(average=True, picks="data", exclude="bads", amplitude=False) # %% # If the data have been filtered, vertical dashed lines will automatically @@ -134,7 +134,7 @@ # documentation of `~mne.time_frequency.Spectrum.plot` for full details): midline = ["EEG 002", "EEG 012", "EEG 030", "EEG 048", "EEG 058", "EEG 060"] -spectrum.plot(picks=midline, exclude="bads") +spectrum.plot(picks=midline, exclude="bads", amplitude=False) # %% # It is also possible to plot spectral power estimates across sensors as a diff --git a/tutorials/simulation/10_array_objs.py b/tutorials/simulation/10_array_objs.py index a2e94ab1c7a..4367d880207 100644 --- a/tutorials/simulation/10_array_objs.py +++ b/tutorials/simulation/10_array_objs.py @@ -232,4 +232,4 @@ info=info, ) -spectrum.plot(spatial_colors=False) +spectrum.plot(spatial_colors=False, amplitude=False) diff --git a/tutorials/time-freq/10_spectrum_class.py b/tutorials/time-freq/10_spectrum_class.py index c5f8f4fd639..9d7eb9fae5d 100644 --- a/tutorials/time-freq/10_spectrum_class.py +++ b/tutorials/time-freq/10_spectrum_class.py @@ -8,9 +8,9 @@ The Spectrum and EpochsSpectrum classes: frequency-domain data ============================================================== -This tutorial shows how to create and visualize frequency-domain -representations of your data, starting from continuous :class:`~mne.io.Raw`, -discontinuous :class:`~mne.Epochs`, or averaged :class:`~mne.Evoked` data. +This tutorial shows how to create and visualize frequency-domain representations of your +data, starting from continuous :class:`~mne.io.Raw`, discontinuous :class:`~mne.Epochs`, +or averaged :class:`~mne.Evoked` data. As usual we'll start by importing the modules we need, and loading our :ref:`sample dataset `: @@ -122,7 +122,7 @@ # (interpolated scalp topography of power, in specific frequency bands). A few # plot options are demonstrated below; see the docstrings for full details. -evk_spectrum.plot(picks="data", exclude="bads") +evk_spectrum.plot(picks="data", exclude="bads", amplitude=False) evk_spectrum.plot_topo(color="k", fig_facecolor="w", axis_facecolor="w") # %% diff --git a/tutorials/time-freq/20_sensors_time_frequency.py b/tutorials/time-freq/20_sensors_time_frequency.py index c4981b2b1e0..247fdddfab1 100644 --- a/tutorials/time-freq/20_sensors_time_frequency.py +++ b/tutorials/time-freq/20_sensors_time_frequency.py @@ -66,7 +66,9 @@ # %% # Let's first check out all channel types by averaging across epochs. -epochs.compute_psd(fmin=2.0, fmax=40.0).plot(average=True, picks="data", exclude="bads") +epochs.compute_psd(fmin=2.0, fmax=40.0).plot( + average=True, amplitude=False, picks="data", exclude="bads" +) # %% # Now, let's take a look at the spatial distributions of the PSD, averaged From 742065d5dd6c69223cc15bb7aca212e5d1a99988 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Dec 2023 14:35:22 +0000 Subject: [PATCH 115/405] Bump actions/setup-python from 4 to 5 (#12287) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Eric Larson --- .github/workflows/release.yml | 2 +- .github/workflows/tests.yml | 4 ++-- examples/time_frequency/source_power_spectrum_opm.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index dd85f1bb8a4..c34bb80fd38 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: '3.10' - name: Install dependencies diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 3a0517d59e1..419595c8354 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -20,7 +20,7 @@ jobs: timeout-minutes: 3 steps: - uses: actions/checkout@v4 - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: '3.11' - uses: pre-commit/action@v3.0.0 @@ -84,7 +84,7 @@ jobs: qt: true pyvista: false # Python (if pip) - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python }} if: startswith(matrix.kind, 'pip') diff --git a/examples/time_frequency/source_power_spectrum_opm.py b/examples/time_frequency/source_power_spectrum_opm.py index 11168cc08a5..8a12b78a9d3 100644 --- a/examples/time_frequency/source_power_spectrum_opm.py +++ b/examples/time_frequency/source_power_spectrum_opm.py @@ -82,7 +82,7 @@ fig = ( raws[kind] .compute_psd(n_fft=n_fft, proj=True) - .plot(picks="data", exclude="bads") + .plot(picks="data", exclude="bads", amplitude=True) ) fig.suptitle(titles[kind]) From 5df4cd6506ca2fb244070865a92bdbba8dabc1c4 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 11 Dec 2023 21:18:53 +0000 Subject: [PATCH 116/405] [pre-commit.ci] pre-commit autoupdate (#12288) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 52a3d560fdc..cd6d522d4e7 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.1.6 + rev: v0.1.7 hooks: - id: ruff name: ruff lint mne @@ -13,7 +13,7 @@ repos: # Ruff tutorials and examples - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.6 + rev: v0.1.7 hooks: - id: ruff name: ruff lint tutorials and examples From 4e2a60073c5c42030f825b7495032a91e5d7e722 Mon Sep 17 00:00:00 2001 From: Daniel McCloy Date: Tue, 12 Dec 2023 13:15:01 -0600 Subject: [PATCH 117/405] support different time formats in Annotations.to_data_frame() (#12289) Co-authored-by: Eric Larson --- doc/changes/devel.rst | 1 + mne/annotations.py | 17 ++++++++++++++--- mne/epochs.py | 2 +- mne/evoked.py | 2 +- mne/io/base.py | 4 +++- mne/source_estimate.py | 2 +- mne/tests/test_annotations.py | 11 ++++++++--- mne/time_frequency/tfr.py | 2 +- mne/utils/dataframe.py | 4 ++-- 9 files changed, 32 insertions(+), 13 deletions(-) diff --git a/doc/changes/devel.rst b/doc/changes/devel.rst index 3fd579ad4be..feae12dcbb2 100644 --- a/doc/changes/devel.rst +++ b/doc/changes/devel.rst @@ -39,6 +39,7 @@ Enhancements - We added type hints for the return values of :func:`mne.read_evokeds` and :func:`mne.io.read_raw`. Development environments like VS Code or PyCharm will now provide more help when using these functions in your code. (:gh:`12250` by `Richard Höchenberger`_ and `Eric Larson`_) - Add ``method="polyphase"`` to :meth:`mne.io.Raw.resample` and related functions to allow resampling using :func:`scipy.signal.upfirdn` (:gh:`12268` by `Eric Larson`_) - The package build backend was switched from ``setuptools`` to ``hatchling``. This will only affect users who build and install MNE-Python from source. (:gh:`12269`, :gh:`12281` by `Richard Höchenberger`_) +- :meth:`mne.Annotations.to_data_frame` can now output different formats for the ``onset`` column: seconds, milliseconds, datetime objects, and timedelta objects. (:gh:`12289` by `Daniel McCloy`_) Bugs ~~~~ diff --git a/mne/annotations.py b/mne/annotations.py index 20ee351e7fa..8a44a84f539 100644 --- a/mne/annotations.py +++ b/mne/annotations.py @@ -38,6 +38,8 @@ _check_fname, _check_option, _check_pandas_installed, + _check_time_format, + _convert_times, _DefaultEventParser, _dt_to_stamp, _is_numeric, @@ -442,9 +444,16 @@ def delete(self, idx): self.description = np.delete(self.description, idx) self.ch_names = np.delete(self.ch_names, idx) - def to_data_frame(self): + @fill_doc + def to_data_frame(self, time_format="datetime"): """Export annotations in tabular structure as a pandas DataFrame. + Parameters + ---------- + %(time_format_df_raw)s + + .. versionadded:: 1.7 + Returns ------- result : pandas.DataFrame @@ -453,12 +462,14 @@ def to_data_frame(self): annotations are channel-specific. """ pd = _check_pandas_installed(strict=True) + valid_time_formats = ["ms", "timedelta", "datetime"] dt = _handle_meas_date(self.orig_time) if dt is None: dt = _handle_meas_date(0) + time_format = _check_time_format(time_format, valid_time_formats, dt) dt = dt.replace(tzinfo=None) - onsets_dt = [dt + timedelta(seconds=o) for o in self.onset] - df = dict(onset=onsets_dt, duration=self.duration, description=self.description) + times = _convert_times(self.onset, time_format, dt) + df = dict(onset=times, duration=self.duration, description=self.description) if self._any_ch_names(): df.update(ch_names=self.ch_names) df = pd.DataFrame(df) diff --git a/mne/epochs.py b/mne/epochs.py index 915670af516..0ae911ac5ae 100644 --- a/mne/epochs.py +++ b/mne/epochs.py @@ -2661,7 +2661,7 @@ def to_data_frame( # prepare extra columns / multiindex mindex = list() times = np.tile(times, n_epochs) - times = _convert_times(self, times, time_format) + times = _convert_times(times, time_format, self.info["meas_date"]) mindex.append(("time", times)) rev_event_id = {v: k for k, v in self.event_id.items()} conditions = [rev_event_id[k] for k in self.events[:, 2]] diff --git a/mne/evoked.py b/mne/evoked.py index 93583edb004..b23a4fc112c 100644 --- a/mne/evoked.py +++ b/mne/evoked.py @@ -1256,7 +1256,7 @@ def to_data_frame( data = _scale_dataframe_data(self, data, picks, scalings) # prepare extra columns / multiindex mindex = list() - times = _convert_times(self, times, time_format) + times = _convert_times(times, time_format, self.info["meas_date"]) mindex.append(("time", times)) # build DataFrame df = _build_data_frame( diff --git a/mne/io/base.py b/mne/io/base.py index 6bd92607eb2..94cd2ffcdd0 100644 --- a/mne/io/base.py +++ b/mne/io/base.py @@ -2271,7 +2271,9 @@ def to_data_frame( data = _scale_dataframe_data(self, data, picks, scalings) # prepare extra columns / multiindex mindex = list() - times = _convert_times(self, times, time_format) + times = _convert_times( + times, time_format, self.info["meas_date"], self.first_time + ) mindex.append(("time", times)) # build DataFrame df = _build_data_frame( diff --git a/mne/source_estimate.py b/mne/source_estimate.py index 50734817431..b2d197d7b2f 100644 --- a/mne/source_estimate.py +++ b/mne/source_estimate.py @@ -1398,7 +1398,7 @@ def to_data_frame( if self.subject is not None: default_index = ["subject", "time"] mindex.append(("subject", np.repeat(self.subject, data.shape[0]))) - times = _convert_times(self, times, time_format) + times = _convert_times(times, time_format) mindex.append(("time", times)) # triage surface vs volume source estimates col_names = list() diff --git a/mne/tests/test_annotations.py b/mne/tests/test_annotations.py index 1a351de5527..8f3124d6a30 100644 --- a/mne/tests/test_annotations.py +++ b/mne/tests/test_annotations.py @@ -1416,7 +1416,8 @@ def test_repr(): assert r == "" -def test_annotation_to_data_frame(): +@pytest.mark.parametrize("time_format", (None, "ms", "datetime", "timedelta")) +def test_annotation_to_data_frame(time_format): """Test annotation class to data frame conversion.""" pytest.importorskip("pandas") onset = np.arange(1, 10) @@ -1427,11 +1428,15 @@ def test_annotation_to_data_frame(): onset=onset, duration=durations, description=description, orig_time=0 ) - df = a.to_data_frame() + df = a.to_data_frame(time_format=time_format) for col in ["onset", "duration", "description"]: assert col in df.columns assert df.description[0] == "yy" - assert (df.onset[1] - df.onset[0]).seconds == 1 + want = 1000 if time_format == "ms" else 1 + got = df.onset[1] - df.onset[0] + if time_format in ("datetime", "timedelta"): + got = got.seconds + assert want == got assert df.groupby("description").count().onset["yy"] == 9 diff --git a/mne/time_frequency/tfr.py b/mne/time_frequency/tfr.py index 400b711512e..279e2c79879 100644 --- a/mne/time_frequency/tfr.py +++ b/mne/time_frequency/tfr.py @@ -1333,7 +1333,7 @@ def to_data_frame( # prepare extra columns / multiindex mindex = list() times = np.tile(times, n_epochs * n_freqs) - times = _convert_times(self, times, time_format) + times = _convert_times(times, time_format, self.info["meas_date"]) mindex.append(("time", times)) freqs = self.freqs freqs = np.tile(np.repeat(freqs, n_times), n_epochs) diff --git a/mne/utils/dataframe.py b/mne/utils/dataframe.py index 599a2f88165..2f70c57c6e7 100644 --- a/mne/utils/dataframe.py +++ b/mne/utils/dataframe.py @@ -35,7 +35,7 @@ def _scale_dataframe_data(inst, data, picks, scalings): return data -def _convert_times(inst, times, time_format): +def _convert_times(times, time_format, meas_date=None, first_time=0): """Convert vector of time in seconds to ms, datetime, or timedelta.""" # private function; pandas already checked in calling function from pandas import to_timedelta @@ -45,7 +45,7 @@ def _convert_times(inst, times, time_format): elif time_format == "timedelta": times = to_timedelta(times, unit="s") elif time_format == "datetime": - times = to_timedelta(times + inst.first_time, unit="s") + inst.info["meas_date"] + times = to_timedelta(times + first_time, unit="s") + meas_date return times From 7ce9aa1789f9ebe928aaa315d993182ad416464e Mon Sep 17 00:00:00 2001 From: Clemens Brunner Date: Tue, 12 Dec 2023 20:31:27 +0100 Subject: [PATCH 118/405] Unignore files (#12291) Co-authored-by: Eric Larson --- .gitignore | 9 +++++++-- pyproject.toml | 6 ------ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index 51707aa39e0..118eebd9c76 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,11 @@ junit-results.xml *.tmproj *.png *.dat +# make sure we ship data files +!mne/data/**/*.dat +!mne/data/**/*.fif +!mne/data/**/*.fif.gz +!mne/icons/**/*.png .DS_Store events.eve foo-lh.label @@ -27,7 +32,6 @@ foo.lout bar.lout foobar.lout epochs_data.mat -memmap*.dat tmp-*.w tmtags auto_examples @@ -62,11 +66,11 @@ tutorials/misc/report.h5 tutorials/io/fnirs.csv pip-log.txt .coverage* +!.coveragerc coverage.xml tags doc/coverages doc/samples -doc/*.dat doc/fil-result doc/optipng.exe sg_execution_times.rst @@ -93,6 +97,7 @@ cover .venv/ venv/ *.json +!codemeta.json .hypothesis/ .ruff_cache/ .ipynb_checkpoints/ diff --git a/pyproject.toml b/pyproject.toml index fb8757150ac..c23caa13d06 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -172,12 +172,6 @@ Forum = "https://mne.discourse.group/" "Source Code" = "https://github.com/mne-tools/mne-python/" [tool.hatch.build] -artifacts = [ - "/mne/data/**/*.dat", - "/mne/data/**/*.fif", - "/mne/data/**/*.fif.gz", - "/mne/icons/**/*.png", -] # excluded via .gitignore, but we do want to ship those files exclude = [ "/.*", "/*.yml", From 1034bffde6fe4a360da3fe155b3c16bdc6380b8d Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Wed, 13 Dec 2023 13:08:17 -0500 Subject: [PATCH 119/405] BUG: Fix passing removed params to command (#12294) --- mne/commands/mne_coreg.py | 38 -------------------------------------- 1 file changed, 38 deletions(-) diff --git a/mne/commands/mne_coreg.py b/mne/commands/mne_coreg.py index b32e8b9e3d7..b0551346e43 100644 --- a/mne/commands/mne_coreg.py +++ b/mne/commands/mne_coreg.py @@ -41,25 +41,6 @@ def run(): default=None, help="FIFF file with digitizer data for coregistration", ) - parser.add_option( - "-t", - "--tabbed", - dest="tabbed", - action="store_true", - default=None, - help="Option for small screens: Combine " - "the data source panel and the coregistration panel " - "into a single panel with tabs.", - ) - parser.add_option( - "--no-guess-mri", - dest="guess_mri_subject", - action="store_false", - default=None, - help="Prevent the GUI from automatically guessing and " - "changing the MRI subject when a new head shape source " - "file is selected.", - ) parser.add_option( "--head-opacity", type=float, @@ -94,20 +75,6 @@ def run(): dest="interaction", help='Interaction style to use, can be "trackball" or ' '"terrain".', ) - parser.add_option( - "--scale", - type=float, - default=None, - dest="scale", - help="Scale factor for the scene.", - ) - parser.add_option( - "--simple-rendering", - action="store_false", - dest="advanced_rendering", - default=None, - help="Use simplified OpenGL rendering", - ) _add_verbose_flag(parser) options, args = parser.parse_args() @@ -134,18 +101,13 @@ def run(): faulthandler.enable() mne.gui.coregistration( - tabbed=options.tabbed, inst=options.inst, subject=options.subject, subjects_dir=subjects_dir, - guess_mri_subject=options.guess_mri_subject, head_opacity=options.head_opacity, head_high_res=head_high_res, trans=trans, - scrollable=None, interaction=options.interaction, - scale=options.scale, - advanced_rendering=options.advanced_rendering, show=True, block=True, verbose=options.verbose, From 35f0ef65d02af33acf55ba01fa5aa62d8697e117 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20H=C3=B6chenberger?= Date: Thu, 14 Dec 2023 22:03:44 +0100 Subject: [PATCH 120/405] Add return type hints to `read_raw_*()`, `read_epochs()`, `read_annotations()` (#12296) Co-authored-by: Eric Larson --- doc/conf.py | 51 +++++++++++++++++---------------- mne/annotations.py | 6 ++-- mne/epochs.py | 2 +- mne/io/artemis123/artemis123.py | 2 +- mne/io/boxy/boxy.py | 2 +- mne/io/bti/bti.py | 2 +- mne/io/cnt/cnt.py | 2 +- mne/io/ctf/ctf.py | 7 +---- mne/io/curry/curry.py | 2 +- mne/io/edf/edf.py | 6 ++-- mne/io/eeglab/eeglab.py | 2 +- mne/io/egi/egi.py | 2 +- mne/io/eximia/eximia.py | 2 +- mne/io/eyelink/eyelink.py | 2 +- mne/io/fieldtrip/fieldtrip.py | 2 +- mne/io/fiff/raw.py | 2 +- mne/io/fil/fil.py | 4 ++- mne/io/hitachi/hitachi.py | 2 +- mne/io/kit/kit.py | 2 +- mne/io/nedf/nedf.py | 2 +- mne/io/neuralynx/neuralynx.py | 2 +- mne/io/nicolet/nicolet.py | 2 +- mne/io/nihon/nihon.py | 2 +- mne/io/nirx/nirx.py | 4 ++- mne/io/nsx/nsx.py | 2 +- mne/io/persyst/persyst.py | 2 +- mne/io/snirf/_snirf.py | 4 ++- pyproject.toml | 4 +-- 28 files changed, 66 insertions(+), 60 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index 758cb7a529a..3b544f2a03e 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -269,6 +269,27 @@ "EOGRegression": "mne.preprocessing.EOGRegression", "Spectrum": "mne.time_frequency.Spectrum", "EpochsSpectrum": "mne.time_frequency.EpochsSpectrum", + "EpochsFIF": "mne.Epochs", + "RawBOXY": "mne.io.Raw", + "RawBrainVision": "mne.io.Raw", + "RawBTi": "mne.io.Raw", + "RawCTF": "mne.io.Raw", + "RawCurry": "mne.io.Raw", + "RawEDF": "mne.io.Raw", + "RawEEGLAB": "mne.io.Raw", + "RawEGI": "mne.io.Raw", + "RawEximia": "mne.io.Raw", + "RawEyelink": "mne.io.Raw", + "RawFIL": "mne.io.Raw", + "RawGDF": "mne.io.Raw", + "RawHitachi": "mne.io.Raw", + "RawKIT": "mne.io.Raw", + "RawNedf": "mne.io.Raw", + "RawNeuralynx": "mne.io.Raw", + "RawNihon": "mne.io.Raw", + "RawNIRX": "mne.io.Raw", + "RawPersyst": "mne.io.Raw", + "RawSNIRF": "mne.io.Raw", # dipy "dipy.align.AffineMap": "dipy.align.imaffine.AffineMap", "dipy.align.DiffeomorphicMap": "dipy.align.imwarp.DiffeomorphicMap", @@ -367,34 +388,12 @@ "n_moments", "n_patterns", "n_new_events", - # Undocumented (on purpose) - "RawKIT", - "RawEximia", - "RawEGI", - "RawEEGLAB", - "RawEDF", - "RawCTF", - "RawBTi", - "RawBrainVision", - "RawCurry", - "RawNIRX", - "RawNeuralynx", - "RawGDF", - "RawSNIRF", - "RawBOXY", - "RawPersyst", - "RawNihon", - "RawNedf", - "RawHitachi", - "RawFIL", - "RawEyelink", # sklearn subclasses "mapping", "to", "any", # unlinkable "CoregistrationUI", - "IntracranialElectrodeLocator", "mne_qt_browser.figure.MNEQtBrowser", # pooch, since its website is unreliable and users will rarely need the links "pooch.Unzip", @@ -779,8 +778,12 @@ def append_attr_meth_examples(app, what, name, obj, options, lines): ("py:class", "None. Remove all items from od."), ] nitpick_ignore_regex = [ - ("py:.*", r"mne\.io\.BaseRaw.*"), - ("py:.*", r"mne\.BaseEpochs.*"), + # Classes whose methods we purposefully do not document + ("py:.*", r"mne\.io\.BaseRaw.*"), # use mne.io.Raw + ("py:.*", r"mne\.BaseEpochs.*"), # use mne.Epochs + # Type hints for undocumented types + ("py:.*", r"mne\.io\..*\.Raw.*"), # RawEDF etc. + ("py:.*", r"mne\.epochs\.EpochsFIF.*"), ( "py:obj", "(filename|metadata|proj|times|tmax|tmin|annotations|ch_names|compensation_grade|filenames|first_samp|first_time|last_samp|n_times|proj|times|tmax|tmin)", diff --git a/mne/annotations.py b/mne/annotations.py index 8a44a84f539..783ee6e1901 100644 --- a/mne/annotations.py +++ b/mne/annotations.py @@ -1150,7 +1150,9 @@ def _write_annotations_txt(fname, annot): @fill_doc -def read_annotations(fname, sfreq="auto", uint16_codec=None, encoding="utf8"): +def read_annotations( + fname, sfreq="auto", uint16_codec=None, encoding="utf8" +) -> Annotations: r"""Read annotations from a file. This function reads a ``.fif``, ``.fif.gz``, ``.vmrk``, ``.amrk``, @@ -1183,7 +1185,7 @@ def read_annotations(fname, sfreq="auto", uint16_codec=None, encoding="utf8"): Returns ------- - annot : instance of Annotations | None + annot : instance of Annotations The annotations. Notes diff --git a/mne/epochs.py b/mne/epochs.py index 0ae911ac5ae..50403345e92 100644 --- a/mne/epochs.py +++ b/mne/epochs.py @@ -3850,7 +3850,7 @@ def _read_one_epoch_file(f, tree, preload): @verbose -def read_epochs(fname, proj=True, preload=True, verbose=None): +def read_epochs(fname, proj=True, preload=True, verbose=None) -> "EpochsFIF": """Read epochs from a fif file. Parameters diff --git a/mne/io/artemis123/artemis123.py b/mne/io/artemis123/artemis123.py index fb7b33e5b6c..64d98c54dc2 100644 --- a/mne/io/artemis123/artemis123.py +++ b/mne/io/artemis123/artemis123.py @@ -23,7 +23,7 @@ @verbose def read_raw_artemis123( input_fname, preload=False, verbose=None, pos_fname=None, add_head_trans=True -): +) -> "RawArtemis123": """Read Artemis123 data as raw object. Parameters diff --git a/mne/io/boxy/boxy.py b/mne/io/boxy/boxy.py index b2afe096f64..a240a1f387e 100644 --- a/mne/io/boxy/boxy.py +++ b/mne/io/boxy/boxy.py @@ -15,7 +15,7 @@ @fill_doc -def read_raw_boxy(fname, preload=False, verbose=None): +def read_raw_boxy(fname, preload=False, verbose=None) -> "RawBOXY": """Reader for an optical imaging recording. This function has been tested using the ISS Imagent I and II systems diff --git a/mne/io/bti/bti.py b/mne/io/bti/bti.py index 99a77cd2b8c..91fb2a112fd 100644 --- a/mne/io/bti/bti.py +++ b/mne/io/bti/bti.py @@ -1435,7 +1435,7 @@ def read_raw_bti( eog_ch=("E63", "E64"), preload=False, verbose=None, -): +) -> "RawBTi": """Raw object from 4D Neuroimaging MagnesWH3600 data. .. note:: diff --git a/mne/io/cnt/cnt.py b/mne/io/cnt/cnt.py index 496ed91cd38..78bc15db580 100644 --- a/mne/io/cnt/cnt.py +++ b/mne/io/cnt/cnt.py @@ -174,7 +174,7 @@ def read_raw_cnt( header="auto", preload=False, verbose=None, -): +) -> "RawCNT": """Read CNT data as raw object. .. Note:: diff --git a/mne/io/ctf/ctf.py b/mne/io/ctf/ctf.py index 1d4970624bd..65983258db5 100644 --- a/mne/io/ctf/ctf.py +++ b/mne/io/ctf/ctf.py @@ -33,7 +33,7 @@ @fill_doc def read_raw_ctf( directory, system_clock="truncate", preload=False, clean_names=False, verbose=None -): +) -> "RawCTF": """Raw object from CTF directory. Parameters @@ -55,11 +55,6 @@ def read_raw_ctf( ------- raw : instance of RawCTF The raw data. - See :class:`mne.io.Raw` for documentation of attributes and methods. - - See Also - -------- - mne.io.Raw : Documentation of attributes and methods of RawCTF. Notes ----- diff --git a/mne/io/curry/curry.py b/mne/io/curry/curry.py index e5b8ce02ed3..27fdc3ce7bc 100644 --- a/mne/io/curry/curry.py +++ b/mne/io/curry/curry.py @@ -542,7 +542,7 @@ def _read_annotations_curry(fname, sfreq="auto"): @verbose -def read_raw_curry(fname, preload=False, verbose=None): +def read_raw_curry(fname, preload=False, verbose=None) -> "RawCurry": """Read raw data from Curry files. Parameters diff --git a/mne/io/edf/edf.py b/mne/io/edf/edf.py index 7c02642ec8f..d9a9c7f2711 100644 --- a/mne/io/edf/edf.py +++ b/mne/io/edf/edf.py @@ -1567,7 +1567,7 @@ def read_raw_edf( encoding="utf8", *, verbose=None, -): +) -> RawEDF: """Reader function for EDF and EDF+ files. Parameters @@ -1701,7 +1701,7 @@ def read_raw_bdf( encoding="utf8", *, verbose=None, -): +) -> RawEDF: """Reader function for BDF files. Parameters @@ -1828,7 +1828,7 @@ def read_raw_gdf( include=None, preload=False, verbose=None, -): +) -> RawGDF: """Reader function for GDF files. Parameters diff --git a/mne/io/eeglab/eeglab.py b/mne/io/eeglab/eeglab.py index f4beee56119..4e9b9da1c5e 100644 --- a/mne/io/eeglab/eeglab.py +++ b/mne/io/eeglab/eeglab.py @@ -293,7 +293,7 @@ def read_raw_eeglab( uint16_codec=None, montage_units="auto", verbose=None, -): +) -> "RawEEGLAB": r"""Read an EEGLAB .set file. Parameters diff --git a/mne/io/egi/egi.py b/mne/io/egi/egi.py index 32cb71db28f..455c47ae726 100644 --- a/mne/io/egi/egi.py +++ b/mne/io/egi/egi.py @@ -104,7 +104,7 @@ def read_raw_egi( preload=False, channel_naming="E%d", verbose=None, -): +) -> "RawEGI": """Read EGI simple binary as raw object. .. note:: This function attempts to create a synthetic trigger channel. diff --git a/mne/io/eximia/eximia.py b/mne/io/eximia/eximia.py index 0af9d9daf5d..8b85768fedc 100644 --- a/mne/io/eximia/eximia.py +++ b/mne/io/eximia/eximia.py @@ -13,7 +13,7 @@ @fill_doc -def read_raw_eximia(fname, preload=False, verbose=None): +def read_raw_eximia(fname, preload=False, verbose=None) -> "RawEximia": """Reader for an eXimia EEG file. Parameters diff --git a/mne/io/eyelink/eyelink.py b/mne/io/eyelink/eyelink.py index 196aef408b1..1eaf82500ae 100644 --- a/mne/io/eyelink/eyelink.py +++ b/mne/io/eyelink/eyelink.py @@ -28,7 +28,7 @@ def read_raw_eyelink( find_overlaps=False, overlap_threshold=0.05, verbose=None, -): +) -> "RawEyelink": """Reader for an Eyelink ``.asc`` file. Parameters diff --git a/mne/io/fieldtrip/fieldtrip.py b/mne/io/fieldtrip/fieldtrip.py index 8d054b076ee..bff6869e147 100644 --- a/mne/io/fieldtrip/fieldtrip.py +++ b/mne/io/fieldtrip/fieldtrip.py @@ -20,7 +20,7 @@ ) -def read_raw_fieldtrip(fname, info, data_name="data"): +def read_raw_fieldtrip(fname, info, data_name="data") -> RawArray: """Load continuous (raw) data from a FieldTrip preprocessing structure. This function expects to find single trial raw data (FT_DATATYPE_RAW) in diff --git a/mne/io/fiff/raw.py b/mne/io/fiff/raw.py index f4053f88b37..1c13189f723 100644 --- a/mne/io/fiff/raw.py +++ b/mne/io/fiff/raw.py @@ -502,7 +502,7 @@ def _check_entry(first, nent): @fill_doc def read_raw_fif( fname, allow_maxshield=False, preload=False, on_split_missing="raise", verbose=None -): +) -> Raw: """Reader function for Raw FIF data. Parameters diff --git a/mne/io/fil/fil.py b/mne/io/fil/fil.py index ea990b741de..99e2b77b2d8 100644 --- a/mne/io/fil/fil.py +++ b/mne/io/fil/fil.py @@ -25,7 +25,9 @@ @verbose -def read_raw_fil(binfile, precision="single", preload=False, *, verbose=None): +def read_raw_fil( + binfile, precision="single", preload=False, *, verbose=None +) -> "RawFIL": """Raw object from FIL-OPMEG formatted data. Parameters diff --git a/mne/io/hitachi/hitachi.py b/mne/io/hitachi/hitachi.py index 0f046bb37e6..a81095712d1 100644 --- a/mne/io/hitachi/hitachi.py +++ b/mne/io/hitachi/hitachi.py @@ -17,7 +17,7 @@ @fill_doc -def read_raw_hitachi(fname, preload=False, verbose=None): +def read_raw_hitachi(fname, preload=False, verbose=None) -> "RawHitachi": """Reader for a Hitachi fNIRS recording. Parameters diff --git a/mne/io/kit/kit.py b/mne/io/kit/kit.py index 88af0b2dc85..e6165a543d4 100644 --- a/mne/io/kit/kit.py +++ b/mne/io/kit/kit.py @@ -913,7 +913,7 @@ def read_raw_kit( allow_unknown_format=False, standardize_names=False, verbose=None, -): +) -> RawKIT: r"""Reader function for Ricoh/KIT conversion to FIF. Parameters diff --git a/mne/io/nedf/nedf.py b/mne/io/nedf/nedf.py index c16f19d91b4..8e37cd36d54 100644 --- a/mne/io/nedf/nedf.py +++ b/mne/io/nedf/nedf.py @@ -202,7 +202,7 @@ def _convert_eeg(chunks, n_eeg, n_tot): @verbose -def read_raw_nedf(filename, preload=False, verbose=None): +def read_raw_nedf(filename, preload=False, verbose=None) -> "RawNedf": """Read NeuroElectrics .nedf files. NEDF file versions starting from 1.3 are supported. diff --git a/mne/io/neuralynx/neuralynx.py b/mne/io/neuralynx/neuralynx.py index 06d5000fcb6..4b6dea1a339 100644 --- a/mne/io/neuralynx/neuralynx.py +++ b/mne/io/neuralynx/neuralynx.py @@ -14,7 +14,7 @@ @fill_doc def read_raw_neuralynx( fname, *, preload=False, exclude_fname_patterns=None, verbose=None -): +) -> "RawNeuralynx": """Reader for Neuralynx files. Parameters diff --git a/mne/io/nicolet/nicolet.py b/mne/io/nicolet/nicolet.py index 37855b97054..9b5fa2b3ae5 100644 --- a/mne/io/nicolet/nicolet.py +++ b/mne/io/nicolet/nicolet.py @@ -19,7 +19,7 @@ @fill_doc def read_raw_nicolet( input_fname, ch_type, eog=(), ecg=(), emg=(), misc=(), preload=False, verbose=None -): +) -> "RawNicolet": """Read Nicolet data as raw object. ..note:: This reader takes data files with the extension ``.data`` as an diff --git a/mne/io/nihon/nihon.py b/mne/io/nihon/nihon.py index 919719f24a2..fb7855e5323 100644 --- a/mne/io/nihon/nihon.py +++ b/mne/io/nihon/nihon.py @@ -24,7 +24,7 @@ def _ensure_path(fname): @fill_doc -def read_raw_nihon(fname, preload=False, verbose=None): +def read_raw_nihon(fname, preload=False, verbose=None) -> "RawNihon": """Reader for an Nihon Kohden EEG file. Parameters diff --git a/mne/io/nirx/nirx.py b/mne/io/nirx/nirx.py index 98d81f9c268..1fb51b50380 100644 --- a/mne/io/nirx/nirx.py +++ b/mne/io/nirx/nirx.py @@ -34,7 +34,9 @@ @fill_doc -def read_raw_nirx(fname, saturated="annotate", preload=False, verbose=None): +def read_raw_nirx( + fname, saturated="annotate", preload=False, verbose=None +) -> "RawNIRX": """Reader for a NIRX fNIRS recording. Parameters diff --git a/mne/io/nsx/nsx.py b/mne/io/nsx/nsx.py index 95448b1b22c..2a39efa2989 100644 --- a/mne/io/nsx/nsx.py +++ b/mne/io/nsx/nsx.py @@ -88,7 +88,7 @@ @fill_doc def read_raw_nsx( input_fname, stim_channel=True, eog=None, misc=None, preload=False, *, verbose=None -): +) -> "RawNSX": """Reader function for NSx (Blackrock Microsystems) files. Parameters diff --git a/mne/io/persyst/persyst.py b/mne/io/persyst/persyst.py index 44334fa4555..0ef6723ba11 100644 --- a/mne/io/persyst/persyst.py +++ b/mne/io/persyst/persyst.py @@ -18,7 +18,7 @@ @fill_doc -def read_raw_persyst(fname, preload=False, verbose=None): +def read_raw_persyst(fname, preload=False, verbose=None) -> "RawPersyst": """Reader for a Persyst (.lay/.dat) recording. Parameters diff --git a/mne/io/snirf/_snirf.py b/mne/io/snirf/_snirf.py index e32b32370b3..0fc9ee246e9 100644 --- a/mne/io/snirf/_snirf.py +++ b/mne/io/snirf/_snirf.py @@ -21,7 +21,9 @@ @fill_doc -def read_raw_snirf(fname, optode_frame="unknown", preload=False, verbose=None): +def read_raw_snirf( + fname, optode_frame="unknown", preload=False, verbose=None +) -> "RawSNIRF": """Reader for a continuous wave SNIRF data. .. note:: This reader supports the .snirf file type only, diff --git a/pyproject.toml b/pyproject.toml index c23caa13d06..39c6876e43d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -295,10 +295,10 @@ ignore_messages = "^.*(Unknown target name|Undefined substitution referenced)[^` [tool.mypy] ignore_errors = true scripts_are_modules = true -strict = true +strict = false [[tool.mypy.overrides]] -module = ['mne.evoked', 'mne.io'] +module = ['mne.annotations', 'mne.epochs', 'mne.evoked', 'mne.io'] ignore_errors = false # Ignore "attr-defined" until we fix stuff like: # - BunchConstNamed: '"BunchConstNamed" has no attribute "FIFFB_EVOKED"' From 40256aef4dd5b417be91ce544fa8031cc2abd9bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20H=C3=B6chenberger?= Date: Fri, 15 Dec 2023 19:22:48 +0100 Subject: [PATCH 121/405] MRG: Add return type hints to all `read_epochs_*()` functions (#12297) --- doc/changes/devel.rst | 5 +++-- doc/conf.py | 25 ++++++++++++------------- mne/io/brainvision/brainvision.py | 2 +- mne/io/bti/bti.py | 2 +- mne/io/eeglab/eeglab.py | 2 +- mne/io/fieldtrip/fieldtrip.py | 4 +++- mne/io/kit/kit.py | 2 +- mne/io/nedf/nedf.py | 2 +- 8 files changed, 23 insertions(+), 21 deletions(-) diff --git a/doc/changes/devel.rst b/doc/changes/devel.rst index feae12dcbb2..d993f4cc26c 100644 --- a/doc/changes/devel.rst +++ b/doc/changes/devel.rst @@ -25,7 +25,8 @@ In this version, we started adding type hints (also known as "type annotations") This meta information will be used by development environments (IDEs) like VS Code and PyCharm automatically to provide better assistance such as tab completion or error detection even before running your code. -So far, we've only added return type hints to :func:`mne.read_evokeds` and :func:`mne.io.read_raw`. Now your editors will know: +So far, we've only added return type hints to :func:`mne.io.read_raw`, :func:`mne.read_epochs`, :func:`mne.read_evokeds` and +all format-specific ``read_raw_*()`` and ``read_epochs_*()`` functions. Now your editors will know: these functions return evoked and raw data, respectively. We are planning add type hints to more functions after careful evaluation in the future. @@ -36,7 +37,7 @@ Enhancements ~~~~~~~~~~~~ - Speed up export to .edf in :func:`mne.export.export_raw` by using ``edfio`` instead of ``EDFlib-Python`` (:gh:`12218` by :newcontrib:`Florian Hofer`) - Inform the user about channel discrepancy between provided info, forward operator, and/or covariance matrices in :func:`mne.beamformer.make_lcmv` (:gh:`12238` by :newcontrib:`Nikolai Kapralov`) -- We added type hints for the return values of :func:`mne.read_evokeds` and :func:`mne.io.read_raw`. Development environments like VS Code or PyCharm will now provide more help when using these functions in your code. (:gh:`12250` by `Richard Höchenberger`_ and `Eric Larson`_) +- We added type hints for the return values of raw, epochs, and evoked reading functions. Development environments like VS Code or PyCharm will now provide more help when using these functions in your code. (:gh:`12250`, :gh:`12297` by `Richard Höchenberger`_ and `Eric Larson`_) - Add ``method="polyphase"`` to :meth:`mne.io.Raw.resample` and related functions to allow resampling using :func:`scipy.signal.upfirdn` (:gh:`12268` by `Eric Larson`_) - The package build backend was switched from ``setuptools`` to ``hatchling``. This will only affect users who build and install MNE-Python from source. (:gh:`12269`, :gh:`12281` by `Richard Höchenberger`_) - :meth:`mne.Annotations.to_data_frame` can now output different formats for the ``onset`` column: seconds, milliseconds, datetime objects, and timedelta objects. (:gh:`12289` by `Daniel McCloy`_) diff --git a/doc/conf.py b/doc/conf.py index 3b544f2a03e..c855a82f0cc 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -6,32 +6,32 @@ # License: BSD-3-Clause # Copyright the MNE-Python contributors. -from datetime import datetime, timezone import faulthandler import gc -from importlib.metadata import metadata import os -from pathlib import Path import subprocess import sys import time import warnings +from datetime import datetime, timezone +from importlib.metadata import metadata +from pathlib import Path -import numpy as np import matplotlib +import numpy as np import sphinx -from sphinx.domains.changeset import versionlabels -from sphinx_gallery.sorting import FileNameSortKey, ExplicitOrder from numpydoc import docscrape +from sphinx.domains.changeset import versionlabels +from sphinx_gallery.sorting import ExplicitOrder, FileNameSortKey import mne import mne.html_templates._templates from mne.tests.test_docstring_parameters import error_ignores from mne.utils import ( - linkcode_resolve, # noqa, analysis:ignore _assert_no_instances, - sizeof_fmt, + linkcode_resolve, # noqa, analysis:ignore run_subprocess, + sizeof_fmt, ) from mne.viz import Brain # noqa @@ -270,6 +270,8 @@ "Spectrum": "mne.time_frequency.Spectrum", "EpochsSpectrum": "mne.time_frequency.EpochsSpectrum", "EpochsFIF": "mne.Epochs", + "EpochsEEGLAB": "mne.Epochs", + "EpochsKIT": "mne.Epochs", "RawBOXY": "mne.io.Raw", "RawBrainVision": "mne.io.Raw", "RawBTi": "mne.io.Raw", @@ -685,11 +687,7 @@ def append_attr_meth_examples(app, what, name, obj, options, lines): .. minigallery:: {1} -""".format( - name.split(".")[-1], name - ).split( - "\n" - ) +""".format(name.split(".")[-1], name).split("\n") # -- Other extension configuration ------------------------------------------- @@ -784,6 +782,7 @@ def append_attr_meth_examples(app, what, name, obj, options, lines): # Type hints for undocumented types ("py:.*", r"mne\.io\..*\.Raw.*"), # RawEDF etc. ("py:.*", r"mne\.epochs\.EpochsFIF.*"), + ("py:.*", r"mne\.io\..*\.Epochs.*"), # EpochsKIT etc. ( "py:obj", "(filename|metadata|proj|times|tmax|tmin|annotations|ch_names|compensation_grade|filenames|first_samp|first_time|last_samp|n_times|proj|times|tmax|tmin)", diff --git a/mne/io/brainvision/brainvision.py b/mne/io/brainvision/brainvision.py index 3a4f63718c3..e0f4e5a5c57 100644 --- a/mne/io/brainvision/brainvision.py +++ b/mne/io/brainvision/brainvision.py @@ -921,7 +921,7 @@ def read_raw_brainvision( scale=1.0, preload=False, verbose=None, -): +) -> RawBrainVision: """Reader for Brain Vision EEG file. Parameters diff --git a/mne/io/bti/bti.py b/mne/io/bti/bti.py index 91fb2a112fd..8b9a6ac973f 100644 --- a/mne/io/bti/bti.py +++ b/mne/io/bti/bti.py @@ -1435,7 +1435,7 @@ def read_raw_bti( eog_ch=("E63", "E64"), preload=False, verbose=None, -) -> "RawBTi": +) -> RawBTi: """Raw object from 4D Neuroimaging MagnesWH3600 data. .. note:: diff --git a/mne/io/eeglab/eeglab.py b/mne/io/eeglab/eeglab.py index 4e9b9da1c5e..cd383c6ddb4 100644 --- a/mne/io/eeglab/eeglab.py +++ b/mne/io/eeglab/eeglab.py @@ -349,7 +349,7 @@ def read_epochs_eeglab( uint16_codec=None, montage_units="auto", verbose=None, -): +) -> "EpochsEEGLAB": r"""Reader function for EEGLAB epochs files. Parameters diff --git a/mne/io/fieldtrip/fieldtrip.py b/mne/io/fieldtrip/fieldtrip.py index bff6869e147..3dac2992be1 100644 --- a/mne/io/fieldtrip/fieldtrip.py +++ b/mne/io/fieldtrip/fieldtrip.py @@ -83,7 +83,9 @@ def read_raw_fieldtrip(fname, info, data_name="data") -> RawArray: return raw -def read_epochs_fieldtrip(fname, info, data_name="data", trialinfo_column=0): +def read_epochs_fieldtrip( + fname, info, data_name="data", trialinfo_column=0 +) -> EpochsArray: """Load epoched data from a FieldTrip preprocessing structure. This function expects to find epoched data in the structure data_name is diff --git a/mne/io/kit/kit.py b/mne/io/kit/kit.py index e6165a543d4..2aaa79017ba 100644 --- a/mne/io/kit/kit.py +++ b/mne/io/kit/kit.py @@ -981,7 +981,7 @@ def read_epochs_kit( allow_unknown_format=False, standardize_names=False, verbose=None, -): +) -> EpochsKIT: """Reader function for Ricoh/KIT epochs files. Parameters diff --git a/mne/io/nedf/nedf.py b/mne/io/nedf/nedf.py index 8e37cd36d54..df6030f31c1 100644 --- a/mne/io/nedf/nedf.py +++ b/mne/io/nedf/nedf.py @@ -202,7 +202,7 @@ def _convert_eeg(chunks, n_eeg, n_tot): @verbose -def read_raw_nedf(filename, preload=False, verbose=None) -> "RawNedf": +def read_raw_nedf(filename, preload=False, verbose=None) -> RawNedf: """Read NeuroElectrics .nedf files. NEDF file versions starting from 1.3 are supported. From b1329c3ae59d0da3646b0c667441e12ee0f7bd8d Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Fri, 15 Dec 2023 13:51:05 -0500 Subject: [PATCH 122/405] MAINT: Use HTML5 embedding for examples (#12298) --- doc/_static/style.css | 6 ++++++ doc/conf.py | 7 +++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/doc/_static/style.css b/doc/_static/style.css index 61eea678830..9b289b6c177 100644 --- a/doc/_static/style.css +++ b/doc/_static/style.css @@ -380,3 +380,9 @@ img.hidden { td.justify { text-align-last: justify; } + +/* Matplotlib HTML5 video embedding */ +div.sphx-glr-animation video { + max-width: 100%; + height: auto; +} diff --git a/doc/conf.py b/doc/conf.py index c855a82f0cc..837282c5b56 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -174,10 +174,7 @@ "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://dipy.org/documentation/1.7.0/", - "https://dipy.org/documentation/1.7.0/objects.inv/", - ), + "dipy": ("https://docs.dipy.org/stable", None), "pybv": ("https://pybv.readthedocs.io/en/latest/", None), "pyqtgraph": ("https://pyqtgraph.readthedocs.io/en/latest/", None), } @@ -481,6 +478,8 @@ def __call__(self, gallery_conf, fname, when): 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: From 7242c291fdd572c58143281a64688968463b928a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Dec 2023 09:12:39 -0500 Subject: [PATCH 123/405] Bump actions/upload-artifact from 3 to 4 (#12302) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c34bb80fd38..6523fb3204d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -28,7 +28,7 @@ jobs: pip install build twine - run: python -m build --sdist --wheel - run: twine check --strict dist/* - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: name: dist path: dist From 60e46f0b6c184e1bfb9c399124fa7b619a96622b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Dec 2023 09:12:47 -0500 Subject: [PATCH 124/405] Bump github/codeql-action from 2 to 3 (#12303) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index a06f3336543..7f348f80778 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -42,7 +42,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -56,7 +56,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v2 + uses: github/codeql-action/autobuild@v3 # ℹ️ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -69,4 +69,4 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 From cf2ca7ea723bc92ec1fdb77abc9eafe165160420 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Dec 2023 09:12:59 -0500 Subject: [PATCH 125/405] Bump actions/download-artifact from 3 to 4 (#12304) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6523fb3204d..c9895e11919 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -43,7 +43,7 @@ jobs: name: pypi url: https://pypi.org/p/mne steps: - - uses: actions/download-artifact@v3 + - uses: actions/download-artifact@v4 with: name: dist path: dist From 0a0cad8802e832669bb954a3bdd8e08bfaecf784 Mon Sep 17 00:00:00 2001 From: Daniel McCloy Date: Mon, 18 Dec 2023 13:37:22 -0600 Subject: [PATCH 126/405] fix icon link colors (#12301) --- doc/_static/style.css | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/doc/_static/style.css b/doc/_static/style.css index 9b289b6c177..ccf032c4a7b 100644 --- a/doc/_static/style.css +++ b/doc/_static/style.css @@ -17,7 +17,7 @@ html[data-theme="light"] { /* topbar logo links */ --mne-color-github: #000; - --mne-color-discourse: #000; + --mne-color-discourse: #d0232b; --mne-color-mastodon: #2F0C7A; /* code block copy button */ --copybtn-opacity: 0.75; @@ -222,16 +222,16 @@ aside.footnote:last-child { } /* ******************************************************* navbar icon links */ -#navbar-icon-links i.fa-square-github::before { +.navbar-icon-links i.fa-square-github::before { color: var(--mne-color-github); } -#navbar-icon-links i.fa-discourse::before { +.navbar-icon-links i.fa-discourse::before { color: var(--mne-color-discourse); } -#navbar-icon-links i.fa-discord::before { +.navbar-icon-links i.fa-discord::before { color: var(--mne-color-discord); } -#navbar-icon-links i.fa-mastodon::before { +.navbar-icon-links i.fa-mastodon::before { color: var(--mne-color-mastodon); } From f1a8120d29a162ec42c85e9d64136e3c2405da2c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 18 Dec 2023 16:00:43 -0500 Subject: [PATCH 127/405] [pre-commit.ci] pre-commit autoupdate (#12307) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cd6d522d4e7..fed7db76310 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.1.7 + rev: v0.1.8 hooks: - id: ruff name: ruff lint mne @@ -13,7 +13,7 @@ repos: # Ruff tutorials and examples - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.7 + rev: v0.1.8 hooks: - id: ruff name: ruff lint tutorials and examples From 4742914ff898d22a3c3012aeefaf2a8301f2c2f8 Mon Sep 17 00:00:00 2001 From: Thomas Samuel Binns Date: Tue, 19 Dec 2023 16:00:09 +0000 Subject: [PATCH 128/405] Switch from `epoch_data` to `data` for TFR array functions (#12308) --- doc/changes/devel.rst | 2 +- mne/time_frequency/multitaper.py | 20 ++++++++++++++++---- mne/time_frequency/tfr.py | 20 ++++++++++++++++---- 3 files changed, 33 insertions(+), 9 deletions(-) diff --git a/doc/changes/devel.rst b/doc/changes/devel.rst index d993f4cc26c..fdf307bbbd1 100644 --- a/doc/changes/devel.rst +++ b/doc/changes/devel.rst @@ -52,4 +52,4 @@ Bugs API changes ~~~~~~~~~~~ -- None yet +- The parameter for providing data to :func:`mne.time_frequency.tfr_array_morlet` and :func:`mne.time_frequency.tfr_array_multitaper` has been switched from ``epoch_data`` to ``data``. Only use the ``data`` parameter to avoid a warning (:gh:`12308` by `Thomas Binns`_) diff --git a/mne/time_frequency/multitaper.py b/mne/time_frequency/multitaper.py index c6af2b20c60..1709d6c16d1 100644 --- a/mne/time_frequency/multitaper.py +++ b/mne/time_frequency/multitaper.py @@ -465,7 +465,7 @@ def psd_array_multitaper( @verbose def tfr_array_multitaper( - epoch_data, + data, sfreq, freqs, n_cycles=7.0, @@ -477,6 +477,7 @@ def tfr_array_multitaper( n_jobs=None, *, verbose=None, + epoch_data=None, ): """Compute Time-Frequency Representation (TFR) using DPSS tapers. @@ -486,7 +487,7 @@ def tfr_array_multitaper( Parameters ---------- - epoch_data : array of shape (n_epochs, n_channels, n_times) + data : array of shape (n_epochs, n_channels, n_times) The epochs. sfreq : float Sampling frequency of the data in Hz. @@ -509,11 +510,15 @@ def tfr_array_multitaper( coherence across trials. %(n_jobs)s %(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 ------- out : array - Time frequency transform of ``epoch_data``. + Time frequency transform of ``data``. - if ``output in ('complex',' 'phase')``, array of shape ``(n_epochs, n_chans, n_tapers, n_freqs, n_times)`` @@ -543,8 +548,15 @@ 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( - epoch_data, + data, freqs, sfreq=sfreq, method="multitaper", diff --git a/mne/time_frequency/tfr.py b/mne/time_frequency/tfr.py index 279e2c79879..ec53cd848f6 100644 --- a/mne/time_frequency/tfr.py +++ b/mne/time_frequency/tfr.py @@ -973,7 +973,7 @@ def tfr_morlet( @verbose def tfr_array_morlet( - epoch_data, + data, sfreq, freqs, n_cycles=7.0, @@ -983,6 +983,7 @@ def tfr_array_morlet( output="complex", n_jobs=None, verbose=None, + epoch_data=None, ): """Compute Time-Frequency Representation (TFR) using Morlet wavelets. @@ -991,7 +992,7 @@ def tfr_array_morlet( Parameters ---------- - epoch_data : array of shape (n_epochs, n_channels, n_times) + data : array of shape (n_epochs, n_channels, n_times) The epochs. sfreq : float | int Sampling frequency of the data. @@ -1015,11 +1016,15 @@ 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 ------- out : array - Time frequency transform of epoch_data. + Time frequency transform of ``data``. - if ``output in ('complex', 'phase', 'power')``, array of shape ``(n_epochs, n_chans, n_freqs, n_times)`` @@ -1049,8 +1054,15 @@ def tfr_array_morlet( ---------- .. footbibliography:: """ + 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=epoch_data, + epoch_data=data, freqs=freqs, sfreq=sfreq, method="morlet", From c52208bedfb36e9157678d091f4b4e03ec96c96d Mon Sep 17 00:00:00 2001 From: Daniel McCloy Date: Tue, 19 Dec 2023 13:36:55 -0600 Subject: [PATCH 129/405] fix 404 link on devel landing page (#12316) --- CONTRIBUTING.md | 2 +- doc/development/index.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bec834c7fdb..e653797b3ad 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,5 +5,5 @@ MNE-Python is maintained by a community of scientists and research labs. The pro Users and contributors to MNE-Python are expected to follow our [code of conduct](https://github.com/mne-tools/.github/blob/main/CODE_OF_CONDUCT.md). -The [contributing guide](https://mne.tools/dev/install/contributing.html) has details on the preferred contribution workflow +The [contributing guide](https://mne.tools/dev/development/contributing.html) has details on the preferred contribution workflow and the recommended system configuration for a smooth contribution/development experience. diff --git a/doc/development/index.rst b/doc/development/index.rst index 1bdc5322f36..98fc28f8e7f 100644 --- a/doc/development/index.rst +++ b/doc/development/index.rst @@ -24,7 +24,7 @@ experience. .. _`opening an issue`: https://github.com/mne-tools/mne-python/issues/new/choose .. _`MNE Forum`: https://mne.discourse.group .. _`code of conduct`: https://github.com/mne-tools/.github/blob/main/CODE_OF_CONDUCT.md -.. _`contributing guide`: https://mne.tools/dev/install/contributing.html +.. _`contributing guide`: https://mne.tools/dev/development/contributing.html .. toctree:: :hidden: From 0f59894a2491797c996272c23c39412a62369f5b Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Tue, 19 Dec 2023 17:31:36 -0500 Subject: [PATCH 130/405] MAINT: Work around bad SciPy nightly wheels (#12317) --- tools/azure_dependencies.sh | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tools/azure_dependencies.sh b/tools/azure_dependencies.sh index 70c82baf1c1..cce220a8188 100755 --- a/tools/azure_dependencies.sh +++ b/tools/azure_dependencies.sh @@ -9,12 +9,13 @@ elif [ "${TEST_MODE}" == "pip-pre" ]; then python -m pip install $STD_ARGS --only-binary ":all:" --extra-index-url "https://www.riverbankcomputing.com/pypi/simple" "PyQt6!=6.6.1" PyQt6-sip PyQt6-Qt6 "PyQt6-Qt6!=6.6.1" echo "Numpy etc." # See github_actions_dependencies.sh for comments - python -m pip install $STD_ARGS --only-binary "numpy" numpy - python -m pip install $STD_ARGS --only-binary ":all:" --extra-index-url "https://pypi.anaconda.org/scientific-python-nightly-wheels/simple" "numpy>=2.0.0.dev0" "scipy>=1.12.0.dev0" scikit-learn matplotlib statsmodels + # Until https://github.com/scipy/scipy/issues/19605 and + # https://github.com/scipy/scipy/issues/19713 are resolved, we can't use the NumPy + # 2.0 wheels :( + python -m pip install $STD_ARGS --only-binary numpy scipy h5py + python -m pip install $STD_ARGS --only-binary ":all:" --extra-index-url "https://pypi.anaconda.org/scientific-python-nightly-wheels/simple" scikit-learn matplotlib statsmodels # echo "dipy" # python -m pip install $STD_ARGS --only-binary ":all:" --extra-index-url "https://pypi.anaconda.org/scipy-wheels-nightly/simple" dipy - # echo "h5py" - # python -m pip install $STD_ARGS --only-binary ":all:" -f "https://7933911d6844c6c53a7d-47bd50c35cd79bd838daf386af554a83.ssl.cf2.rackcdn.com" h5py # echo "OpenMEEG" # pip install $STD_ARGS --only-binary ":all:" --extra-index-url "https://test.pypi.org/simple" openmeeg echo "vtk" From 97512a15a74c6a610132c83e8e420dd4a7caf4f5 Mon Sep 17 00:00:00 2001 From: Kristijan Armeni Date: Wed, 20 Dec 2023 09:11:50 -0500 Subject: [PATCH 131/405] BUG: handle temporal discontinuities in Neuralynx `.ncs` files (#12279) Co-authored-by: Eric Larson --- doc/changes/devel.rst | 1 + environment.yml | 1 + mne/datasets/config.py | 4 +- mne/io/neuralynx/neuralynx.py | 214 ++++++++++++++++++++--- mne/io/neuralynx/tests/test_neuralynx.py | 108 ++++++++++-- mne/utils/config.py | 1 + pyproject.toml | 2 + 7 files changed, 289 insertions(+), 42 deletions(-) diff --git a/doc/changes/devel.rst b/doc/changes/devel.rst index fdf307bbbd1..565a9f9fbf0 100644 --- a/doc/changes/devel.rst +++ b/doc/changes/devel.rst @@ -49,6 +49,7 @@ Bugs - Remove incorrect type hints in :func:`mne.io.read_raw_neuralynx` (:gh:`12236` by `Richard Höchenberger`_) - Fix bug where parent directory existence was not checked properly in :meth:`mne.io.Raw.save` (:gh:`12282` by `Eric Larson`_) - ``defusedxml`` is now an optional (rather than required) dependency and needed when reading EGI-MFF data, NEDF data, and BrainVision montages (:gh:`12264` by `Eric Larson`_) +- Correctly handle temporal gaps in Neuralynx .ncs files via :func:`mne.io.read_raw_neuralynx` (:gh:`12279` by `Kristijan Armeni`_ and `Eric Larson`_) API changes ~~~~~~~~~~~ diff --git a/environment.yml b/environment.yml index 8978dfc64e8..96c89fe472b 100644 --- a/environment.yml +++ b/environment.yml @@ -61,3 +61,4 @@ dependencies: - mamba - lazy_loader - defusedxml + - python-neo diff --git a/mne/datasets/config.py b/mne/datasets/config.py index b548f5273f2..b7780778f24 100644 --- a/mne/datasets/config.py +++ b/mne/datasets/config.py @@ -88,7 +88,7 @@ # respective repos, and make a new release of the dataset on GitHub. Then # update the checksum in the MNE_DATASETS dict below, and change version # here: ↓↓↓↓↓ ↓↓↓ -RELEASES = dict(testing="0.150", misc="0.27") +RELEASES = dict(testing="0.151", misc="0.27") TESTING_VERSIONED = f'mne-testing-data-{RELEASES["testing"]}' MISC_VERSIONED = f'mne-misc-data-{RELEASES["misc"]}' @@ -112,7 +112,7 @@ # Testing and misc are at the top as they're updated most often MNE_DATASETS["testing"] = dict( archive_name=f"{TESTING_VERSIONED}.tar.gz", - hash="md5:0b7452daef4d19132505b5639d695628", + hash="md5:5832b4d44f0423d22305fa61cb75bc25", url=( "https://codeload.github.com/mne-tools/mne-testing-data/" f'tar.gz/{RELEASES["testing"]}' diff --git a/mne/io/neuralynx/neuralynx.py b/mne/io/neuralynx/neuralynx.py index 4b6dea1a339..1c007ba5787 100644 --- a/mne/io/neuralynx/neuralynx.py +++ b/mne/io/neuralynx/neuralynx.py @@ -7,10 +7,51 @@ from ..._fiff.meas_info import create_info from ..._fiff.utils import _mult_cal_one +from ...annotations import Annotations from ...utils import _check_fname, _soft_import, fill_doc, logger, verbose from ..base import BaseRaw +class AnalogSignalGap(object): + """Dummy object to represent gaps in Neuralynx data. + + Creates a AnalogSignalProxy-like object. + Propagate `signal`, `units`, and `sampling_rate` attributes + to the `AnalogSignal` init returned by `load()`. + + Parameters + ---------- + signal : array-like + Array of shape (n_channels, n_samples) containing the data. + units : str + Units of the data. (e.g., 'uV') + sampling_rate : quantity + Sampling rate of the data. (e.g., 4000 * pq.Hz) + + Returns + ------- + sig : instance of AnalogSignal + A AnalogSignal object representing a gap in Neuralynx data. + """ + + def __init__(self, signal, units, sampling_rate): + self.signal = signal + self.units = units + self.sampling_rate = sampling_rate + + def load(self, channel_indexes): + """Return AnalogSignal object.""" + _soft_import("neo", "Reading NeuralynxIO files", strict=True) + from neo import AnalogSignal + + sig = AnalogSignal( + signal=self.signal[:, channel_indexes], + units=self.units, + sampling_rate=self.sampling_rate, + ) + return sig + + @fill_doc def read_raw_neuralynx( fname, *, preload=False, exclude_fname_patterns=None, verbose=None @@ -59,11 +100,11 @@ def __init__( exclude_fname_patterns=None, verbose=None, ): + fname = _check_fname(fname, "read", True, "fname", need_dir=True) + _soft_import("neo", "Reading NeuralynxIO files", strict=True) from neo.io import NeuralynxIO - fname = _check_fname(fname, "read", True, "fname", need_dir=True) - logger.info(f"Checking files in {fname}") # construct a list of filenames to ignore @@ -81,12 +122,18 @@ def __init__( try: nlx_reader = NeuralynxIO(dirname=fname, exclude_filename=exclude_fnames) except ValueError as e: - raise ValueError( - "It seems some .ncs channels might have different number of samples. " - + "This is likely due to different sampling rates. " - + "Try excluding them with `exclude_fname_patterns` input arg." - + f"\nOriginal neo.NeuralynxIO.parse_header() ValueError:\n{e}" - ) + # give a more informative error message and what the user can do about it + if "Incompatible section structures across streams" in str(e): + raise ValueError( + "It seems .ncs channels have different numbers of samples. " + + "This is likely due to different sampling rates. " + + "Try reading in only channels with uniform sampling rate " + + "by excluding other channels with `exclude_fname_patterns` " + + "input argument." + + f"\nOriginal neo.NeuralynxRawIO ValueError:\n{e}" + ) from None + else: + raise info = create_info( ch_types="seeg", @@ -98,32 +145,122 @@ def __init__( # the sample sizes of all segments n_segments = nlx_reader.header["nb_segment"][0] block_id = 0 # assumes there's only one block of recording - n_total_samples = sum( - nlx_reader.get_signal_size(block_id, segment) - for segment in range(n_segments) + + # get segment start/stop times + start_times = np.array( + [nlx_reader.segment_t_start(block_id, i) for i in range(n_segments)] + ) + stop_times = np.array( + [nlx_reader.segment_t_stop(block_id, i) for i in range(n_segments)] ) - # construct an array of shape (n_total_samples,) indicating - # segment membership for each sample - sample2segment = np.concatenate( + # find discontinuous boundaries (of length n-1) + next_start_times = start_times[1::] + previous_stop_times = stop_times[:-1] + seg_diffs = next_start_times - previous_stop_times + + # mark as discontinuous any two segments that have + # start/stop delta larger than sampling period (1/sampling_rate) + logger.info("Checking for temporal discontinuities in Neo data segments.") + delta = 1.5 / info["sfreq"] + gaps = seg_diffs > delta + + seg_gap_dict = {} + + logger.info( + f"N = {gaps.sum()} discontinuous Neo segments detected " + + f"with delta > {delta} sec. " + + "Annotating gaps as BAD_ACQ_SKIP." + if gaps.any() + else "No discontinuities detected." + ) + + gap_starts = stop_times[:-1][gaps] # gap starts at segment offset + gap_stops = start_times[1::][gaps] # gap stops at segment onset + + # (n_gaps,) array of ints giving number of samples per inferred gap + gap_n_samps = np.array( + [ + int(round(stop * info["sfreq"])) - int(round(start * info["sfreq"])) + for start, stop in zip(gap_starts, gap_stops) + ] + ).astype(int) # force an int array (if no gaps, empty array is a float) + + # get sort indices for all segments (valid and gap) in ascending order + all_starts_ids = np.argsort(np.concatenate([start_times, gap_starts])) + + # variable indicating whether each segment is a gap or not + gap_indicator = np.concatenate( [ - np.full(shape=(nlx_reader.get_signal_size(block_id, i),), fill_value=i) - for i in range(n_segments) + np.full(len(start_times), fill_value=0), + np.full(len(gap_starts), fill_value=1), ] ) + gap_indicator = gap_indicator[all_starts_ids].astype(bool) + + # store this in a dict to be passed to _raw_extras + seg_gap_dict = { + "gap_n_samps": gap_n_samps, + "isgap": gap_indicator, # False (data segment) or True (gap segment) + } + + valid_segment_sizes = [ + nlx_reader.get_signal_size(block_id, i) for i in range(n_segments) + ] + + sizes_sorted = np.concatenate([valid_segment_sizes, gap_n_samps])[ + all_starts_ids + ] + + # now construct an (n_samples,) indicator variable + sample2segment = np.concatenate( + [np.full(shape=(n,), fill_value=i) for i, n in enumerate(sizes_sorted)] + ) + + # construct Annotations() + gap_seg_ids = np.unique(sample2segment)[gap_indicator] + gap_start_ids = np.array( + [np.where(sample2segment == seg_id)[0][0] for seg_id in gap_seg_ids] + ) + + # recreate time axis for gap annotations + mne_times = np.arange(0, len(sample2segment)) / info["sfreq"] + + assert len(gap_start_ids) == len(gap_n_samps) + annotations = Annotations( + onset=[mne_times[onset_id] for onset_id in gap_start_ids], + duration=[ + mne_times[onset_id + (n - 1)] - mne_times[onset_id] + for onset_id, n in zip(gap_start_ids, gap_n_samps) + ], + description=["BAD_ACQ_SKIP"] * len(gap_start_ids), + ) super(RawNeuralynx, self).__init__( info=info, - last_samps=[n_total_samples - 1], + last_samps=[sizes_sorted.sum() - 1], filenames=[fname], preload=preload, - raw_extras=[dict(smp2seg=sample2segment, exclude_fnames=exclude_fnames)], + raw_extras=[ + dict( + smp2seg=sample2segment, + exclude_fnames=exclude_fnames, + segment_sizes=sizes_sorted, + seg_gap_dict=seg_gap_dict, + ) + ], ) + self.set_annotations(annotations) + def _read_segment_file(self, data, idx, fi, start, stop, cals, mult): """Read a chunk of raw data.""" + from neo import Segment from neo.io import NeuralynxIO + # quantities is a dependency of neo so we are guaranteed it exists + from quantities import Hz + nlx_reader = NeuralynxIO( dirname=self._filenames[fi], exclude_filename=self._raw_extras[0]["exclude_fnames"], @@ -136,13 +273,7 @@ def _read_segment_file(self, data, idx, fi, start, stop, cals, mult): [len(segment.analogsignals) for segment in neo_block[0].segments] ) == len(neo_block[0].segments) - # collect sizes of each segment - segment_sizes = np.array( - [ - nlx_reader.get_signal_size(0, segment_id) - for segment_id in range(len(neo_block[0].segments)) - ] - ) + segment_sizes = self._raw_extras[fi]["segment_sizes"] # construct a (n_segments, 2) array of the first and last # sample index for each segment relative to the start of the recording @@ -188,15 +319,44 @@ def _read_segment_file(self, data, idx, fi, start, stop, cals, mult): -1, 0 ] # express stop sample relative to segment onset + # array containing Segments + segments_arr = np.array(neo_block[0].segments, dtype=object) + + # if gaps were detected, correctly insert gap Segments in between valid Segments + gap_samples = self._raw_extras[fi]["seg_gap_dict"]["gap_n_samps"] + gap_segments = [Segment(f"gap-{i}") for i in range(len(gap_samples))] + + # create AnalogSignal objects representing gap data filled with 0's + sfreq = nlx_reader.get_signal_sampling_rate() + n_chans = ( + np.arange(idx.start, idx.stop, idx.step).size + if type(idx) is slice + else len(idx) # idx can be a slice or an np.array so check both + ) + + for seg, n in zip(gap_segments, gap_samples): + asig = AnalogSignalGap( + signal=np.zeros((n, n_chans)), units="uV", sampling_rate=sfreq * Hz + ) + seg.analogsignals.append(asig) + + n_total_segments = len(neo_block[0].segments + gap_segments) + segments_arr = np.zeros((n_total_segments,), dtype=object) + + # insert inferred gap segments at the right place in between valid segments + isgap = self._raw_extras[0]["seg_gap_dict"]["isgap"] + segments_arr[~isgap] = neo_block[0].segments + segments_arr[isgap] = gap_segments + # now load data from selected segments/channels via - # neo.Segment.AnalogSignal.load() + # neo.Segment.AnalogSignal.load() or AnalogSignalGap.load() all_data = np.concatenate( [ signal.load(channel_indexes=idx).magnitude[ samples[0] : samples[-1] + 1, : ] for seg, samples in zip( - neo_block[0].segments[first_seg : last_seg + 1], sel_samples_local + segments_arr[first_seg : last_seg + 1], sel_samples_local ) for signal in seg.analogsignals ] diff --git a/mne/io/neuralynx/tests/test_neuralynx.py b/mne/io/neuralynx/tests/test_neuralynx.py index 21cb73927a8..1532845ab7a 100644 --- a/mne/io/neuralynx/tests/test_neuralynx.py +++ b/mne/io/neuralynx/tests/test_neuralynx.py @@ -15,6 +15,8 @@ testing_path = data_path(download=False) / "neuralynx" +pytest.importorskip("neo") + def _nlxheader_to_dict(matdict: Dict) -> Dict: """Convert the read-in "Header" field into a dict. @@ -65,14 +67,42 @@ def _read_nlx_mat_chan(matfile: str) -> np.ndarray: return x -mne_testing_ncs = [ - "LAHC1.ncs", - "LAHC2.ncs", - "LAHC3.ncs", - "LAHCu1.ncs", # the 'u' files are going to be filtered out - "xAIR1.ncs", - "xEKG1.ncs", -] +def _read_nlx_mat_chan_keep_gaps(matfile: str) -> np.ndarray: + """Read a single channel from a Neuralynx .mat file and keep invalid samples.""" + mat = loadmat(matfile) + + hdr_dict = _nlxheader_to_dict(mat) + + # Nlx2MatCSC.m reads the data in N equal-sized (512-item) chunks + # this array (1, n_chunks) stores the number of valid samples + # per chunk (the last chunk is usually shorter) + n_valid_samples = mat["NumberOfValidSamples"].ravel() + + # read in the artificial zeros so that + # we can compare with the mne padded arrays + ncs_records_with_gaps = [9, 15, 20] + for i in ncs_records_with_gaps: + n_valid_samples[i] = 512 + + # concatenate chunks, respecting the number of valid samples + x = np.concatenate( + [mat["Samples"][0:n, i] for i, n in enumerate(n_valid_samples)] + ) # in ADBits + + # this value is the same for all channels and + # converts data from ADBits to Volts + conversionf = literal_eval(hdr_dict["ADBitVolts"]) + x = x * conversionf + + # if header says input was inverted at acquisition + # (possibly for spike detection or so?), flip it back + # NeuralynxIO does this under the hood in NeuralynxIO.parse_header() + # see this discussion: https://github.com/NeuralEnsemble/python-neo/issues/819 + if hdr_dict["InputInverted"] == "True": + x *= -1 + + return x + expected_chan_names = ["LAHC1", "LAHC2", "LAHC3", "xAIR1", "xEKG1"] @@ -80,15 +110,20 @@ def _read_nlx_mat_chan(matfile: str) -> np.ndarray: @requires_testing_data def test_neuralynx(): """Test basic reading.""" - pytest.importorskip("neo") - from neo.io import NeuralynxIO - excluded_ncs_files = ["LAHCu1.ncs", "LAHCu2.ncs", "LAHCu3.ncs"] + excluded_ncs_files = [ + "LAHCu1.ncs", + "LAHC1_3_gaps.ncs", + "LAHC2_3_gaps.ncs", + ] # ==== MNE-Python ==== # + fname_patterns = ["*u*.ncs", "*3_gaps.ncs"] raw = read_raw_neuralynx( - fname=testing_path, preload=True, exclude_fname_patterns=["*u*.ncs"] + fname=testing_path, + preload=True, + exclude_fname_patterns=fname_patterns, ) # test that channel selection worked @@ -136,5 +171,52 @@ def test_neuralynx(): ) # data _test_raw_reader( - read_raw_neuralynx, fname=testing_path, exclude_fname_patterns=["*u*.ncs"] + read_raw_neuralynx, + fname=testing_path, + exclude_fname_patterns=fname_patterns, + ) + + +@requires_testing_data +def test_neuralynx_gaps(): + """Test gap detection.""" + # ignore files with no gaps + ignored_ncs_files = [ + "LAHC1.ncs", + "LAHC2.ncs", + "LAHC3.ncs", + "xAIR1.ncs", + "xEKG1.ncs", + "LAHCu1.ncs", + ] + raw = read_raw_neuralynx( + fname=testing_path, + preload=True, + exclude_fname_patterns=ignored_ncs_files, + ) + mne_y, _ = raw.get_data(return_times=True) # in V + + # there should be 2 channels with 3 gaps (of 130 samples in total) + n_expected_gaps = 3 + n_expected_missing_samples = 130 + assert len(raw.annotations) == n_expected_gaps, "Wrong number of gaps detected" + assert ( + (mne_y[0, :] == 0).sum() == n_expected_missing_samples + ), "Number of true and inferred missing samples differ" + + # read in .mat files containing original gaps + matchans = ["LAHC1_3_gaps.mat", "LAHC2_3_gaps.mat"] + + # (n_chan, n_samples) array, in V + mat_y = np.stack( + [ + _read_nlx_mat_chan_keep_gaps(os.path.join(testing_path, ch)) + for ch in matchans + ] + ) + + # compare originally modified .ncs arrays with MNE-padded arrays + # and test that we back-inserted 0's at the right places + assert_allclose( + mne_y, mat_y, rtol=1e-6, err_msg="MNE and Nlx2MatCSC.m not all close" ) diff --git a/mne/utils/config.py b/mne/utils/config.py index 77b94508114..62b4d053012 100644 --- a/mne/utils/config.py +++ b/mne/utils/config.py @@ -684,6 +684,7 @@ def sys_info( "mne-connectivity", "mne-icalabel", "mne-bids-pipeline", + "neo", "", ) if dependencies == "developer": diff --git a/pyproject.toml b/pyproject.toml index 39c6876e43d..092e4dd102a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -104,6 +104,7 @@ full = [ "pybv", "snirf", "defusedxml", + "neo", ] # Dependencies for running the test infrastructure @@ -135,6 +136,7 @@ test_extra = [ "imageio>=2.6.1", "imageio-ffmpeg>=0.4.1", "snirf", + "neo", ] # Dependencies for building the docuemntation From 64c56936929edfff7822c94929cdaea97fb80135 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Wed, 20 Dec 2023 11:16:35 -0500 Subject: [PATCH 132/405] MAINT: Add bot entry [ci skip] --- pyproject.toml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 092e4dd102a..34555177bed 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -323,3 +323,9 @@ disable_error_code = [ 'assignment', 'operator', ] + +[tool.changelog-bot] +[tool.changelog-bot.towncrier_changelog] +enabled = true +verify_pr_number = true +changelog_skip_label = "no-changelog-entry-needed" From 5d740c11c375125a25abefd135bc33637401ffec Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Wed, 20 Dec 2023 11:23:01 -0500 Subject: [PATCH 133/405] MAINT: More [ci skip] --- pyproject.toml | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 34555177bed..0e76af897b8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -324,6 +324,43 @@ disable_error_code = [ 'operator', ] +[tool.towncrier] +package = "mne" +directory = "doc/changes/devel/" +filename = "doc/changes/devel.rst" +title_format = "{version} ({project_date})" +issue_format = "`#{issue} `__" + +[[tool.towncrier.type]] +directory = "notable" +name = "Notable changes" +showcontent = true + +[[tool.towncrier.type]] +directory = "dependency" +name = "Dependencies" +showcontent = true + +[[tool.towncrier.type]] +directory = "bugfix" +name = "Bugfixes" +showcontent = true + +[[tool.towncrier.type]] +directory = "apichange" +name = "API changes by deprecation" +showcontent = true + +[[tool.towncrier.type]] +directory = "newfeature" +name = "New features" +showcontent = true + +[[tool.towncrier.type]] +directory = "other" +name = "Other changes" +showcontent = true + [tool.changelog-bot] [tool.changelog-bot.towncrier_changelog] enabled = true From 00882bc2d24b07594c080af1a768f970476bdd4c Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Wed, 20 Dec 2023 12:40:40 -0500 Subject: [PATCH 134/405] MAINT: Use towncrier for release notes (#12299) --- .github/workflows/check_changelog.yml | 15 ++++ doc/changes/devel.rst | 55 +------------ doc/changes/devel.rst.template | 34 -------- doc/changes/devel/.gitignore | 1 + doc/changes/devel/12190.bugfix.rst | 1 + doc/changes/devel/12218.newfeature.rst | 1 + doc/changes/devel/12236.bugfix.rst | 1 + doc/changes/devel/12238.newfeature.rst | 1 + doc/changes/devel/12248.bugfix.rst | 1 + doc/changes/devel/12250.newfeature.rst | 1 + doc/changes/devel/12250.notable.rst | 11 +++ doc/changes/devel/12264.dependency.rst | 1 + doc/changes/devel/12268.newfeature.rst | 1 + doc/changes/devel/12269.newfeature.rst | 1 + doc/changes/devel/12279.bugfix.rst | 1 + doc/changes/devel/12282.bugfix.rst | 1 + doc/changes/devel/12289.newfeature.rst | 1 + doc/changes/devel/12299.other.rst | 1 + doc/changes/devel/12308.apichange.rst | 1 + doc/conf.py | 14 ++-- doc/development/contributing.rst | 104 ++++++++++++------------- doc/links.inc | 1 + pyproject.toml | 2 + 23 files changed, 106 insertions(+), 145 deletions(-) create mode 100644 .github/workflows/check_changelog.yml delete mode 100644 doc/changes/devel.rst.template create mode 100644 doc/changes/devel/.gitignore create mode 100644 doc/changes/devel/12190.bugfix.rst create mode 100644 doc/changes/devel/12218.newfeature.rst create mode 100644 doc/changes/devel/12236.bugfix.rst create mode 100644 doc/changes/devel/12238.newfeature.rst create mode 100644 doc/changes/devel/12248.bugfix.rst create mode 100644 doc/changes/devel/12250.newfeature.rst create mode 100644 doc/changes/devel/12250.notable.rst create mode 100644 doc/changes/devel/12264.dependency.rst create mode 100644 doc/changes/devel/12268.newfeature.rst create mode 100644 doc/changes/devel/12269.newfeature.rst create mode 100644 doc/changes/devel/12279.bugfix.rst create mode 100644 doc/changes/devel/12282.bugfix.rst create mode 100644 doc/changes/devel/12289.newfeature.rst create mode 100644 doc/changes/devel/12299.other.rst create mode 100644 doc/changes/devel/12308.apichange.rst diff --git a/.github/workflows/check_changelog.yml b/.github/workflows/check_changelog.yml new file mode 100644 index 00000000000..cf59c165258 --- /dev/null +++ b/.github/workflows/check_changelog.yml @@ -0,0 +1,15 @@ +name: Changelog + +on: # yamllint disable-line rule:truthy + pull_request: + types: [opened, synchronize, labeled, unlabeled] + +jobs: + changelog_checker: + name: Check towncrier entry in doc/changes/devel/ + runs-on: ubuntu-latest + steps: + - uses: larsoner/action-towncrier-changelog@co # revert to scientific-python @ 0.1.1 once bug is fixed + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BOT_USERNAME: changelog-bot diff --git a/doc/changes/devel.rst b/doc/changes/devel.rst index 565a9f9fbf0..0e80d522b51 100644 --- a/doc/changes/devel.rst +++ b/doc/changes/devel.rst @@ -1,56 +1,5 @@ -.. NOTE: we use cross-references to highlight new functions and classes. - Please follow the examples below like :func:`mne.stats.f_mway_rm`, so the - whats_new page will have a link to the function/class documentation. - -.. NOTE: there are 3 separate sections for changes, based on type: - - "Enhancements" for new features - - "Bugs" for bug fixes - - "API changes" for backward-incompatible changes - -.. NOTE: changes from first-time contributors should be added to the TOP of - the relevant section (Enhancements / Bugs / API changes), and should look - like this (where xxxx is the pull request number): - - - description of enhancement/bugfix/API change (:gh:`xxxx` by - :newcontrib:`Firstname Lastname`) - - Also add a corresponding entry for yourself in doc/changes/names.inc +.. See doc/development/contributing.rst for description of how to add entries. .. _current: -Version 1.7.dev0 (development) ------------------------------- - -In this version, we started adding type hints (also known as "type annotations") to select parts of the codebase. -This meta information will be used by development environments (IDEs) like VS Code and PyCharm automatically to provide -better assistance such as tab completion or error detection even before running your code. - -So far, we've only added return type hints to :func:`mne.io.read_raw`, :func:`mne.read_epochs`, :func:`mne.read_evokeds` and -all format-specific ``read_raw_*()`` and ``read_epochs_*()`` functions. Now your editors will know: -these functions return evoked and raw data, respectively. We are planning add type hints to more functions after careful -evaluation in the future. - -You don't need to do anything to benefit from these changes – your editor will pick them up automatically and provide the -enhanced experience if it supports it! - -Enhancements -~~~~~~~~~~~~ -- Speed up export to .edf in :func:`mne.export.export_raw` by using ``edfio`` instead of ``EDFlib-Python`` (:gh:`12218` by :newcontrib:`Florian Hofer`) -- Inform the user about channel discrepancy between provided info, forward operator, and/or covariance matrices in :func:`mne.beamformer.make_lcmv` (:gh:`12238` by :newcontrib:`Nikolai Kapralov`) -- We added type hints for the return values of raw, epochs, and evoked reading functions. Development environments like VS Code or PyCharm will now provide more help when using these functions in your code. (:gh:`12250`, :gh:`12297` by `Richard Höchenberger`_ and `Eric Larson`_) -- Add ``method="polyphase"`` to :meth:`mne.io.Raw.resample` and related functions to allow resampling using :func:`scipy.signal.upfirdn` (:gh:`12268` by `Eric Larson`_) -- The package build backend was switched from ``setuptools`` to ``hatchling``. This will only affect users who build and install MNE-Python from source. (:gh:`12269`, :gh:`12281` by `Richard Höchenberger`_) -- :meth:`mne.Annotations.to_data_frame` can now output different formats for the ``onset`` column: seconds, milliseconds, datetime objects, and timedelta objects. (:gh:`12289` by `Daniel McCloy`_) - -Bugs -~~~~ -- Allow :func:`mne.viz.plot_compare_evokeds` to plot eyetracking channels, and improve error handling (:gh:`12190` by `Scott Huberty`_) -- Fix bug with accessing the last data sample using ``raw[:, -1]`` where an empty array was returned (:gh:`12248` by `Eric Larson`_) -- Remove incorrect type hints in :func:`mne.io.read_raw_neuralynx` (:gh:`12236` by `Richard Höchenberger`_) -- Fix bug where parent directory existence was not checked properly in :meth:`mne.io.Raw.save` (:gh:`12282` by `Eric Larson`_) -- ``defusedxml`` is now an optional (rather than required) dependency and needed when reading EGI-MFF data, NEDF data, and BrainVision montages (:gh:`12264` by `Eric Larson`_) -- Correctly handle temporal gaps in Neuralynx .ncs files via :func:`mne.io.read_raw_neuralynx` (:gh:`12279` by `Kristijan Armeni`_ and `Eric Larson`_) - -API changes -~~~~~~~~~~~ -- The parameter for providing data to :func:`mne.time_frequency.tfr_array_morlet` and :func:`mne.time_frequency.tfr_array_multitaper` has been switched from ``epoch_data`` to ``data``. Only use the ``data`` parameter to avoid a warning (:gh:`12308` by `Thomas Binns`_) +.. towncrier-draft-entries:: Version |release| (development) diff --git a/doc/changes/devel.rst.template b/doc/changes/devel.rst.template deleted file mode 100644 index 09c49cad107..00000000000 --- a/doc/changes/devel.rst.template +++ /dev/null @@ -1,34 +0,0 @@ -.. NOTE: we use cross-references to highlight new functions and classes. - Please follow the examples below like :func:`mne.stats.f_mway_rm`, so the - whats_new page will have a link to the function/class documentation. - -.. NOTE: there are 3 separate sections for changes, based on type: - - "Enhancements" for new features - - "Bugs" for bug fixes - - "API changes" for backward-incompatible changes - -.. NOTE: changes from first-time contributors should be added to the TOP of - the relevant section (Enhancements / Bugs / API changes), and should look - like this (where xxxx is the pull request number): - - - description of enhancement/bugfix/API change (:gh:`xxxx` by - :newcontrib:`Firstname Lastname`) - - Also add a corresponding entry for yourself in doc/changes/names.inc - -.. _current: - -Version X.Y.dev0 (development) ------------------------------- - -Enhancements -~~~~~~~~~~~~ -- None yet - -Bugs -~~~~ -- None yet - -API changes -~~~~~~~~~~~ -- None yet diff --git a/doc/changes/devel/.gitignore b/doc/changes/devel/.gitignore new file mode 100644 index 00000000000..f935021a8f8 --- /dev/null +++ b/doc/changes/devel/.gitignore @@ -0,0 +1 @@ +!.gitignore diff --git a/doc/changes/devel/12190.bugfix.rst b/doc/changes/devel/12190.bugfix.rst new file mode 100644 index 00000000000..d7ef2e07444 --- /dev/null +++ b/doc/changes/devel/12190.bugfix.rst @@ -0,0 +1 @@ +Allow :func:`mne.viz.plot_compare_evokeds` to plot eyetracking channels, and improve error handling, y `Scott Huberty`_. \ No newline at end of file diff --git a/doc/changes/devel/12218.newfeature.rst b/doc/changes/devel/12218.newfeature.rst new file mode 100644 index 00000000000..4ea286f0a22 --- /dev/null +++ b/doc/changes/devel/12218.newfeature.rst @@ -0,0 +1 @@ +Speed up export to .edf in :func:`mne.export.export_raw` by using ``edfio`` instead of ``EDFlib-Python``. diff --git a/doc/changes/devel/12236.bugfix.rst b/doc/changes/devel/12236.bugfix.rst new file mode 100644 index 00000000000..ad807ea3487 --- /dev/null +++ b/doc/changes/devel/12236.bugfix.rst @@ -0,0 +1 @@ +Remove incorrect type hints in :func:`mne.io.read_raw_neuralynx`, by `Richard Höchenberger`_. diff --git a/doc/changes/devel/12238.newfeature.rst b/doc/changes/devel/12238.newfeature.rst new file mode 100644 index 00000000000..631722bc07a --- /dev/null +++ b/doc/changes/devel/12238.newfeature.rst @@ -0,0 +1 @@ +Inform the user about channel discrepancy between provided info, forward operator, and/or covariance matrices in :func:`mne.beamformer.make_lcmv`, by :newcontrib:`Nikolai Kapralov`. \ No newline at end of file diff --git a/doc/changes/devel/12248.bugfix.rst b/doc/changes/devel/12248.bugfix.rst new file mode 100644 index 00000000000..bc4124a2267 --- /dev/null +++ b/doc/changes/devel/12248.bugfix.rst @@ -0,0 +1 @@ +Fix bug with accessing the last data sample using ``raw[:, -1]`` where an empty array was returned, by `Eric Larson`_. diff --git a/doc/changes/devel/12250.newfeature.rst b/doc/changes/devel/12250.newfeature.rst new file mode 100644 index 00000000000..20d67dead77 --- /dev/null +++ b/doc/changes/devel/12250.newfeature.rst @@ -0,0 +1 @@ +We added type hints for the return values of :func:`mne.read_evokeds` and :func:`mne.io.read_raw`. Development environments like VS Code or PyCharm will now provide more help when using these functions in your code. By `Richard Höchenberger`_ and `Eric Larson`_. (:gh:`12297`) diff --git a/doc/changes/devel/12250.notable.rst b/doc/changes/devel/12250.notable.rst new file mode 100644 index 00000000000..7616894e636 --- /dev/null +++ b/doc/changes/devel/12250.notable.rst @@ -0,0 +1,11 @@ +In this version, we started adding type hints (also known as "type annotations") to select parts of the codebase. +This meta information will be used by development environments (IDEs) like VS Code and PyCharm automatically to provide +better assistance such as tab completion or error detection even before running your code. + +So far, we've only added return type hints to :func:`mne.io.read_raw`, :func:`mne.read_epochs`, :func:`mne.read_evokeds` and +all format-specific ``read_raw_*()`` and ``read_epochs_*()`` functions. Now your editors will know: +these functions return evoked and raw data, respectively. We are planning add type hints to more functions after careful +evaluation in the future. + +You don't need to do anything to benefit from these changes – your editor will pick them up automatically and provide the +enhanced experience if it supports it! diff --git a/doc/changes/devel/12264.dependency.rst b/doc/changes/devel/12264.dependency.rst new file mode 100644 index 00000000000..c511b3448a8 --- /dev/null +++ b/doc/changes/devel/12264.dependency.rst @@ -0,0 +1 @@ +``defusedxml`` is now an optional (rather than required) dependency and needed when reading EGI-MFF data, NEDF data, and BrainVision montages, by `Eric Larson`_. \ No newline at end of file diff --git a/doc/changes/devel/12268.newfeature.rst b/doc/changes/devel/12268.newfeature.rst new file mode 100644 index 00000000000..caf46fec03f --- /dev/null +++ b/doc/changes/devel/12268.newfeature.rst @@ -0,0 +1 @@ +Add ``method="polyphase"`` to :meth:`mne.io.Raw.resample` and related functions to allow resampling using :func:`scipy.signal.upfirdn`, by `Eric Larson`_. \ No newline at end of file diff --git a/doc/changes/devel/12269.newfeature.rst b/doc/changes/devel/12269.newfeature.rst new file mode 100644 index 00000000000..321bd02070e --- /dev/null +++ b/doc/changes/devel/12269.newfeature.rst @@ -0,0 +1 @@ +The package build backend was switched from ``setuptools`` to ``hatchling``. This will only affect users who build and install MNE-Python from source. By `Richard Höchenberger`_. (:gh:`12281`) \ No newline at end of file diff --git a/doc/changes/devel/12279.bugfix.rst b/doc/changes/devel/12279.bugfix.rst new file mode 100644 index 00000000000..93aee511fec --- /dev/null +++ b/doc/changes/devel/12279.bugfix.rst @@ -0,0 +1 @@ +Correctly handle temporal gaps in Neuralynx .ncs files via :func:`mne.io.read_raw_neuralynx`, by `Kristijan Armeni`_ and `Eric Larson`_. \ No newline at end of file diff --git a/doc/changes/devel/12282.bugfix.rst b/doc/changes/devel/12282.bugfix.rst new file mode 100644 index 00000000000..e743d0b6071 --- /dev/null +++ b/doc/changes/devel/12282.bugfix.rst @@ -0,0 +1 @@ +Fix bug where parent directory existence was not checked properly in :meth:`mne.io.Raw.save`, by `Eric Larson`_. diff --git a/doc/changes/devel/12289.newfeature.rst b/doc/changes/devel/12289.newfeature.rst new file mode 100644 index 00000000000..8110e4cf737 --- /dev/null +++ b/doc/changes/devel/12289.newfeature.rst @@ -0,0 +1 @@ +:meth:`mne.Annotations.to_data_frame` can now output different formats for the ``onset`` column: seconds, milliseconds, datetime objects, and timedelta objects. By `Daniel McCloy`_. diff --git a/doc/changes/devel/12299.other.rst b/doc/changes/devel/12299.other.rst new file mode 100644 index 00000000000..61c4bf56725 --- /dev/null +++ b/doc/changes/devel/12299.other.rst @@ -0,0 +1 @@ +Adopted towncrier_ for changelog entries, by `Eric Larson`_. diff --git a/doc/changes/devel/12308.apichange.rst b/doc/changes/devel/12308.apichange.rst new file mode 100644 index 00000000000..4d1b8e13923 --- /dev/null +++ b/doc/changes/devel/12308.apichange.rst @@ -0,0 +1 @@ +The parameter for providing data to :func:`mne.time_frequency.tfr_array_morlet` and :func:`mne.time_frequency.tfr_array_multitaper` has been switched from ``epoch_data`` to ``data``. Only use the ``data`` parameter to avoid a warning. Changes by `Thomas Binns`_. \ No newline at end of file diff --git a/doc/conf.py b/doc/conf.py index 837282c5b56..e058234ebe2 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -51,9 +51,8 @@ # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -curdir = os.path.dirname(__file__) -sys.path.append(os.path.abspath(os.path.join(curdir, "..", "mne"))) -sys.path.append(os.path.abspath(os.path.join(curdir, "sphinxext"))) +curpath = Path(__file__).parent.resolve(strict=True) +sys.path.append(str(curpath / "sphinxext")) # -- Project information ----------------------------------------------------- @@ -107,6 +106,7 @@ "sphinx_gallery.gen_gallery", "sphinxcontrib.bibtex", "sphinxcontrib.youtube", + "sphinxcontrib.towncrier.ext", # homegrown "contrib_avatars", "gen_commands", @@ -123,7 +123,7 @@ # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. -exclude_patterns = ["_includes"] +exclude_patterns = ["_includes", "changes/devel"] # The suffix of source filenames. source_suffix = ".rst" @@ -149,6 +149,10 @@ copybutton_prompt_text = r">>> |\.\.\. |\$ " copybutton_prompt_is_regexp = True +# -- sphinxcontrib-towncrier configuration ----------------------------------- + +towncrier_draft_working_directory = str(curpath.parent) + # -- Intersphinx configuration ----------------------------------------------- intersphinx_mapping = { @@ -804,7 +808,7 @@ def append_attr_meth_examples(app, what, name, obj, options, lines): # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. -switcher_version_match = "dev" if release.endswith("dev0") else version +switcher_version_match = "dev" if ".dev" in version else version html_theme_options = { "icon_links": [ dict( diff --git a/doc/development/contributing.rst b/doc/development/contributing.rst index 2957b434751..15ad9dad2db 100644 --- a/doc/development/contributing.rst +++ b/doc/development/contributing.rst @@ -591,42 +591,54 @@ Describe your changes in the changelog -------------------------------------- Include in your changeset a brief description of the change in the -:ref:`changelog ` (:file:`doc/changes/devel.rst`; this can be -skipped for very minor changes like correcting typos in the documentation). - -There are different sections of the changelog for each release, and separate -**subsections for bugfixes, new features, and changes to the public API.** -Please be sure to add your entry to the appropriate subsection. - -The styling and positioning of the entry depends on whether you are a -first-time contributor or have been mentioned in the changelog before. - -First-time contributors -""""""""""""""""""""""" - -Welcome to MNE-Python! We're very happy to have you here. 🤗 And to ensure you -get proper credit for your work, please add a changelog entry with the -following pattern **at the top** of the respective subsection (bugs, -enhancements, etc.): - -.. code-block:: rst - - - Bugs - ---- - - - Short description of the changes (:gh:`0000` by :newcontrib:`Firstname Lastname`) - - - ... - -where ``0000`` must be replaced with the respective GitHub pull request (PR) -number, and ``Firstname Lastname`` must be replaced with your full name. - -It is usually best to wait to add a line to the changelog until your PR is -finalized, to avoid merge conflicts (since the changelog is updated with -almost every PR). - -Lastly, make sure that your name is included in the list of authors in +:ref:`changelog ` using towncrier_ format, which aggregates small, +properly-named ``.rst`` files to create a change log. This can be +skipped for very minor changes like correcting typos in the documentation. + +There are six separate sections for changes, based on change type. +To add a changelog entry to a given section, name it as +:file:`doc/changes/devel/..rst`. The types are: + +notable + For overarching changes, e.g., adding type hints package-wide. These are rare. +dependency + For changes to dependencies, e.g., adding a new dependency or changing + the minimum version of an existing dependency. +bugfix + For bug fixes. Can change code behavior with no deprecation period. +apichange + Code behavior changes that require a deprecation period. +newfeature + For new features. +other + For changes that don't fit into any of the above categories, e.g., + internal refactorings. + +For example, for an enhancement PR with number 12345, the changelog entry should be +added as a new file :file:`doc/changes/devel/12345.enhancement.rst`. The file should +contain: + +1. A brief description of the change, typically in a single line of one or two + sentences. +2. reST links to **public** API endpoints like functions (``:func:``), + classes (``:class``), and methods (``:meth:``). If changes are only internal + to private functions/attributes, mention internal refactoring rather than name + the private attributes changed. +3. Author credit. If you are a new contributor (we're very happy to have you here! 🤗), + you should using the ``:newcontrib:`` reST role, whereas previous contributors should + use a standard reST link to their name. For example, a new contributor could write: + + .. code-block:: rst + + Short description of the changes, by :newcontrib:`Firstname Lastname`. + + And an previous contributor could write: + + .. code-block:: rst + + Short description of the changes, by `Firstname Lastname`_. + +Make sure that your name is included in the list of authors in :file:`doc/changes/names.inc`, otherwise the documentation build will fail. To add an author name, append a line with the following pattern (note how the syntax is different from that used in the changelog): @@ -638,27 +650,13 @@ how the syntax is different from that used in the changelog): Many contributors opt to link to their GitHub profile that way. Have a look at the existing entries in the file to get some inspiration. -Recurring contributors -"""""""""""""""""""""" - -The changelog entry should follow the following patterns: - -.. code-block:: rst - - - Short description of the changes from one contributor (:gh:`0000` by `Contributor Name`_) - - Short description of the changes from several contributors (:gh:`0000` by `Contributor Name`_, `Second Contributor`_, and `Third Contributor`_) - -where ``0000`` must be replaced with the respective GitHub pull request (PR) -number. Mind the Oxford comma in the case of multiple contributors. - Sometimes, changes that shall appear as a single changelog entry are spread out -across multiple PRs. In this case, name all relevant PRs, separated by -commas: +across multiple PRs. In this case, edit the existing towncrier file for the relevant +change, and append additional PR numbers in parentheticals with the ``:gh:`` role like: .. code-block:: rst - - Short description of the changes from one contributor in multiple PRs (:gh:`0000`, :gh:`1111` by `Contributor Name`_) - - Short description of the changes from several contributors in multiple PRs (:gh:`0000`, :gh:`1111` by `Contributor Name`_, `Second Contributor`_, and `Third Contributor`_) + Short description of the changes, by `Firstname Lastname`_. (:gh:`12346`) Test locally before opening pull requests (PRs) ----------------------------------------------- diff --git a/doc/links.inc b/doc/links.inc index 9dd1f34872c..27e61c850bc 100644 --- a/doc/links.inc +++ b/doc/links.inc @@ -96,6 +96,7 @@ .. _PIL: https://pypi.python.org/pypi/PIL .. _tqdm: https://tqdm.github.io/ .. _pooch: https://www.fatiando.org/pooch/latest/ +.. _towncrier: https://towncrier.readthedocs.io/ .. python editors diff --git a/pyproject.toml b/pyproject.toml index 0e76af897b8..db21c0a1012 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -146,6 +146,7 @@ doc = [ "pydata_sphinx_theme==0.13.3", "sphinx-gallery", "sphinxcontrib-bibtex>=2.5", + "sphinxcontrib-towncrier", "memory_profiler", "neo", "seaborn!=0.11.2", @@ -291,6 +292,7 @@ ignore_directives = [ "toctree", "rst-class", "tab-set", + "towncrier-draft-entries", ] ignore_messages = "^.*(Unknown target name|Undefined substitution referenced)[^`]*$" From 7ccd100310892617dc3c4290465c1eefe1d47282 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Thu, 21 Dec 2023 10:23:58 -0500 Subject: [PATCH 135/405] MAINT: Fix CIs (#12320) --- mne/decoding/tests/test_transformer.py | 2 +- tools/github_actions_dependencies.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mne/decoding/tests/test_transformer.py b/mne/decoding/tests/test_transformer.py index f7eeb78ff33..1c2a29bdf8e 100644 --- a/mne/decoding/tests/test_transformer.py +++ b/mne/decoding/tests/test_transformer.py @@ -62,7 +62,7 @@ def test_scaler(info, method): epochs_data_t = epochs_data.transpose([1, 0, 2]) if method in ("mean", "median"): if not check_version("sklearn"): - with pytest.raises(ImportError, match="No module"): + with pytest.raises((ImportError, RuntimeError), match=" module "): Scaler(info, method) return diff --git a/tools/github_actions_dependencies.sh b/tools/github_actions_dependencies.sh index 9489a95f397..b9b425c67fb 100755 --- a/tools/github_actions_dependencies.sh +++ b/tools/github_actions_dependencies.sh @@ -28,7 +28,7 @@ else echo "PyQt6" pip install $STD_ARGS --only-binary ":all:" --default-timeout=60 --extra-index-url https://www.riverbankcomputing.com/pypi/simple "PyQt6!=6.6.1" "PyQt6-Qt6!=6.6.1" echo "NumPy/SciPy/pandas etc." - pip install $STD_ARGS --only-binary ":all:" --default-timeout=60 --extra-index-url "https://pypi.anaconda.org/scientific-python-nightly-wheels/simple" "numpy>=2.0.0.dev0" "scipy>=1.12.0.dev0" scikit-learn matplotlib pillow statsmodels + pip install $STD_ARGS --only-binary ":all:" --default-timeout=60 --extra-index-url "https://pypi.anaconda.org/scientific-python-nightly-wheels/simple" "numpy>=2.0.0.dev0" "scipy>=1.12.0.dev0" "scikit-learn==1.4.dev0" matplotlib pillow statsmodels # No pandas, dipy, h5py, openmeeg, python-picard (needs numexpr) until they update to NumPy 2.0 compat INSTALL_KIND="test_extra" # echo "dipy" From a03a40d50f871b3d51da923ac156d50443162ea8 Mon Sep 17 00:00:00 2001 From: "Peter J. Molfese" Date: Thu, 21 Dec 2023 11:21:55 -0500 Subject: [PATCH 136/405] [MRG][ENH]: Add Ability to export STC files as GIFTI (#12309) Co-authored-by: Eric Larson Co-authored-by: Daniel McCloy --- doc/changes/devel/12309.newfeature.rst | 1 + mne/source_estimate.py | 72 ++++++++++++++++++++++++++ mne/tests/test_source_estimate.py | 28 ++++++++++ 3 files changed, 101 insertions(+) create mode 100644 doc/changes/devel/12309.newfeature.rst diff --git a/doc/changes/devel/12309.newfeature.rst b/doc/changes/devel/12309.newfeature.rst new file mode 100644 index 00000000000..8e732044a8e --- /dev/null +++ b/doc/changes/devel/12309.newfeature.rst @@ -0,0 +1 @@ +Add method :meth:`mne.SourceEstimate.save_as_surface` to allow saving GIFTI files from surface source estimates, by `Peter Molfese`_. diff --git a/mne/source_estimate.py b/mne/source_estimate.py index b2d197d7b2f..19b23da7d60 100644 --- a/mne/source_estimate.py +++ b/mne/source_estimate.py @@ -31,6 +31,7 @@ _ensure_src_subject, _get_morph_src_reordering, _get_src_nn, + get_decimated_surfaces, ) from .surface import _get_ico_surface, _project_onto_surface, mesh_edges, read_surface from .transforms import _get_trans, apply_trans @@ -1584,6 +1585,77 @@ def in_label(self, label): ) return label_stc + def save_as_surface(self, fname, src, *, scale=1, scale_rr=1e3): + """Save a surface source estimate (stc) as a GIFTI file. + + Parameters + ---------- + fname : path-like + Filename basename to save files as. + Will write anatomical GIFTI plus time series GIFTI for both lh/rh, + for example ``"basename"`` will write ``"basename.lh.gii"``, + ``"basename.lh.time.gii"``, ``"basename.rh.gii"``, and + ``"basename.rh.time.gii"``. + src : instance of SourceSpaces + The source space of the forward solution. + scale : float + Scale factor to apply to the data (functional) values. + scale_rr : float + Scale factor for the source vertex positions. The default (1e3) will + scale from meters to millimeters, which is more standard for GIFTI files. + + Notes + ----- + .. versionadded:: 1.7 + """ + nib = _import_nibabel() + _check_option("src.kind", src.kind, ("surface", "mixed")) + ss = get_decimated_surfaces(src) + assert len(ss) == 2 # should be guaranteed by _check_option above + + # Create lists to put DataArrays into + hemis = ("lh", "rh") + for s, hemi in zip(ss, hemis): + darrays = list() + darrays.append( + nib.gifti.gifti.GiftiDataArray( + data=(s["rr"] * scale_rr).astype(np.float32), + intent="NIFTI_INTENT_POINTSET", + datatype="NIFTI_TYPE_FLOAT32", + ) + ) + + # Make the topology DataArray + darrays.append( + nib.gifti.gifti.GiftiDataArray( + data=s["tris"].astype(np.int32), + intent="NIFTI_INTENT_TRIANGLE", + datatype="NIFTI_TYPE_INT32", + ) + ) + + # Make the output GIFTI for anatomicals + topo_gi_hemi = nib.gifti.gifti.GiftiImage(darrays=darrays) + + # actually save the file + nib.save(topo_gi_hemi, f"{fname}-{hemi}.gii") + + # Make the Time Series data arrays + ts = [] + data = getattr(self, f"{hemi}_data") * scale + ts = [ + nib.gifti.gifti.GiftiDataArray( + data=data[:, idx].astype(np.float32), + intent="NIFTI_INTENT_POINTSET", + datatype="NIFTI_TYPE_FLOAT32", + ) + for idx in range(data.shape[1]) + ] + + # save the time series + ts_gi = nib.gifti.gifti.GiftiImage(darrays=ts) + nib.save(ts_gi, f"{fname}-{hemi}.time.gii") + def expand(self, vertices): """Expand SourceEstimate to include more vertices. diff --git a/mne/tests/test_source_estimate.py b/mne/tests/test_source_estimate.py index be31fd1501b..ebe1a369e4d 100644 --- a/mne/tests/test_source_estimate.py +++ b/mne/tests/test_source_estimate.py @@ -248,6 +248,34 @@ def test_volume_stc(tmp_path): assert_array_almost_equal(stc.data, stc_new.data) +@testing.requires_testing_data +def test_save_stc_as_gifti(tmp_path): + """Save the stc as a GIFTI file and export.""" + nib = pytest.importorskip("nibabel") + surfpath_src = bem_path / "sample-oct-6-src.fif" + surfpath_stc = data_path / "MEG" / "sample" / "sample_audvis_trunc-meg" + src = read_source_spaces(surfpath_src) # need source space + stc = read_source_estimate(surfpath_stc) # need stc + assert isinstance(src, SourceSpaces) + assert isinstance(stc, SourceEstimate) + + surf_fname = tmp_path / "stc_write" + + stc.save_as_surface(surf_fname, src) + + # did structural get written? + img_lh = nib.load(f"{surf_fname}-lh.gii") + img_rh = nib.load(f"{surf_fname}-rh.gii") + assert isinstance(img_lh, nib.gifti.gifti.GiftiImage) + assert isinstance(img_rh, nib.gifti.gifti.GiftiImage) + + # did time series get written? + img_timelh = nib.load(f"{surf_fname}-lh.time.gii") + img_timerh = nib.load(f"{surf_fname}-rh.time.gii") + assert isinstance(img_timelh, nib.gifti.gifti.GiftiImage) + assert isinstance(img_timerh, nib.gifti.gifti.GiftiImage) + + @testing.requires_testing_data def test_stc_as_volume(): """Test previous volume source estimate morph.""" From ca7fe266c0a6d4426a62798f37d6a3428d08de6b Mon Sep 17 00:00:00 2001 From: Martin Oberg Date: Thu, 21 Dec 2023 12:53:16 -0800 Subject: [PATCH 137/405] fix section parameter to allow proper hierarchy (#12319) --- doc/changes/devel/12319.bugfix.rst | 1 + doc/changes/names.inc | 2 ++ mne/report/report.py | 2 +- 3 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 doc/changes/devel/12319.bugfix.rst diff --git a/doc/changes/devel/12319.bugfix.rst b/doc/changes/devel/12319.bugfix.rst new file mode 100644 index 00000000000..16eb1a3350a --- /dev/null +++ b/doc/changes/devel/12319.bugfix.rst @@ -0,0 +1 @@ +Fix bug where section parameter in :meth:`mne.Report.add_html` was not being utilized resulting in improper formatting, by :newcontrib:`Martin Oberg`. diff --git a/doc/changes/names.inc b/doc/changes/names.inc index 0d62d247dd3..f1a0c951da4 100644 --- a/doc/changes/names.inc +++ b/doc/changes/names.inc @@ -348,6 +348,8 @@ .. _Martin Luessi: https://github.com/mluessi +.. _Martin Oberg: https://github.com/obergmartin + .. _Martin Schulz: https://github.com/marsipu .. _Mathieu Scheltienne: https://github.com/mscheltienne diff --git a/mne/report/report.py b/mne/report/report.py index 9a547d4f7b6..ab56d03ab7e 100644 --- a/mne/report/report.py +++ b/mne/report/report.py @@ -2383,7 +2383,7 @@ def add_html( ) self._add_or_replace( title=title, - section=None, + section=section, tags=tags, html_partial=html_partial, replace=replace, From 6733cae2a0765da9ec1b67a98937839d4cd9aadf Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Thu, 21 Dec 2023 16:13:55 -0500 Subject: [PATCH 138/405] MAINT: Automate renaming of towncrier stubs (#12318) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- .../rename_towncrier/rename_towncrier.py | 56 +++++++++++++++++++ .github/workflows/autofix.yml | 21 +++++++ .pre-commit-config.yaml | 3 - doc/changes/devel/12318.other.rst | 1 + 4 files changed, 78 insertions(+), 3 deletions(-) create mode 100755 .github/actions/rename_towncrier/rename_towncrier.py create mode 100644 .github/workflows/autofix.yml create mode 100644 doc/changes/devel/12318.other.rst diff --git a/.github/actions/rename_towncrier/rename_towncrier.py b/.github/actions/rename_towncrier/rename_towncrier.py new file mode 100755 index 00000000000..68971d1c83f --- /dev/null +++ b/.github/actions/rename_towncrier/rename_towncrier.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python3 + +# Adapted from action-towncrier-changelog +import json +import os +import re +import subprocess +import sys +from pathlib import Path + +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}') + sys.exit(0) +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'] + 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')) +baserepo = g.get_repo(basereponame) + +# Grab config from upstream's default branch +toml_cfg = loads(Path("pyproject.toml").read_text("utf-8")) + +config = toml_cfg["tool"]["towncrier"] +pr = baserepo.get_pull(pr_num) +modified_files = [f.filename for f in pr.get_files()] + +# Get types from config +types = [ent["directory"] for ent in toml_cfg["tool"]["towncrier"]["type"]] +type_pipe = "|".join(types) + +# Get files that potentially match the types +directory = toml_cfg["tool"]["towncrier"]["directory"] +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) +] +for stub in found_stubs: + fro = stub + to = file_re.sub(rf"{directory}{pr_num}.\1.rst", fro) + print(f"Renaming {fro} to {to}") + if real: + subprocess.check_call(["mv", fro, to]) diff --git a/.github/workflows/autofix.yml b/.github/workflows/autofix.yml new file mode 100644 index 00000000000..2c0b693750e --- /dev/null +++ b/.github/workflows/autofix.yml @@ -0,0 +1,21 @@ +name: autofix.ci + +on: # yamllint disable-line rule:truthy + pull_request: + types: [opened, synchronize, labeled, unlabeled] + +permissions: + contents: read + +jobs: + autofix: + name: Autoupdate changelog entry + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + - run: pip install --upgrade towncrier pygithub + - run: python ./.github/actions/rename_towncrier/rename_towncrier.py + - uses: autofix-ci/action@ea32e3a12414e6d3183163c3424a7d7a8631ad84 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fed7db76310..f23220d9819 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -59,6 +59,3 @@ repos: # 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"] - -ci: - autofix_prs: false diff --git a/doc/changes/devel/12318.other.rst b/doc/changes/devel/12318.other.rst new file mode 100644 index 00000000000..94890e1dfc4 --- /dev/null +++ b/doc/changes/devel/12318.other.rst @@ -0,0 +1 @@ +Automate adding of PR number to towncrier stubs, by `Eric Larson`_. From 6790426221b83ee16375ec19e974808d7b9aad4c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 25 Dec 2023 22:34:11 +0100 Subject: [PATCH 139/405] [pre-commit.ci] pre-commit autoupdate (#12325) --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f23220d9819..66f56539781 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.1.8 + rev: v0.1.9 hooks: - id: ruff name: ruff lint mne @@ -13,7 +13,7 @@ repos: # Ruff tutorials and examples - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.8 + rev: v0.1.9 hooks: - id: ruff name: ruff lint tutorials and examples @@ -53,7 +53,7 @@ repos: # mypy - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.7.1 + rev: v1.8.0 hooks: - id: mypy # Avoid the conflict between mne/__init__.py and mne/__init__.pyi by ignoring the former From c73b8afcf3cb6304bb67c390d667cd1ac526473d Mon Sep 17 00:00:00 2001 From: Alex Rockhill Date: Sat, 30 Dec 2023 18:35:06 -0800 Subject: [PATCH 140/405] [ENH, MRG] Allow epoch construction from annotations (#12311) 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 --- doc/changes/devel/12311.newfeature.rst | 1 + doc/conf.py | 1 + examples/decoding/decoding_csp_eeg.py | 13 ++-- examples/decoding/decoding_csp_timefreq.py | 19 +++--- .../time_frequency/time_frequency_erds.py | 11 ++-- .../visualization/eyetracking_plot_heatmap.py | 6 +- mne/epochs.py | 60 +++++++++++++++++-- mne/tests/test_epochs.py | 20 +++++++ mne/utils/docs.py | 8 ++- tools/setup_xvfb.sh | 2 +- tutorials/clinical/20_seeg.py | 3 +- tutorials/clinical/30_ecog.py | 6 +- tutorials/time-freq/50_ssvep.py | 14 ++--- 13 files changed, 109 insertions(+), 55 deletions(-) create mode 100644 doc/changes/devel/12311.newfeature.rst diff --git a/doc/changes/devel/12311.newfeature.rst b/doc/changes/devel/12311.newfeature.rst new file mode 100644 index 00000000000..c5e074278f9 --- /dev/null +++ b/doc/changes/devel/12311.newfeature.rst @@ -0,0 +1 @@ +:class:`mne.Epochs` can now be constructed using :class:`mne.Annotations` stored in the ``raw`` object, by specifying ``events=None``. By `Alex Rockhill`_. \ No newline at end of file diff --git a/doc/conf.py b/doc/conf.py index e058234ebe2..d114237bd5a 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -1310,6 +1310,7 @@ def reset_warnings(gallery_conf, fname): 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", diff --git a/examples/decoding/decoding_csp_eeg.py b/examples/decoding/decoding_csp_eeg.py index 85a468cb590..cf588ebf18a 100644 --- a/examples/decoding/decoding_csp_eeg.py +++ b/examples/decoding/decoding_csp_eeg.py @@ -27,7 +27,7 @@ from sklearn.model_selection import ShuffleSplit, cross_val_score from sklearn.pipeline import Pipeline -from mne import Epochs, events_from_annotations, pick_types +from mne import Epochs, pick_types from mne.channels import make_standard_montage from mne.datasets import eegbci from mne.decoding import CSP @@ -41,7 +41,6 @@ # avoid classification of evoked responses by using epochs that start 1s after # cue onset. tmin, tmax = -1.0, 4.0 -event_id = dict(hands=2, feet=3) subject = 1 runs = [6, 10, 14] # motor imagery: hands vs feet @@ -50,22 +49,20 @@ eegbci.standardize(raw) # set channel names montage = make_standard_montage("standard_1005") raw.set_montage(montage) +raw.annotations.rename(dict(T1="hands", T2="feet")) # Apply band-pass filter raw.filter(7.0, 30.0, fir_design="firwin", skip_by_annotation="edge") -events, _ = events_from_annotations(raw, event_id=dict(T1=2, T2=3)) - picks = pick_types(raw.info, meg=False, eeg=True, stim=False, eog=False, exclude="bads") # Read epochs (train will be done only between 1 and 2s) # Testing will be done with a running classifier epochs = Epochs( raw, - events, - event_id, - tmin, - tmax, + event_id=["hands", "feet"], + tmin=tmin, + tmax=tmax, proj=True, picks=picks, baseline=None, diff --git a/examples/decoding/decoding_csp_timefreq.py b/examples/decoding/decoding_csp_timefreq.py index f81e4fc0fea..2f36064b615 100644 --- a/examples/decoding/decoding_csp_timefreq.py +++ b/examples/decoding/decoding_csp_timefreq.py @@ -29,7 +29,7 @@ from sklearn.pipeline import make_pipeline from sklearn.preprocessing import LabelEncoder -from mne import Epochs, create_info, events_from_annotations +from mne import Epochs, create_info from mne.datasets import eegbci from mne.decoding import CSP from mne.io import concatenate_raws, read_raw_edf @@ -37,15 +37,14 @@ # %% # Set parameters and read data -event_id = dict(hands=2, feet=3) # motor imagery: hands vs feet subject = 1 runs = [6, 10, 14] raw_fnames = eegbci.load_data(subject, runs) raw = concatenate_raws([read_raw_edf(f) for f in raw_fnames]) +raw.annotations.rename(dict(T1="hands", T2="feet")) # Extract information from the raw file sfreq = raw.info["sfreq"] -events, _ = events_from_annotations(raw, event_id=dict(T1=2, T2=3)) raw.pick(picks="eeg", exclude="bads") raw.load_data() @@ -95,10 +94,9 @@ # Extract epochs from filtered data, padded by window size epochs = Epochs( raw_filter, - events, - event_id, - tmin - w_size, - tmax + w_size, + event_id=["hands", "feet"], + tmin=tmin - w_size, + tmax=tmax + w_size, proj=False, baseline=None, preload=True, @@ -148,10 +146,9 @@ # Extract epochs from filtered data, padded by window size epochs = Epochs( raw_filter, - events, - event_id, - tmin - w_size, - tmax + w_size, + event_id=["hands", "feet"], + tmin=tmin - w_size, + tmax=tmax + w_size, proj=False, baseline=None, preload=True, diff --git a/examples/time_frequency/time_frequency_erds.py b/examples/time_frequency/time_frequency_erds.py index ee2dd62a2ba..593861674ed 100644 --- a/examples/time_frequency/time_frequency_erds.py +++ b/examples/time_frequency/time_frequency_erds.py @@ -54,8 +54,8 @@ raw = concatenate_raws([read_raw_edf(f, preload=True) for f in fnames]) raw.rename_channels(lambda x: x.strip(".")) # remove dots from channel names - -events, _ = mne.events_from_annotations(raw, event_id=dict(T1=2, T2=3)) +# rename descriptions to be more easily interpretable +raw.annotations.rename(dict(T1="hands", T2="feet")) # %% # Now we can create 5-second epochs around events of interest. @@ -64,10 +64,9 @@ epochs = mne.Epochs( raw, - events, - event_ids, - tmin - 0.5, - tmax + 0.5, + event_id=["hands", "feet"], + tmin=tmin - 0.5, + tmax=tmax + 0.5, picks=("C3", "Cz", "C4"), baseline=None, preload=True, diff --git a/examples/visualization/eyetracking_plot_heatmap.py b/examples/visualization/eyetracking_plot_heatmap.py index c12aa689984..e1826efb6f7 100644 --- a/examples/visualization/eyetracking_plot_heatmap.py +++ b/examples/visualization/eyetracking_plot_heatmap.py @@ -44,12 +44,8 @@ mne.preprocessing.eyetracking.interpolate_blinks(raw, interpolate_gaze=True) raw.annotations.rename({"dvns": "natural"}) # more intuitive -event_ids = {"natural": 1} -events, event_dict = mne.events_from_annotations(raw, event_id=event_ids) -epochs = mne.Epochs( - raw, events=events, event_id=event_dict, tmin=0, tmax=20, baseline=None -) +epochs = mne.Epochs(raw, event_id=["natural"], tmin=0, tmax=20, baseline=None) # %% diff --git a/mne/epochs.py b/mne/epochs.py index 50403345e92..34d942536bd 100644 --- a/mne/epochs.py +++ b/mne/epochs.py @@ -62,6 +62,7 @@ EpochAnnotationsMixin, _read_annotations_fif, _write_annotations, + events_from_annotations, ) from .baseline import _check_baseline, _log_rescale, rescale from .bem import _check_origin @@ -487,10 +488,7 @@ def __init__( if events is not None: # RtEpochs can have events=None for key, val in self.event_id.items(): if val not in events[:, 2]: - msg = "No matching events found for %s " "(event id %i)" % ( - key, - val, - ) + msg = f"No matching events found for {key} (event id {val})" _on_missing(on_missing, msg) # ensure metadata matches original events size @@ -3104,6 +3102,40 @@ def _ensure_list(x): return metadata, events, event_id +def _events_from_annotations(raw, events, event_id, annotations, on_missing): + """Generate events and event_ids from annotations.""" + events, event_id_tmp = events_from_annotations(raw) + if events.size == 0: + raise RuntimeError( + "No usable annotations found in the raw object. " + "Either `events` must be provided or the raw " + "object must have annotations to construct epochs" + ) + if any(raw.annotations.duration > 0): + logger.info( + "Ignoring annotation durations and creating fixed-duration epochs " + "around annotation onsets." + ) + if event_id is None: + event_id = event_id_tmp + # if event_id is the names of events, map to events integers + if isinstance(event_id, str): + event_id = [event_id] + if isinstance(event_id, (list, tuple, set)): + if not set(event_id).issubset(set(event_id_tmp)): + msg = ( + "No matching annotations found for event_id(s) " + f"{set(event_id) - set(event_id_tmp)}" + ) + _on_missing(on_missing, msg) + # remove extras if on_missing not error + event_id = set(event_id) & set(event_id_tmp) + event_id = {my_id: event_id_tmp[my_id] for my_id in event_id} + # remove any non-selected annotations + annotations.delete(~np.isin(raw.annotations.description, list(event_id))) + return events, event_id, annotations + + @fill_doc class Epochs(BaseEpochs): """Epochs extracted from a Raw instance. @@ -3111,7 +3143,16 @@ class Epochs(BaseEpochs): Parameters ---------- %(raw_epochs)s + + .. note:: + If ``raw`` contains annotations, ``Epochs`` can be constructed around + ``raw.annotations.onset``, but note that the durations of the annotations + are ignored in this case. %(events_epochs)s + + .. versionchanged:: 1.7 + Allow ``events=None`` to use ``raw.annotations.onset`` as the source of + epoch times. %(event_id)s %(epochs_tmin_tmax)s %(baseline_epochs)s @@ -3212,7 +3253,7 @@ class Epochs(BaseEpochs): def __init__( self, raw, - events, + events=None, event_id=None, tmin=-0.2, tmax=0.5, @@ -3240,6 +3281,7 @@ def __init__( "instance of mne.io.BaseRaw" ) info = deepcopy(raw.info) + annotations = raw.annotations.copy() # proj is on when applied in Raw proj = proj or raw.proj @@ -3249,6 +3291,12 @@ def __init__( # keep track of original sfreq (needed for annotations) raw_sfreq = raw.info["sfreq"] + # get events from annotations if no events given + if events is None: + events, event_id, annotations = _events_from_annotations( + raw, events, event_id, annotations, on_missing + ) + # call BaseEpochs constructor super(Epochs, self).__init__( info, @@ -3273,7 +3321,7 @@ def __init__( event_repeated=event_repeated, verbose=verbose, raw_sfreq=raw_sfreq, - annotations=raw.annotations, + annotations=annotations, ) @verbose diff --git a/mne/tests/test_epochs.py b/mne/tests/test_epochs.py index 1c0ff6c027c..c68fc7ce6bd 100644 --- a/mne/tests/test_epochs.py +++ b/mne/tests/test_epochs.py @@ -992,6 +992,26 @@ def test_filter(tmp_path): assert_allclose(epochs.get_data(), data_filt, atol=1e-17) +def test_epochs_from_annotations(): + """Test epoch instantiation using annotations.""" + raw, events = _get_data()[:2] + with pytest.raises( + RuntimeError, match="No usable annotations found in the raw object" + ): + Epochs(raw) + raw.set_annotations( + mne.annotations_from_events( + events, raw.info["sfreq"], first_samp=raw.first_samp + ) + ) + # test on_missing + with pytest.raises(ValueError, match="No matching annotations"): + Epochs(raw, event_id="foo") + # test on_missing warn + with pytest.warns(match="No matching annotations"): + Epochs(raw, event_id=["1", "foo"], on_missing="warn") + + def test_epochs_hash(): """Test epoch hashing.""" raw, events = _get_data()[:2] diff --git a/mne/utils/docs.py b/mne/utils/docs.py index 6d26d01dc40..1fa26fa16dd 100644 --- a/mne/utils/docs.py +++ b/mne/utils/docs.py @@ -1107,12 +1107,14 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): """ docdict["event_id"] = """ -event_id : int | list of int | dict | None +event_id : int | list of int | dict | str | list of str | None The id of the :term:`events` to consider. If dict, the keys can later be used to access associated :term:`events`. Example: dict(auditory=1, visual=3). If int, a dict will be created with the id as - string. If a list, all :term:`events` with the IDs specified in the list - are used. If None, all :term:`events` will be used and a dict is created + string. If a list of int, all :term:`events` with the IDs specified in the list + are used. If a str or list of str, ``events`` must be ``None`` to use annotations + and then the IDs must be the name(s) of the annotations to use. + If None, all :term:`events` will be used and a dict is created with string integer names corresponding to the event id integers.""" docdict["event_id_ecg"] = """ diff --git a/tools/setup_xvfb.sh b/tools/setup_xvfb.sh index a5c55d0819b..d22f8e2b7ac 100755 --- a/tools/setup_xvfb.sh +++ b/tools/setup_xvfb.sh @@ -11,5 +11,5 @@ done # This also includes the libraries necessary for PyQt5/PyQt6 sudo apt update -sudo apt install -yqq xvfb libxkbcommon-x11-0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-xinerama0 libxcb-xfixes0 libopengl0 libegl1 libosmesa6 mesa-utils libxcb-shape0 libxcb-cursor0 +sudo apt install -yqq xvfb libxkbcommon-x11-0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-xinerama0 libxcb-xfixes0 libopengl0 libegl1 libosmesa6 mesa-utils libxcb-shape0 libxcb-cursor0 libxml2 /sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -screen 0 1400x900x24 -ac +extension GLX +render -noreset diff --git a/tutorials/clinical/20_seeg.py b/tutorials/clinical/20_seeg.py index cce5f4a089a..dac5739110d 100644 --- a/tutorials/clinical/20_seeg.py +++ b/tutorials/clinical/20_seeg.py @@ -58,8 +58,7 @@ raw = mne.io.read_raw(misc_path / "seeg" / "sample_seeg_ieeg.fif") -events, event_id = mne.events_from_annotations(raw) -epochs = mne.Epochs(raw, events, event_id, detrend=1, baseline=None) +epochs = mne.Epochs(raw, detrend=1, baseline=None) epochs = epochs["Response"][0] # just process one epoch of data for speed # %% diff --git a/tutorials/clinical/30_ecog.py b/tutorials/clinical/30_ecog.py index 2ccc2d6cb91..d568d3b1bb4 100644 --- a/tutorials/clinical/30_ecog.py +++ b/tutorials/clinical/30_ecog.py @@ -100,15 +100,11 @@ # at the posterior commissure) raw.set_montage(montage) -# Find the annotated events -events, event_id = mne.events_from_annotations(raw) - # Make a 25 second epoch that spans before and after the seizure onset epoch_length = 25 # seconds epochs = mne.Epochs( raw, - events, - event_id=event_id["onset"], + event_id="onset", tmin=13, tmax=13 + epoch_length, baseline=None, diff --git a/tutorials/time-freq/50_ssvep.py b/tutorials/time-freq/50_ssvep.py index 323e8a4fe54..39113f08132 100644 --- a/tutorials/time-freq/50_ssvep.py +++ b/tutorials/time-freq/50_ssvep.py @@ -84,14 +84,12 @@ raw.filter(l_freq=0.1, h_freq=None, fir_design="firwin", verbose=False) # Construct epochs -event_id = {"12hz": 255, "15hz": 155} -events, _ = mne.events_from_annotations(raw, verbose=False) +raw.annotations.rename({"Stimulus/S255": "12hz", "Stimulus/S155": "15hz"}) tmin, tmax = -1.0, 20.0 # in s baseline = None epochs = mne.Epochs( raw, - events=events, - event_id=[event_id["12hz"], event_id["15hz"]], + event_id=["12hz", "15hz"], tmin=tmin, tmax=tmax, baseline=baseline, @@ -356,8 +354,8 @@ def snr_spectrum(psd, noise_n_neighbor_freqs=1, noise_skip_neighbor_freqs=1): # Get indices for the different trial types # ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -i_trial_12hz = np.where(epochs.events[:, 2] == event_id["12hz"])[0] -i_trial_15hz = np.where(epochs.events[:, 2] == event_id["15hz"])[0] +i_trial_12hz = np.where(epochs.annotations.description == "12hz")[0] +i_trial_15hz = np.where(epochs.annotations.description == "15hz")[0] # %% # Get indices of EEG channels forming the ROI @@ -604,7 +602,7 @@ def snr_spectrum(psd, noise_n_neighbor_freqs=1, noise_skip_neighbor_freqs=1): window_snrs = [[]] * len(window_lengths) for i_win, win in enumerate(window_lengths): # compute spectrogram - this_spectrum = epochs[str(event_id["12hz"])].compute_psd( + this_spectrum = epochs["12hz"].compute_psd( "welch", n_fft=int(sfreq * win), n_overlap=0, @@ -688,7 +686,7 @@ def snr_spectrum(psd, noise_n_neighbor_freqs=1, noise_skip_neighbor_freqs=1): for i_win, win in enumerate(window_starts): # compute spectrogram - this_spectrum = epochs[str(event_id["12hz"])].compute_psd( + this_spectrum = epochs["12hz"].compute_psd( "welch", n_fft=int(sfreq * window_length) - 1, n_overlap=0, From ff03c6ca17f7d12f72ee1f488264acec6a8db06e Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Tue, 2 Jan 2024 14:17:33 -0500 Subject: [PATCH 141/405] BUG: Fix bug with epochs image (#12330) --- .github/workflows/tests.yml | 12 ++++++------ mne/conftest.py | 2 +- mne/viz/epochs.py | 5 ++--- pyproject.toml | 2 +- 4 files changed, 10 insertions(+), 11 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 419595c8354..85f537930a5 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -89,14 +89,14 @@ jobs: python-version: ${{ matrix.python }} if: startswith(matrix.kind, 'pip') # Python (if conda) - - uses: conda-incubator/setup-miniconda@v3 + - uses: mamba-org/setup-micromamba@v1 with: - python-version: ${{ env.PYTHON_VERSION }} environment-file: ${{ env.CONDA_ENV }} - activate-environment: mne - miniforge-version: latest - miniforge-variant: Mambaforge - use-mamba: ${{ matrix.kind != 'conda' }} + environment-name: mne + create-args: >- + python=${{ env.PYTHON_VERSION }} + mamba + fmt!=10.2.0 if: ${{ !startswith(matrix.kind, 'pip') }} - run: ./tools/github_actions_dependencies.sh # Minimal commands on Linux (macOS stalls) diff --git a/mne/conftest.py b/mne/conftest.py index ba2bfd51dfa..b0882346586 100644 --- a/mne/conftest.py +++ b/mne/conftest.py @@ -788,7 +788,7 @@ def src_volume_labels(): """Create a 7mm source space with labels.""" pytest.importorskip("nibabel") volume_labels = mne.get_volume_labels_from_aseg(fname_aseg) - with pytest.warns(RuntimeWarning, match="Found no usable.*Left-vessel.*"): + with pytest.warns(RuntimeWarning, match="Found no usable.*t-vessel.*"): src = mne.setup_volume_source_space( "sample", 7.0, diff --git a/mne/viz/epochs.py b/mne/viz/epochs.py index c830570d457..e3ae7b28e6e 100644 --- a/mne/viz/epochs.py +++ b/mne/viz/epochs.py @@ -654,10 +654,9 @@ def _plot_epochs_image( # draw the colorbar if colorbar: - from matplotlib.pyplot import colorbar as cbar - if "colorbar" in ax: # axes supplied by user - this_colorbar = cbar(im, cax=ax["colorbar"]) + cax = ax["colorbar"] + this_colorbar = cax.figure.colorbar(im, cax=cax) this_colorbar.ax.set_ylabel(unit, rotation=270, labelpad=12) else: # we created them this_colorbar = fig.colorbar(im, ax=ax_im) diff --git a/pyproject.toml b/pyproject.toml index db21c0a1012..0b90b4a4e69 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -109,7 +109,7 @@ full = [ # Dependencies for running the test infrastructure test = [ - "pytest", + "pytest!=8.0.0rc1", "pytest-cov", "pytest-timeout", "pytest-harvest", From 596122d1f39a962e8299c63020885e207f127c87 Mon Sep 17 00:00:00 2001 From: Clemens Brunner Date: Thu, 4 Jan 2024 16:31:19 +0100 Subject: [PATCH 142/405] Fix typo in contributing guide (#12335) --- doc/development/contributing.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/development/contributing.rst b/doc/development/contributing.rst index 15ad9dad2db..6249251911f 100644 --- a/doc/development/contributing.rst +++ b/doc/development/contributing.rst @@ -621,7 +621,7 @@ contain: 1. A brief description of the change, typically in a single line of one or two sentences. 2. reST links to **public** API endpoints like functions (``:func:``), - classes (``:class``), and methods (``:meth:``). If changes are only internal + classes (``:class:``), and methods (``:meth:``). If changes are only internal to private functions/attributes, mention internal refactoring rather than name the private attributes changed. 3. Author credit. If you are a new contributor (we're very happy to have you here! 🤗), From 4750f0dd5dc81c15704230c785541ab09fa5373b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 8 Jan 2024 20:55:56 +0000 Subject: [PATCH 143/405] [pre-commit.ci] pre-commit autoupdate (#12340) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 66f56539781..038d082b2e6 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.1.9 + rev: v0.1.11 hooks: - id: ruff name: ruff lint mne @@ -13,7 +13,7 @@ repos: # Ruff tutorials and examples - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.9 + rev: v0.1.11 hooks: - id: ruff name: ruff lint tutorials and examples From f70378a922a46b5432a761281ae857ec747b984b Mon Sep 17 00:00:00 2001 From: Alex Rockhill Date: Wed, 10 Jan 2024 05:19:03 -0800 Subject: [PATCH 144/405] [ENH] Add support for ieeg interpolation (#12336) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- doc/changes/devel/12336.bugfix.rst | 1 + mne/channels/channels.py | 38 ++++-- mne/channels/interpolation.py | 142 ++++++++++++++++++----- mne/channels/tests/test_interpolation.py | 50 ++++++++ mne/defaults.py | 4 +- 5 files changed, 197 insertions(+), 38 deletions(-) create mode 100644 doc/changes/devel/12336.bugfix.rst diff --git a/doc/changes/devel/12336.bugfix.rst b/doc/changes/devel/12336.bugfix.rst new file mode 100644 index 00000000000..c7ce44b8dab --- /dev/null +++ b/doc/changes/devel/12336.bugfix.rst @@ -0,0 +1 @@ +Allow :meth:`mne.io.Raw.interpolate_bads` and :meth:`mne.Epochs.interpolate_bads` to work on ``ecog`` and ``seeg`` data; for ``seeg`` data a spline is fit to neighboring electrode contacts on the same shaft, by `Alex Rockhill`_ \ No newline at end of file diff --git a/mne/channels/channels.py b/mne/channels/channels.py index 325be7350a6..4b87f8131e6 100644 --- a/mne/channels/channels.py +++ b/mne/channels/channels.py @@ -870,9 +870,12 @@ def interpolate_bads( .. versionadded:: 0.9.0 """ from .interpolation import ( + _interpolate_bads_ecog, _interpolate_bads_eeg, _interpolate_bads_meeg, + _interpolate_bads_nan, _interpolate_bads_nirs, + _interpolate_bads_seeg, ) _check_preload(self, "interpolation") @@ -894,35 +897,48 @@ def interpolate_bads( "eeg": ("spline", "MNE", "nan"), "meg": ("MNE", "nan"), "fnirs": ("nearest", "nan"), + "ecog": ("spline", "nan"), + "seeg": ("spline", "nan"), } for key in method: - _check_option("method[key]", key, ("meg", "eeg", "fnirs")) + _check_option("method[key]", key, tuple(valids)) _check_option(f"method['{key}']", method[key], valids[key]) logger.info("Setting channel interpolation method to %s.", method) idx = _picks_to_idx(self.info, list(method), exclude=(), allow_empty=True) if idx.size == 0 or len(pick_info(self.info, idx)["bads"]) == 0: warn("No bad channels to interpolate. Doing nothing...") return self + for ch_type in method.copy(): + idx = _picks_to_idx(self.info, ch_type, exclude=(), allow_empty=True) + if len(pick_info(self.info, idx)["bads"]) == 0: + method.pop(ch_type) logger.info("Interpolating bad channels.") - origin = _check_origin(origin, self.info) + needs_origin = [key != "seeg" and val != "nan" for key, val in method.items()] + if any(needs_origin): + origin = _check_origin(origin, self.info) + for ch_type, interp in method.items(): + if interp == "nan": + _interpolate_bads_nan(self, ch_type, exclude=exclude) if method.get("eeg", "") == "spline": _interpolate_bads_eeg(self, origin=origin, exclude=exclude) - eeg_mne = False - elif "eeg" not in method: - eeg_mne = False - else: - eeg_mne = True - if "meg" in method or eeg_mne: + meg_mne = method.get("meg", "") == "MNE" + eeg_mne = method.get("eeg", "") == "MNE" + if meg_mne or eeg_mne: _interpolate_bads_meeg( self, mode=mode, - origin=origin, + meg=meg_mne, eeg=eeg_mne, + origin=origin, exclude=exclude, method=method, ) - if "fnirs" in method: - _interpolate_bads_nirs(self, exclude=exclude, method=method["fnirs"]) + if method.get("fnirs", "") == "nearest": + _interpolate_bads_nirs(self, exclude=exclude) + if method.get("ecog", "") == "spline": + _interpolate_bads_ecog(self, origin=origin, exclude=exclude) + if method.get("seeg", "") == "spline": + _interpolate_bads_seeg(self, exclude=exclude) if reset_bads is True: if "nan" in method.values(): diff --git a/mne/channels/interpolation.py b/mne/channels/interpolation.py index 807639b8bcf..f805d640258 100644 --- a/mne/channels/interpolation.py +++ b/mne/channels/interpolation.py @@ -6,13 +6,14 @@ import numpy as np from numpy.polynomial.legendre import legval +from scipy.interpolate import RectBivariateSpline from scipy.linalg import pinv from scipy.spatial.distance import pdist, squareform from .._fiff.meas_info import _simplify_info from .._fiff.pick import pick_channels, pick_info, pick_types from ..surface import _normalize_vectors -from ..utils import _check_option, _validate_type, logger, verbose, warn +from ..utils import _validate_type, logger, verbose, warn def _calc_h(cosang, stiffness=4, n_legendre_terms=50): @@ -132,13 +133,13 @@ def _do_interp_dots(inst, interpolation, goods_idx, bads_idx): @verbose -def _interpolate_bads_eeg(inst, origin, exclude=None, verbose=None): +def _interpolate_bads_eeg(inst, origin, exclude=None, ecog=False, verbose=None): if exclude is None: exclude = list() bads_idx = np.zeros(len(inst.ch_names), dtype=bool) goods_idx = np.zeros(len(inst.ch_names), dtype=bool) - picks = pick_types(inst.info, meg=False, eeg=True, exclude=exclude) + picks = pick_types(inst.info, meg=False, eeg=not ecog, ecog=ecog, exclude=exclude) inst.info._check_consistency() bads_idx[picks] = [inst.ch_names[ch] in inst.info["bads"] for ch in picks] @@ -172,6 +173,11 @@ def _interpolate_bads_eeg(inst, origin, exclude=None, verbose=None): _do_interp_dots(inst, interpolation, goods_idx, bads_idx) +@verbose +def _interpolate_bads_ecog(inst, origin, exclude=None, verbose=None): + _interpolate_bads_eeg(inst, origin, exclude=exclude, ecog=True, verbose=verbose) + + def _interpolate_bads_meg( inst, mode="accurate", origin=(0.0, 0.0, 0.04), verbose=None, ref_meg=False ): @@ -180,6 +186,26 @@ def _interpolate_bads_meg( ) +@verbose +def _interpolate_bads_nan( + inst, + ch_type, + ref_meg=False, + exclude=(), + *, + verbose=None, +): + info = _simplify_info(inst.info) + picks_type = pick_types(info, ref_meg=ref_meg, exclude=exclude, **{ch_type: True}) + use_ch_names = [inst.info["ch_names"][p] for p in picks_type] + bads_type = [ch for ch in inst.info["bads"] if ch in use_ch_names] + if len(bads_type) == 0 or len(picks_type) == 0: + return + # select the bad channels to be interpolated + picks_bad = pick_channels(inst.info["ch_names"], bads_type, exclude=[]) + inst._data[..., picks_bad, :] = np.nan + + @verbose def _interpolate_bads_meeg( inst, @@ -213,10 +239,6 @@ def _interpolate_bads_meeg( # select the bad channels to be interpolated picks_bad = pick_channels(inst.info["ch_names"], bads_type, exclude=[]) - if method[ch_type] == "nan": - inst._data[picks_bad] = np.nan - continue - # do MNE based interpolation if ch_type == "eeg": picks_to = picks_type @@ -232,7 +254,7 @@ def _interpolate_bads_meeg( @verbose -def _interpolate_bads_nirs(inst, method="nearest", exclude=(), verbose=None): +def _interpolate_bads_nirs(inst, exclude=(), verbose=None): from mne.preprocessing.nirs import _validate_nirs_info if len(pick_types(inst.info, fnirs=True, exclude=())) == 0: @@ -251,25 +273,93 @@ def _interpolate_bads_nirs(inst, method="nearest", exclude=(), verbose=None): chs = [inst.info["chs"][i] for i in picks_nirs] locs3d = np.array([ch["loc"][:3] for ch in chs]) - _check_option("fnirs_method", method, ["nearest", "nan"]) - - if method == "nearest": - dist = pdist(locs3d) - dist = squareform(dist) - - for bad in picks_bad: - dists_to_bad = dist[bad] - # Ignore distances to self - dists_to_bad[dists_to_bad == 0] = np.inf - # Ignore distances to other bad channels - dists_to_bad[bads_mask] = np.inf - # Find closest remaining channels for same frequency - closest_idx = np.argmin(dists_to_bad) + (bad % 2) - inst._data[bad] = inst._data[closest_idx] - else: - assert method == "nan" - inst._data[picks_bad] = np.nan + dist = pdist(locs3d) + dist = squareform(dist) + + for bad in picks_bad: + dists_to_bad = dist[bad] + # Ignore distances to self + dists_to_bad[dists_to_bad == 0] = np.inf + # Ignore distances to other bad channels + dists_to_bad[bads_mask] = np.inf + # Find closest remaining channels for same frequency + closest_idx = np.argmin(dists_to_bad) + (bad % 2) + inst._data[bad] = inst._data[closest_idx] + # TODO: this seems like a bug because it does not respect reset_bads inst.info["bads"] = [ch for ch in inst.info["bads"] if ch in exclude] return inst + + +def _find_seeg_electrode_shaft(pos, tol=2e-3): + # 1) find nearest neighbor to define the electrode shaft line + # 2) find all contacts on the same line + + dist = squareform(pdist(pos)) + np.fill_diagonal(dist, np.inf) + + shafts = list() + for i, n1 in enumerate(pos): + if any([i in shaft for shaft in shafts]): + continue + n2 = pos[np.argmin(dist[i])] # 1 + # https://mathworld.wolfram.com/Point-LineDistance3-Dimensional.html + 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 + + +@verbose +def _interpolate_bads_seeg(inst, exclude=None, tol=2e-3, verbose=None): + if exclude is None: + exclude = list() + picks = pick_types(inst.info, meg=False, seeg=True, exclude=exclude) + inst.info._check_consistency() + bads_idx = np.isin(np.array(inst.ch_names)[picks], inst.info["bads"]) + + if len(picks) == 0 or bads_idx.sum() == 0: + return + + pos = inst._get_channel_positions(picks) + + # Make sure only sEEG are used + bads_idx_pos = bads_idx[picks] + + shafts = _find_seeg_electrode_shaft(pos, tol=tol) + + # interpolate the bad contacts + picks_bad = list(np.where(bads_idx_pos)[0]) + for shaft in shafts: + 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: + 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." + ) + logger.debug( + f"Interpolating {np.array(inst.ch_names)[bads_shaft]} using " + f"data from {np.array(inst.ch_names)[goods_shaft]}" + ) + 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] + ] + ) + 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 diff --git a/mne/channels/tests/test_interpolation.py b/mne/channels/tests/test_interpolation.py index 999e0c16402..7e282562955 100644 --- a/mne/channels/tests/test_interpolation.py +++ b/mne/channels/tests/test_interpolation.py @@ -10,6 +10,7 @@ from mne import Epochs, pick_channels, pick_types, read_events from mne._fiff.constants import FIFF from mne._fiff.proj import _has_eeg_average_ref_proj +from mne.channels import make_dig_montage from mne.channels.interpolation import _make_interpolation_matrix from mne.datasets import testing from mne.io import RawArray, read_raw_ctf, read_raw_fif, read_raw_nirx @@ -329,6 +330,55 @@ def test_interpolation_nirs(): assert raw_haemo.info["bads"] == [] +@testing.requires_testing_data +def test_interpolation_ecog(): + """Test interpolation for ECoG.""" + raw, epochs_eeg = _load_data("eeg") + bads = ["EEG 012"] + bads_mask = np.isin(epochs_eeg.ch_names, bads) + + epochs_ecog = epochs_eeg.set_channel_types( + {ch: "ecog" for ch in epochs_eeg.ch_names} + ) + epochs_ecog.info["bads"] = bads + + # check that interpolation changes the data in raw + raw_ecog = RawArray(data=epochs_ecog._data[0], info=epochs_ecog.info) + raw_before = raw_ecog.copy() + raw_after = raw_ecog.interpolate_bads(method=dict(ecog="spline")) + 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]) + + +@testing.requires_testing_data +def test_interpolation_seeg(): + """Test interpolation for sEEG.""" + raw, epochs_eeg = _load_data("eeg") + bads = ["EEG 012"] + bads_mask = np.isin(epochs_eeg.ch_names, bads) + epochs_seeg = epochs_eeg.set_channel_types( + {ch: "seeg" for ch in epochs_eeg.ch_names} + ) + epochs_seeg.info["bads"] = bads + + # 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") + n0 = ch_pos[epochs_seeg.ch_names[0]] + n1 = ch_pos[epochs_seeg.ch_names[1]] + for i, ch in enumerate(epochs_seeg.ch_names[2:]): + ch_pos[ch] = n0 + (n1 - n0) * (i + 2) + raw_seeg.set_montage(make_dig_montage(ch_pos, **pos)) + raw_after = raw_seeg.interpolate_bads(method=dict(seeg="spline")) + 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]) + + def test_nan_interpolation(raw): """Test 'nan' method for interpolating bads.""" ch_to_interp = [raw.ch_names[1]] # don't use channel 0 (type is IAS not MEG) diff --git a/mne/defaults.py b/mne/defaults.py index 8732280998f..b9e6702edec 100644 --- a/mne/defaults.py +++ b/mne/defaults.py @@ -278,7 +278,9 @@ combine_xyz="fro", allow_fixed_depth=True, ), - interpolation_method=dict(eeg="spline", meg="MNE", fnirs="nearest"), + interpolation_method=dict( + eeg="spline", meg="MNE", fnirs="nearest", ecog="spline", seeg="spline" + ), volume_options=dict( alpha=None, resolution=1.0, From d2c806c198ccedb5b66c8eb0da31519ad0e970e9 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Wed, 10 Jan 2024 11:22:03 -0500 Subject: [PATCH 145/405] MAINT: Fix for pandas pre (#12347) --- mne/epochs.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mne/epochs.py b/mne/epochs.py index 34d942536bd..c042c207905 100644 --- a/mne/epochs.py +++ b/mne/epochs.py @@ -2977,11 +2977,11 @@ def _ensure_list(x): *last_cols, ] - data = np.empty((len(events_df), len(columns))) + data = np.empty((len(events_df), len(columns)), float) metadata = pd.DataFrame(data=data, columns=columns, index=events_df.index) # Event names - metadata.iloc[:, 0] = "" + metadata["event_name"] = "" # Event times start_idx = 1 @@ -2990,7 +2990,7 @@ def _ensure_list(x): # keep_first and keep_last names start_idx = stop_idx - metadata.iloc[:, start_idx:] = None + metadata[columns[start_idx:]] = "" # We're all set, let's iterate over all events and fill in in the # respective cells in the metadata. We will subset this to include only From 4f1557d7c122d3bad83a5f43c648c0cd3cfc802a Mon Sep 17 00:00:00 2001 From: Mathieu Scheltienne Date: Wed, 10 Jan 2024 18:30:10 +0100 Subject: [PATCH 146/405] [MRG] Fix clicking on an axis of mne.viz.plot_evoked_topo when multiple vertical lines are added (#12345) --- doc/changes/devel/12345.bugfix.rst | 1 + mne/viz/evoked.py | 2 +- mne/viz/topo.py | 14 +++++++++----- 3 files changed, 11 insertions(+), 6 deletions(-) create mode 100644 doc/changes/devel/12345.bugfix.rst diff --git a/doc/changes/devel/12345.bugfix.rst b/doc/changes/devel/12345.bugfix.rst new file mode 100644 index 00000000000..fa592c6926c --- /dev/null +++ b/doc/changes/devel/12345.bugfix.rst @@ -0,0 +1 @@ +Fix clicking on an axis of :func:`mne.viz.plot_evoked_topo` when multiple vertical lines ``vlines`` are used, by `Mathieu Scheltienne`_. diff --git a/mne/viz/evoked.py b/mne/viz/evoked.py index 3db8d745368..b01092951e6 100644 --- a/mne/viz/evoked.py +++ b/mne/viz/evoked.py @@ -1218,7 +1218,7 @@ def plot_evoked_topo( If true SSP projections are applied before display. If 'interactive', a check box for reversible selection of SSP projection vectors will be shown. - vline : list of float | None + vline : list of float | float| None The values at which to show a vertical line. fig_background : None | ndarray A background image for the figure. This must work with a call to diff --git a/mne/viz/topo.py b/mne/viz/topo.py index 9c3f7c5bd75..1751c7efa57 100644 --- a/mne/viz/topo.py +++ b/mne/viz/topo.py @@ -16,7 +16,7 @@ from .._fiff.pick import channel_type, pick_types from ..defaults import _handle_default -from ..utils import Bunch, _check_option, _clean_names, _to_rgb, fill_doc +from ..utils import Bunch, _check_option, _clean_names, _is_numeric, _to_rgb, fill_doc from .utils import ( DraggableColorbar, _check_cov, @@ -631,10 +631,14 @@ def _rm_cursor(event): else: ax.set_ylabel(y_label) - if vline: - plt.axvline(vline, color=hvline_color, linewidth=1.0, linestyle="--") - if hline: - plt.axhline(hline, color=hvline_color, linewidth=1.0, zorder=10) + if vline is not None: + vline = [vline] if _is_numeric(vline) else vline + for vline_ in vline: + plt.axvline(vline_, color=hvline_color, linewidth=1.0, linestyle="--") + if hline is not None: + hline = [hline] if _is_numeric(hline) else hline + for hline_ in hline: + plt.axhline(hline_, color=hvline_color, linewidth=1.0, zorder=10) if colorbar: plt.colorbar() From 16c17b4dba8381615d57267c01f1f2f310fc522c Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Wed, 10 Jan 2024 12:47:36 -0500 Subject: [PATCH 147/405] ENH: Speed up reading of small buffers (#12343) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- doc/changes/devel/12343.newfeature.rst | 1 + mne/_fiff/open.py | 29 +++--- mne/_fiff/tag.py | 97 +++++++----------- mne/_fiff/tree.py | 47 +-------- mne/commands/tests/test_commands.py | 13 ++- mne/epochs.py | 5 +- mne/io/fiff/raw.py | 136 ++++++++++++------------- mne/io/fiff/tests/test_raw_fiff.py | 12 +-- 8 files changed, 140 insertions(+), 200 deletions(-) create mode 100644 doc/changes/devel/12343.newfeature.rst diff --git a/doc/changes/devel/12343.newfeature.rst b/doc/changes/devel/12343.newfeature.rst new file mode 100644 index 00000000000..9825f924e48 --- /dev/null +++ b/doc/changes/devel/12343.newfeature.rst @@ -0,0 +1 @@ +Speed up raw FIF reading when using small buffer sizes by `Eric Larson`_. \ No newline at end of file diff --git a/mne/_fiff/open.py b/mne/_fiff/open.py index d1794317772..02fcda445a0 100644 --- a/mne/_fiff/open.py +++ b/mne/_fiff/open.py @@ -13,7 +13,13 @@ from ..utils import _file_like, logger, verbose, warn from .constants import FIFF -from .tag import Tag, _call_dict_names, _matrix_info, read_tag, read_tag_info +from .tag import ( + Tag, + _call_dict_names, + _matrix_info, + _read_tag_header, + read_tag, +) from .tree import dir_tree_find, make_dir_tree @@ -139,7 +145,7 @@ def _fiff_open(fname, fid, preload): with fid as fid_old: fid = BytesIO(fid_old.read()) - tag = read_tag_info(fid) + tag = _read_tag_header(fid, 0) # Check that this looks like a fif file prefix = f"file {repr(fname)} does not" @@ -152,7 +158,7 @@ def _fiff_open(fname, fid, preload): if tag.size != 20: raise ValueError(f"{prefix} start with a file id tag") - tag = read_tag(fid) + tag = read_tag(fid, tag.next_pos) if tag.kind != FIFF.FIFF_DIR_POINTER: raise ValueError(f"{prefix} have a directory pointer") @@ -176,16 +182,15 @@ def _fiff_open(fname, fid, preload): directory = dir_tag.data read_slow = False if read_slow: - fid.seek(0, 0) + pos = 0 + fid.seek(pos, 0) directory = list() - while tag.next >= 0: - pos = fid.tell() - tag = read_tag_info(fid) + while pos is not None: + tag = _read_tag_header(fid, pos) if tag is None: break # HACK : to fix file ending with empty tag... - else: - tag.pos = pos - directory.append(tag) + pos = tag.next_pos + directory.append(tag) tree, _ = make_dir_tree(fid, directory) @@ -309,7 +314,7 @@ def _show_tree( for k, kn, size, pos, type_ in zip(kinds[:-1], kinds[1:], sizes, poss, types): if not tag_found and k != tag_id: continue - tag = Tag(k, size, 0, pos) + tag = Tag(kind=k, type=type_, size=size, next=FIFF.FIFFV_NEXT_NONE, pos=pos) if read_limit is None or size <= read_limit: try: tag = read_tag(fid, pos) @@ -348,7 +353,7 @@ def _show_tree( ) else: postpend += " ... type=" + str(type(tag.data)) - postpend = ">" * 20 + "BAD" if not good else postpend + postpend = ">" * 20 + f"BAD @{pos}" if not good else postpend matrix_info = _matrix_info(tag) if matrix_info is not None: _, type_, _, _ = matrix_info diff --git a/mne/_fiff/tag.py b/mne/_fiff/tag.py index 81ed12baf6f..e1ae5ae571a 100644 --- a/mne/_fiff/tag.py +++ b/mne/_fiff/tag.py @@ -7,7 +7,9 @@ import html import re import struct +from dataclasses import dataclass from functools import partial +from typing import Any import numpy as np from scipy.sparse import csc_matrix, csr_matrix @@ -28,40 +30,16 @@ # HELPERS +@dataclass class Tag: - """Tag in FIF tree structure. + """Tag in FIF tree structure.""" - Parameters - ---------- - kind : int - Kind of Tag. - type_ : int - Type of Tag. - size : int - Size in bytes. - int : next - Position of next Tag. - pos : int - Position of Tag is the original file. - """ - - def __init__(self, kind, type_, size, next, pos=None): - self.kind = int(kind) - self.type = int(type_) - self.size = int(size) - self.next = int(next) - self.pos = pos if pos is not None else next - self.pos = int(self.pos) - self.data = None - - def __repr__(self): # noqa: D105 - attrs = list() - for attr in ("kind", "type", "size", "next", "pos", "data"): - try: - attrs.append(f"{attr} {getattr(self, attr)}") - except AttributeError: - pass - return "" + kind: int + type: int + size: int + next: int + pos: int + data: Any = None def __eq__(self, tag): # noqa: D105 return int( @@ -73,17 +51,15 @@ def __eq__(self, tag): # noqa: D105 and self.data == tag.data ) - -def read_tag_info(fid): - """Read Tag info (or header).""" - tag = _read_tag_header(fid) - if tag is None: - return None - if tag.next == 0: - fid.seek(tag.size, 1) - elif tag.next > 0: - fid.seek(tag.next, 0) - return tag + @property + def next_pos(self): + """The next tag position.""" + if self.next == FIFF.FIFFV_NEXT_SEQ: # 0 + return self.pos + 16 + self.size + elif self.next > 0: + return self.next + else: # self.next should be -1 if we get here + return None # safest to return None so that things like fid.seek die def _frombuffer_rows(fid, tag_size, dtype=None, shape=None, rlims=None): @@ -157,16 +133,18 @@ def _loc_to_eeg_loc(loc): # by the function names. -def _read_tag_header(fid): +def _read_tag_header(fid, pos): """Read only the header of a Tag.""" - s = fid.read(4 * 4) + fid.seek(pos, 0) + s = fid.read(16) if len(s) != 16: where = fid.tell() - len(s) extra = f" in file {fid.name}" if hasattr(fid, "name") else "" warn(f"Invalid tag with only {len(s)}/16 bytes at position {where}{extra}") return None # struct.unpack faster than np.frombuffer, saves ~10% of time some places - return Tag(*struct.unpack(">iIii", s)) + kind, type_, size, next_ = struct.unpack(">iIii", s) + return Tag(kind, type_, size, next_, pos) def _read_matrix(fid, tag, shape, rlims): @@ -178,10 +156,10 @@ def _read_matrix(fid, tag, shape, rlims): matrix_coding, matrix_type, bit, dtype = _matrix_info(tag) + pos = tag.pos + 16 + fid.seek(pos + tag.size - 4, 0) if matrix_coding == "dense": # Find dimensions and return to the beginning of tag data - pos = fid.tell() - fid.seek(tag.size - 4, 1) ndim = int(np.frombuffer(fid.read(4), dtype=">i4").item()) fid.seek(-(ndim + 1) * 4, 1) dims = np.frombuffer(fid.read(4 * ndim), dtype=">i4")[::-1] @@ -205,8 +183,6 @@ def _read_matrix(fid, tag, shape, rlims): data.shape = dims else: # Find dimensions and return to the beginning of tag data - pos = fid.tell() - fid.seek(tag.size - 4, 1) ndim = int(np.frombuffer(fid.read(4), dtype=">i4").item()) fid.seek(-(ndim + 2) * 4, 1) dims = np.frombuffer(fid.read(4 * (ndim + 1)), dtype=">i4") @@ -388,7 +364,16 @@ def _read_old_pack(fid, tag, shape, rlims): def _read_dir_entry_struct(fid, tag, shape, rlims): """Read dir entry struct tag.""" - return [_read_tag_header(fid) for _ in range(tag.size // 16 - 1)] + pos = tag.pos + 16 + entries = list() + for offset in range(1, tag.size // 16): + ent = _read_tag_header(fid, pos + offset * 16) + # The position of the real tag on disk is stored in the "next" entry within the + # directory, so we need to overwrite ent.pos. For safety let's also overwrite + # ent.next to point nowhere + ent.pos, ent.next = ent.next, FIFF.FIFFV_NEXT_NONE + entries.append(ent) + return entries def _read_julian(fid, tag, shape, rlims): @@ -439,7 +424,7 @@ def _read_julian(fid, tag, shape, rlims): _call_dict_names[key] = dtype -def read_tag(fid, pos=None, shape=None, rlims=None): +def read_tag(fid, pos, shape=None, rlims=None): """Read a Tag from a file at a given position. Parameters @@ -462,9 +447,7 @@ def read_tag(fid, pos=None, shape=None, rlims=None): tag : Tag The Tag read. """ - if pos is not None: - fid.seek(pos, 0) - tag = _read_tag_header(fid) + tag = _read_tag_header(fid, pos) if tag is None: return tag if tag.size > 0: @@ -477,10 +460,6 @@ def read_tag(fid, pos=None, shape=None, rlims=None): except KeyError: raise Exception(f"Unimplemented tag data type {tag.type}") from None tag.data = fun(fid, tag, shape, rlims) - if tag.next != FIFF.FIFFV_NEXT_SEQ: - # f.seek(tag.next,0) - fid.seek(tag.next, 1) # XXX : fix? pb when tag.next < 0 - return tag diff --git a/mne/_fiff/tree.py b/mne/_fiff/tree.py index 6aa7b5f4539..556dab1a537 100644 --- a/mne/_fiff/tree.py +++ b/mne/_fiff/tree.py @@ -4,12 +4,10 @@ # License: BSD-3-Clause # Copyright the MNE-Python contributors. -import numpy as np from ..utils import logger, verbose from .constants import FIFF -from .tag import Tag, read_tag -from .write import _write, end_block, start_block, write_id +from .tag import read_tag def dir_tree_find(tree, kind): @@ -108,46 +106,3 @@ def make_dir_tree(fid, directory, start=0, indent=0, verbose=None): logger.debug(" " * indent + "end } %d" % block) last = this return tree, last - - -############################################################################### -# Writing - - -def copy_tree(fidin, in_id, nodes, fidout): - """Copy directory subtrees from fidin to fidout.""" - if len(nodes) <= 0: - return - - if not isinstance(nodes, list): - nodes = [nodes] - - for node in nodes: - start_block(fidout, node["block"]) - if node["id"] is not None: - if in_id is not None: - write_id(fidout, FIFF.FIFF_PARENT_FILE_ID, in_id) - - write_id(fidout, FIFF.FIFF_BLOCK_ID, in_id) - write_id(fidout, FIFF.FIFF_PARENT_BLOCK_ID, node["id"]) - - if node["directory"] is not None: - for d in node["directory"]: - # Do not copy these tags - if ( - d.kind == FIFF.FIFF_BLOCK_ID - or d.kind == FIFF.FIFF_PARENT_BLOCK_ID - or d.kind == FIFF.FIFF_PARENT_FILE_ID - ): - continue - - # Read and write tags, pass data through transparently - fidin.seek(d.pos, 0) - tag = Tag(*np.fromfile(fidin, (">i4,>I4,>i4,>i4"), 1)[0]) - tag.data = np.fromfile(fidin, ">B", tag.size) - _write(fidout, tag.data, tag.kind, 1, tag.type, ">B") - - for child in node["children"]: - copy_tree(fidin, in_id, child, fidout) - - end_block(fidout, node["block"]) diff --git a/mne/commands/tests/test_commands.py b/mne/commands/tests/test_commands.py index ea87c717db0..26e1f7fa540 100644 --- a/mne/commands/tests/test_commands.py +++ b/mne/commands/tests/test_commands.py @@ -43,7 +43,7 @@ mne_what, ) from mne.datasets import testing -from mne.io import read_info, read_raw_fif +from mne.io import read_info, read_raw_fif, show_fiff from mne.utils import ( ArgvSetter, _record_warnings, @@ -100,13 +100,22 @@ def test_compare_fiff(): check_usage(mne_compare_fiff) -def test_show_fiff(): +def test_show_fiff(tmp_path): """Test mne compare_fiff.""" check_usage(mne_show_fiff) with ArgvSetter((raw_fname,)): mne_show_fiff.run() with ArgvSetter((raw_fname, "--tag=102")): mne_show_fiff.run() + bad_fname = tmp_path / "test_bad_raw.fif" + with open(bad_fname, "wb") as fout: + with open(raw_fname, "rb") as fin: + fout.write(fin.read(100000)) + with pytest.warns(RuntimeWarning, match="Invalid tag"): + lines = show_fiff(bad_fname, output=list) + last_line = lines[-1] + assert last_line.endswith(">>>>BAD @9015") + assert "302 = FIFF_EPOCH (734412b >f4)" in last_line @requires_mne diff --git a/mne/epochs.py b/mne/epochs.py index c042c207905..d11ba5f59aa 100644 --- a/mne/epochs.py +++ b/mne/epochs.py @@ -39,7 +39,7 @@ pick_info, ) from ._fiff.proj import ProjMixin, setup_proj -from ._fiff.tag import read_tag, read_tag_info +from ._fiff.tag import _read_tag_header, read_tag from ._fiff.tree import dir_tree_find from ._fiff.utils import _make_split_fnames from ._fiff.write import ( @@ -3779,8 +3779,7 @@ def _read_one_epoch_file(f, tree, preload): elif kind == FIFF.FIFF_EPOCH: # delay reading until later fid.seek(pos, 0) - data_tag = read_tag_info(fid) - data_tag.pos = pos + data_tag = _read_tag_header(fid, pos) data_tag.type = data_tag.type ^ (1 << 30) elif kind in [FIFF.FIFF_MNE_BASELINE_MIN, 304]: # Constant 304 was used before v0.11 diff --git a/mne/io/fiff/raw.py b/mne/io/fiff/raw.py index 1c13189f723..39f0466e1eb 100644 --- a/mne/io/fiff/raw.py +++ b/mne/io/fiff/raw.py @@ -16,7 +16,7 @@ from ..._fiff.constants import FIFF from ..._fiff.meas_info import read_meas_info from ..._fiff.open import _fiff_get_fid, _get_next_fname, fiff_open -from ..._fiff.tag import read_tag, read_tag_info +from ..._fiff.tag import _call_dict, read_tag from ..._fiff.tree import dir_tree_find from ..._fiff.utils import _mult_cal_one from ...annotations import Annotations, _read_annotations_fif @@ -255,48 +255,40 @@ def _read_raw_file( nskip = 0 orig_format = None + _byte_dict = { + FIFF.FIFFT_DAU_PACK16: 2, + FIFF.FIFFT_SHORT: 2, + FIFF.FIFFT_FLOAT: 4, + FIFF.FIFFT_DOUBLE: 8, + FIFF.FIFFT_INT: 4, + FIFF.FIFFT_COMPLEX_FLOAT: 8, + FIFF.FIFFT_COMPLEX_DOUBLE: 16, + } + _orig_format_dict = { + FIFF.FIFFT_DAU_PACK16: "short", + FIFF.FIFFT_SHORT: "short", + FIFF.FIFFT_FLOAT: "single", + FIFF.FIFFT_DOUBLE: "double", + FIFF.FIFFT_INT: "int", + FIFF.FIFFT_COMPLEX_FLOAT: "single", + FIFF.FIFFT_COMPLEX_DOUBLE: "double", + } + for k in range(first, nent): ent = directory[k] # There can be skips in the data (e.g., if the user unclicked) # an re-clicked the button - if ent.kind == FIFF.FIFF_DATA_SKIP: - tag = read_tag(fid, ent.pos) - nskip = int(tag.data.item()) - elif ent.kind == FIFF.FIFF_DATA_BUFFER: + if ent.kind == FIFF.FIFF_DATA_BUFFER: # Figure out the number of samples in this buffer - if ent.type == FIFF.FIFFT_DAU_PACK16: - nsamp = ent.size // (2 * nchan) - elif ent.type == FIFF.FIFFT_SHORT: - nsamp = ent.size // (2 * nchan) - elif ent.type == FIFF.FIFFT_FLOAT: - nsamp = ent.size // (4 * nchan) - elif ent.type == FIFF.FIFFT_DOUBLE: - nsamp = ent.size // (8 * nchan) - elif ent.type == FIFF.FIFFT_INT: - nsamp = ent.size // (4 * nchan) - elif ent.type == FIFF.FIFFT_COMPLEX_FLOAT: - nsamp = ent.size // (8 * nchan) - elif ent.type == FIFF.FIFFT_COMPLEX_DOUBLE: - nsamp = ent.size // (16 * nchan) - else: - raise ValueError( - "Cannot handle data buffers of type " "%d" % ent.type - ) + try: + div = _byte_dict[ent.type] + except KeyError: + raise RuntimeError( + f"Cannot handle data buffers of type {ent.type}" + ) from None + nsamp = ent.size // (div * nchan) if orig_format is None: - if ent.type == FIFF.FIFFT_DAU_PACK16: - orig_format = "short" - elif ent.type == FIFF.FIFFT_SHORT: - orig_format = "short" - elif ent.type == FIFF.FIFFT_FLOAT: - orig_format = "single" - elif ent.type == FIFF.FIFFT_DOUBLE: - orig_format = "double" - elif ent.type == FIFF.FIFFT_INT: - orig_format = "int" - elif ent.type == FIFF.FIFFT_COMPLEX_FLOAT: - orig_format = "single" - elif ent.type == FIFF.FIFFT_COMPLEX_DOUBLE: - orig_format = "double" + orig_format = _orig_format_dict[ent.type] # Do we have an initial skip pending? if first_skip > 0: @@ -327,6 +319,9 @@ def _read_raw_file( ) ) first_samp += nsamp + elif ent.kind == FIFF.FIFF_DATA_SKIP: + tag = read_tag(fid, ent.pos) + nskip = int(tag.data.item()) next_fname = _get_next_fname(fid, fname_rep, tree) @@ -381,22 +376,17 @@ def _dtype(self): if self._dtype_ is not None: return self._dtype_ dtype = None - for raw_extra, filename in zip(self._raw_extras, self._filenames): + for raw_extra in self._raw_extras: for ent in raw_extra["ent"]: if ent is not None: - with _fiff_get_fid(filename) as fid: - fid.seek(ent.pos, 0) - tag = read_tag_info(fid) - if tag is not None: - if tag.type in ( - FIFF.FIFFT_COMPLEX_FLOAT, - FIFF.FIFFT_COMPLEX_DOUBLE, - ): - dtype = np.complex128 - else: - dtype = np.float64 - if dtype is not None: - break + if ent.type in ( + FIFF.FIFFT_COMPLEX_FLOAT, + FIFF.FIFFT_COMPLEX_DOUBLE, + ): + dtype = np.complex128 + else: + dtype = np.float64 + break if dtype is not None: break if dtype is None: @@ -421,27 +411,31 @@ def _read_segment_file(self, data, idx, fi, start, stop, cals, mult): first_pick = max(start - first, 0) last_pick = min(nsamp, stop - first) picksamp = last_pick - first_pick - # only read data if it exists - if ent is not None: - one = read_tag( - fid, - ent.pos, - shape=(nsamp, nchan), - rlims=(first_pick, last_pick), - ).data - try: - one.shape = (picksamp, nchan) - except AttributeError: # one is None - n_bad += picksamp - else: - _mult_cal_one( - data[:, offset : (offset + picksamp)], - one.T, - idx, - cals, - mult, - ) + this_start = offset offset += picksamp + this_stop = offset + # only read data if it exists + if ent is None: + continue # just use zeros for gaps + # faster to always read full tag, taking advantage of knowing the header + # already (cutting out some of read_tag) ... + fid.seek(ent.pos + 16, 0) + one = _call_dict[ent.type](fid, ent, shape=None, rlims=None) + try: + one.shape = (nsamp, nchan) + except AttributeError: # one is None + n_bad += picksamp + else: + # ... then pick samples we want + if first_pick != 0 or last_pick != nsamp: + one = one[first_pick:last_pick] + _mult_cal_one( + data[:, this_start:this_stop], + one.T, + idx, + cals, + mult, + ) if n_bad: warn( f"FIF raw buffer could not be read, acquisition error " diff --git a/mne/io/fiff/tests/test_raw_fiff.py b/mne/io/fiff/tests/test_raw_fiff.py index 329d205e8d3..154d70b0dee 100644 --- a/mne/io/fiff/tests/test_raw_fiff.py +++ b/mne/io/fiff/tests/test_raw_fiff.py @@ -30,8 +30,7 @@ pick_types, ) from mne._fiff.constants import FIFF -from mne._fiff.open import read_tag, read_tag_info -from mne._fiff.tag import _read_tag_header +from mne._fiff.tag import _read_tag_header, read_tag from mne.annotations import Annotations from mne.datasets import testing from mne.filter import filter_data @@ -2044,8 +2043,7 @@ def test_bad_acq(fname): raw = read_raw_fif(fname, allow_maxshield="yes").load_data() with open(fname, "rb") as fid: for ent in raw._raw_extras[0]["ent"]: - fid.seek(ent.pos, 0) - tag = _read_tag_header(fid) + tag = _read_tag_header(fid, ent.pos) # hack these, others (kind, type) should be correct tag.pos, tag.next = ent.pos, ent.next assert tag == ent @@ -2085,9 +2083,9 @@ def test_corrupted(tmp_path, offset): # at the end, so use the skip one (straight from acq). raw = read_raw_fif(skip_fname) with open(skip_fname, "rb") as fid: - tag = read_tag_info(fid) - tag = read_tag(fid) - dirpos = int(tag.data.item()) + file_id_tag = read_tag(fid, 0) + dir_pos_tag = read_tag(fid, file_id_tag.next_pos) + dirpos = int(dir_pos_tag.data.item()) assert dirpos == 12641532 fid.seek(0) data = fid.read(dirpos + offset) From 8eb10e36ebfdac990ed86da919f2729177090dd2 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Thu, 11 Jan 2024 14:52:03 -0500 Subject: [PATCH 148/405] BUG: Fix bug with recon trans (#12348) Co-authored-by: motofumi-fushimi <30593537+motofumi-fushimi@users.noreply.github.com> --- .mailmap | 1 + doc/changes/devel/12348.bugfix.rst | 1 + doc/changes/names.inc | 2 ++ mne/preprocessing/maxwell.py | 7 +++- mne/preprocessing/tests/test_maxwell.py | 43 +++++++++++-------------- 5 files changed, 28 insertions(+), 26 deletions(-) create mode 100644 doc/changes/devel/12348.bugfix.rst diff --git a/.mailmap b/.mailmap index e6d5377c402..10afa14ea85 100644 --- a/.mailmap +++ b/.mailmap @@ -220,6 +220,7 @@ Mikołaj Magnuski Mikolaj Magnuski mmagnuski Mohamed Sherif mohdsherif Mohammad Daneshzand <55800429+mdaneshzand@users.noreply.github.com> mdaneshzand <55800429+mdaneshzand@users.noreply.github.com> +Motofumi Fushimi <30593537+motofumi-fushimi@users.noreply.github.com> motofumi-fushimi <30593537+motofumi-fushimi@users.noreply.github.com> Natalie Klein natalieklein Nathalie Gayraud Nathalie Nathalie Gayraud Nathalie diff --git a/doc/changes/devel/12348.bugfix.rst b/doc/changes/devel/12348.bugfix.rst new file mode 100644 index 00000000000..aad91ed9dec --- /dev/null +++ b/doc/changes/devel/12348.bugfix.rst @@ -0,0 +1 @@ +Fix bug in :func:`mne.preprocessing.maxwell_filter` where calibration was incorrectly applied during virtual sensor reconstruction, by `Eric Larson`_ and :newcontrib:`Motofumi Fushimi`. diff --git a/doc/changes/names.inc b/doc/changes/names.inc index f1a0c951da4..811029ddaa7 100644 --- a/doc/changes/names.inc +++ b/doc/changes/names.inc @@ -386,6 +386,8 @@ .. _Moritz Gerster: https://github.com/moritz-gerster +.. _Motofumi Fushimi: https://github.com/motofumi-fushimi/motofumi-fushimi.github.io + .. _Natalie Klein: https://github.com/natalieklein .. _Nathalie Gayraud: https://github.com/ngayraud diff --git a/mne/preprocessing/maxwell.py b/mne/preprocessing/maxwell.py index 25430db6f9e..5620f300ff1 100644 --- a/mne/preprocessing/maxwell.py +++ b/mne/preprocessing/maxwell.py @@ -519,8 +519,12 @@ def _prep_maxwell_filter( # sss_cal = dict() if calibration is not None: + # Modifies info in place, so make a copy for recon later + info_recon = info.copy() calibration, sss_cal = _update_sensor_geometry(info, calibration, ignore_ref) mag_or_fine.fill(True) # all channels now have some mag-type data + else: + info_recon = info # Determine/check the origin of the expansion origin = _check_origin(origin, info, coord_frame, disp=True) @@ -553,7 +557,8 @@ def _prep_maxwell_filter( # exp = dict(origin=origin_head, int_order=int_order, ext_order=0) all_coils = _prep_mf_coils(info, ignore_ref) - S_recon = _trans_sss_basis(exp, all_coils, recon_trans, coil_scale) + all_coils_recon = _prep_mf_coils(info_recon, ignore_ref) + S_recon = _trans_sss_basis(exp, all_coils_recon, recon_trans, coil_scale) exp["ext_order"] = ext_order exp["extended_proj"] = extended_proj del extended_proj diff --git a/mne/preprocessing/tests/test_maxwell.py b/mne/preprocessing/tests/test_maxwell.py index 6234b79c544..336a007dd16 100644 --- a/mne/preprocessing/tests/test_maxwell.py +++ b/mne/preprocessing/tests/test_maxwell.py @@ -730,7 +730,8 @@ def test_spatiotemporal_only(): raw_tsss = maxwell_filter(raw, st_duration=tmax, st_correlation=1.0, st_only=True) assert_allclose(raw[:][0], raw_tsss[:][0]) # degenerate - pytest.raises(ValueError, maxwell_filter, raw, st_only=True) # no ST + with pytest.raises(ValueError, match="must not be None if st_only"): + maxwell_filter(raw, st_only=True) # two-step process equivalent to single-step process raw_tsss = maxwell_filter(raw, st_duration=tmax, st_only=True) raw_tsss = maxwell_filter(raw_tsss) @@ -771,7 +772,7 @@ def test_fine_calibration(): log = log.getvalue() assert "Using fine calibration" in log assert fine_cal_fname.stem in log - assert_meg_snr(raw_sss, sss_fine_cal, 82, 611) + assert_meg_snr(raw_sss, sss_fine_cal, 1.3, 180) # similar to MaxFilter py_cal = raw_sss.info["proc_history"][0]["max_info"]["sss_cal"] assert py_cal is not None assert len(py_cal) > 0 @@ -812,15 +813,11 @@ def test_fine_calibration(): regularize=None, bad_condition="ignore", ) - assert_meg_snr(raw_sss_3D, sss_fine_cal, 1.0, 6.0) + assert_meg_snr(raw_sss_3D, sss_fine_cal, 0.9, 6.0) + assert_meg_snr(raw_sss_3D, raw_sss, 1.1, 6.0) # slightly better than 1D raw_ctf = read_crop(fname_ctf_raw).apply_gradient_compensation(0) - pytest.raises( - RuntimeError, - maxwell_filter, - raw_ctf, - origin=(0.0, 0.0, 0.04), - calibration=fine_cal_fname, - ) + with pytest.raises(RuntimeError, match="Not all MEG channels"): + maxwell_filter(raw_ctf, origin=(0.0, 0.0, 0.04), calibration=fine_cal_fname) @pytest.mark.slowtest @@ -884,7 +881,8 @@ def test_cross_talk(tmp_path): assert len(py_ctc) > 0 with pytest.raises(TypeError, match="path-like"): maxwell_filter(raw, cross_talk=raw) - pytest.raises(ValueError, maxwell_filter, raw, cross_talk=raw_fname) + with pytest.raises(ValueError, match="Invalid cross-talk FIF"): + maxwell_filter(raw, cross_talk=raw_fname) mf_ctc = sss_ctc.info["proc_history"][0]["max_info"]["sss_ctc"] del mf_ctc["block_id"] # we don't write this assert isinstance(py_ctc["decoupler"], sparse.csc_matrix) @@ -916,13 +914,8 @@ def test_cross_talk(tmp_path): with pytest.warns(RuntimeWarning, match="Not all cross-talk channels"): maxwell_filter(raw_missing, cross_talk=ctc_fname) # MEG channels not in cross-talk - pytest.raises( - RuntimeError, - maxwell_filter, - raw_ctf, - origin=(0.0, 0.0, 0.04), - cross_talk=ctc_fname, - ) + with pytest.raises(RuntimeError, match="Missing MEG channels"): + maxwell_filter(raw_ctf, origin=(0.0, 0.0, 0.04), cross_talk=ctc_fname) @testing.requires_testing_data @@ -970,10 +963,10 @@ def test_head_translation(): read_info(sample_fname)["dev_head_t"]["trans"], ) # Degenerate cases - pytest.raises( - RuntimeError, maxwell_filter, raw, destination=mf_head_origin, coord_frame="meg" - ) - pytest.raises(ValueError, maxwell_filter, raw, destination=[0.0] * 4) + with pytest.raises(RuntimeError, match=".* can only be set .* head .*"): + maxwell_filter(raw, destination=mf_head_origin, coord_frame="meg") + with pytest.raises(ValueError, match="destination must be"): + maxwell_filter(raw, destination=[0.0] * 4) # TODO: Eventually add simulation tests mirroring Taulu's original paper @@ -1395,7 +1388,7 @@ def test_all(): coord_frames = ("head", "head", "meg", "head") ctcs = (ctc_fname, ctc_fname, ctc_fname, ctc_mgh_fname) mins = (3.5, 3.5, 1.2, 0.9) - meds = (10.8, 10.4, 3.2, 6.0) + meds = (10.8, 10.2, 3.2, 5.9) st_durs = (1.0, 1.0, 1.0, None) destinations = (None, sample_fname, None, None) origins = (mf_head_origin, mf_head_origin, mf_meg_origin, mf_head_origin) @@ -1436,7 +1429,7 @@ def test_triux(): sss_py = maxwell_filter( raw, coord_frame="meg", regularize=None, calibration=tri_cal_fname ) - assert_meg_snr(sss_py, read_crop(tri_sss_cal_fname), 22, 200) + assert_meg_snr(sss_py, read_crop(tri_sss_cal_fname), 5, 100) # ctc+cal sss_py = maxwell_filter( raw, @@ -1445,7 +1438,7 @@ def test_triux(): calibration=tri_cal_fname, cross_talk=tri_ctc_fname, ) - assert_meg_snr(sss_py, read_crop(tri_sss_ctc_cal_fname), 28, 200) + assert_meg_snr(sss_py, read_crop(tri_sss_ctc_cal_fname), 5, 100) # regularization sss_py = maxwell_filter(raw, coord_frame="meg", regularize="in") sss_mf = read_crop(tri_sss_reg_fname) From eefd179b767fea456fd1eab3700c5299310b5b8e Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Thu, 11 Jan 2024 15:09:39 -0500 Subject: [PATCH 149/405] MAINT: Check and format with NPY201 (#12353) --- .pre-commit-config.yaml | 9 ++++----- mne/_fiff/open.py | 2 +- mne/decoding/tests/test_search_light.py | 16 ++++++++-------- mne/epochs.py | 2 +- mne/fixes.py | 2 +- mne/preprocessing/_annotate_amplitude.py | 2 +- mne/preprocessing/artifact_detection.py | 2 +- mne/preprocessing/interpolate.py | 4 ++-- mne/preprocessing/tests/test_realign.py | 2 +- mne/proj.py | 2 +- mne/report/report.py | 4 ++-- mne/source_space/_source_space.py | 2 +- mne/stats/cluster_level.py | 4 ++-- mne/stats/tests/test_cluster_level.py | 4 ++-- mne/tests/test_annotations.py | 4 +++- mne/utils/numerics.py | 2 +- mne/viz/_mpl_figure.py | 2 +- mne/viz/evoked_field.py | 4 +++- 18 files changed, 36 insertions(+), 33 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 038d082b2e6..e8d1a4ae4bf 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -7,14 +7,13 @@ repos: name: ruff lint mne args: ["--fix"] files: ^mne/ + - id: ruff + name: ruff lint mne preview + args: ["--fix", "--preview", "--select=NPY201"] + files: ^mne/ - id: ruff-format name: ruff format mne files: ^mne/ - - # Ruff tutorials and examples - - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.11 - hooks: - id: ruff name: ruff lint tutorials and examples # D103: missing docstring in public function diff --git a/mne/_fiff/open.py b/mne/_fiff/open.py index 02fcda445a0..1b3117dc63e 100644 --- a/mne/_fiff/open.py +++ b/mne/_fiff/open.py @@ -263,7 +263,7 @@ def show_fiff( tag_id=tag, show_bytes=show_bytes, ) - if output == str: + if output is str: out = "\n".join(out) return out diff --git a/mne/decoding/tests/test_search_light.py b/mne/decoding/tests/test_search_light.py index 992efbfec30..6b445972d5f 100644 --- a/mne/decoding/tests/test_search_light.py +++ b/mne/decoding/tests/test_search_light.py @@ -63,19 +63,19 @@ def test_search_light(): # transforms pytest.raises(ValueError, sl.predict, X[:, :, :2]) y_trans = sl.transform(X) - assert X.dtype == y_trans.dtype == float + assert X.dtype == y_trans.dtype == np.dtype(float) y_pred = sl.predict(X) - assert y_pred.dtype == int + assert y_pred.dtype == np.dtype(int) assert_array_equal(y_pred.shape, [n_epochs, n_time]) y_proba = sl.predict_proba(X) - assert y_proba.dtype == float + assert y_proba.dtype == np.dtype(float) assert_array_equal(y_proba.shape, [n_epochs, n_time, 2]) # score score = sl.score(X, y) assert_array_equal(score.shape, [n_time]) assert np.sum(np.abs(score)) != 0 - assert score.dtype == float + assert score.dtype == np.dtype(float) sl = SlidingEstimator(logreg) assert_equal(sl.scoring, None) @@ -122,7 +122,7 @@ def test_search_light(): X = rng.randn(*X.shape) # randomize X to avoid AUCs in [0, 1] score_sl = sl1.score(X, y) assert_array_equal(score_sl.shape, [n_time]) - assert score_sl.dtype == float + assert score_sl.dtype == np.dtype(float) # Check that scoring was applied adequately scoring = make_scorer(roc_auc_score, needs_threshold=True) @@ -195,9 +195,9 @@ def test_generalization_light(): # transforms y_pred = gl.predict(X) assert_array_equal(y_pred.shape, [n_epochs, n_time, n_time]) - assert y_pred.dtype == int + assert y_pred.dtype == np.dtype(int) y_proba = gl.predict_proba(X) - assert y_proba.dtype == float + assert y_proba.dtype == np.dtype(float) assert_array_equal(y_proba.shape, [n_epochs, n_time, n_time, 2]) # transform to different datasize @@ -208,7 +208,7 @@ def test_generalization_light(): score = gl.score(X[:, :, :3], y) assert_array_equal(score.shape, [n_time, 3]) assert np.sum(np.abs(score)) != 0 - assert score.dtype == float + assert score.dtype == np.dtype(float) gl = GeneralizingEstimator(logreg, scoring="roc_auc") gl.fit(X, y) diff --git a/mne/epochs.py b/mne/epochs.py index d11ba5f59aa..53cab1c81f0 100644 --- a/mne/epochs.py +++ b/mne/epochs.py @@ -1537,7 +1537,7 @@ def drop(self, indices, reason="USER", verbose=None): if indices.ndim > 1: raise ValueError("indices must be a scalar or a 1-d array") - if indices.dtype == bool: + if indices.dtype == np.dtype(bool): indices = np.where(indices)[0] try_idx = np.where(indices < 0, indices + len(self.events), indices) diff --git a/mne/fixes.py b/mne/fixes.py index 1d3cc5aadb4..4759366f386 100644 --- a/mne/fixes.py +++ b/mne/fixes.py @@ -223,7 +223,7 @@ def get_params(self, deep=True): try: with warnings.catch_warnings(record=True) as w: value = getattr(self, key, None) - if len(w) and w[0].category == DeprecationWarning: + if len(w) and w[0].category is DeprecationWarning: # if the parameter is deprecated, don't show it continue finally: diff --git a/mne/preprocessing/_annotate_amplitude.py b/mne/preprocessing/_annotate_amplitude.py index 527e74650f0..2f61b19c3db 100644 --- a/mne/preprocessing/_annotate_amplitude.py +++ b/mne/preprocessing/_annotate_amplitude.py @@ -249,7 +249,7 @@ def _check_min_duration(min_duration, raw_duration): def _reject_short_segments(arr, min_duration_samples): """Check if flat or peak segments are longer than the minimum duration.""" - assert arr.dtype == bool and arr.ndim == 2 + assert arr.dtype == np.dtype(bool) and arr.ndim == 2 for k, ch in enumerate(arr): onsets, offsets = _mask_to_onsets_offsets(ch) _mark_inner(arr[k], onsets, offsets, min_duration_samples) diff --git a/mne/preprocessing/artifact_detection.py b/mne/preprocessing/artifact_detection.py index d5bcfccb730..d2bed58fd78 100644 --- a/mne/preprocessing/artifact_detection.py +++ b/mne/preprocessing/artifact_detection.py @@ -599,7 +599,7 @@ def annotate_break( # Log some info n_breaks = len(break_annotations) break_times = [ - f"{o:.1f} – {o+d:.1f} s [{d:.1f} s]" + f"{o:.1f} – {o + d:.1f} s [{d:.1f} s]" for o, d in zip(break_annotations.onset, break_annotations.duration) ] break_times = "\n ".join(break_times) diff --git a/mne/preprocessing/interpolate.py b/mne/preprocessing/interpolate.py index 828261d2651..0cbe8b73ce4 100644 --- a/mne/preprocessing/interpolate.py +++ b/mne/preprocessing/interpolate.py @@ -163,7 +163,7 @@ def interpolate_bridged_electrodes(inst, bridged_idx, bad_limit=4): # compute centroid position in spherical "head" coordinates pos_virtual = _find_centroid_sphere(pos["ch_pos"], group_names) # create the virtual channel info and set the position - virtual_info = create_info([f"virtual {k+1}"], inst.info["sfreq"], "eeg") + virtual_info = create_info([f"virtual {k + 1}"], inst.info["sfreq"], "eeg") virtual_info["chs"][0]["loc"][:3] = pos_virtual # create virtual channel data = inst.get_data(picks=group_names) @@ -182,7 +182,7 @@ def interpolate_bridged_electrodes(inst, bridged_idx, bad_limit=4): nave=inst.nave, kind=inst.kind, ) - virtual_chs[f"virtual {k+1}"] = virtual_ch + virtual_chs[f"virtual {k + 1}"] = virtual_ch # add the virtual channels inst.add_channels(list(virtual_chs.values()), force_update_info=True) diff --git a/mne/preprocessing/tests/test_realign.py b/mne/preprocessing/tests/test_realign.py index 60ec5b0d5ba..952c6ac30bb 100644 --- a/mne/preprocessing/tests/test_realign.py +++ b/mne/preprocessing/tests/test_realign.py @@ -158,7 +158,7 @@ def _assert_similarity(raw, other, n_events, ratio_other, events_raw=None): evoked_other = Epochs(other, events_other, **kwargs).average() assert evoked_raw.nave == evoked_other.nave == len(events_raw) assert len(evoked_raw.data) == len(evoked_other.data) == 1 # just EEG - if 0.99 <= ratio_other <= 1.01: # when drift is not too large + if 0.99 <= ratio_other <= 1.01: # when drift is not too large corr = np.corrcoef(evoked_raw.data[0], evoked_other.data[0])[0, 1] assert 0.9 <= corr <= 1.0 return evoked_raw, events_raw, evoked_other, events_other diff --git a/mne/proj.py b/mne/proj.py index a5bb406b844..6395a187a54 100644 --- a/mne/proj.py +++ b/mne/proj.py @@ -151,7 +151,7 @@ def _compute_proj( nrow=1, ncol=u.size, ) - desc = f"{kind}-{desc_prefix}-PCA-{k+1:02d}" + desc = f"{kind}-{desc_prefix}-PCA-{k + 1:02d}" logger.info("Adding projection: %s", desc) proj = Projection( active=False, diff --git a/mne/report/report.py b/mne/report/report.py index ab56d03ab7e..f8243d8c820 100644 --- a/mne/report/report.py +++ b/mne/report/report.py @@ -2271,7 +2271,7 @@ def add_figure( elif caption is None and len(figs) == 1: captions = [None] elif caption is None and len(figs) > 1: - captions = [f"Figure {i+1}" for i in range(len(figs))] + captions = [f"Figure {i + 1}" for i in range(len(figs))] else: captions = tuple(caption) @@ -3143,7 +3143,7 @@ def _add_raw_butterfly_segments( del orig_annotations - captions = [f"Segment {i+1} of {len(images)}" for i in range(len(images))] + captions = [f"Segment {i + 1} of {len(images)}" for i in range(len(images))] self._add_slider( figs=None, diff --git a/mne/source_space/_source_space.py b/mne/source_space/_source_space.py index ee8ef432a90..8ec15ad48b0 100644 --- a/mne/source_space/_source_space.py +++ b/mne/source_space/_source_space.py @@ -2408,7 +2408,7 @@ def _grid_interp(from_shape, to_shape, trans, order=1, inuse=None): shape = (np.prod(to_shape), np.prod(from_shape)) if inuse is None: inuse = np.ones(shape[1], bool) - assert inuse.dtype == bool + assert inuse.dtype == np.dtype(bool) assert inuse.shape == (shape[1],) data, indices, indptr = _grid_interp_jit(from_shape, to_shape, trans, order, inuse) data = np.concatenate(data) diff --git a/mne/stats/cluster_level.py b/mne/stats/cluster_level.py index 479bba3f45b..82bd0943c29 100644 --- a/mne/stats/cluster_level.py +++ b/mne/stats/cluster_level.py @@ -479,7 +479,7 @@ def _find_clusters( len_c = c.stop - c.start elif isinstance(c, tuple): len_c = len(c) - elif c.dtype == bool: + elif c.dtype == np.dtype(bool): len_c = np.sum(c) else: len_c = len(c) @@ -1634,7 +1634,7 @@ def _reshape_clusters(clusters, sample_shape): """Reshape cluster masks or indices to be of the correct shape.""" # format of the bool mask and indices are ndarrays if len(clusters) > 0 and isinstance(clusters[0], np.ndarray): - if clusters[0].dtype == bool: # format of mask + if clusters[0].dtype == np.dtype(bool): # format of mask clusters = [c.reshape(sample_shape) for c in clusters] else: # format of indices clusters = [np.unravel_index(c, sample_shape) for c in clusters] diff --git a/mne/stats/tests/test_cluster_level.py b/mne/stats/tests/test_cluster_level.py index d0fe0672bde..c1c4ba40851 100644 --- a/mne/stats/tests/test_cluster_level.py +++ b/mne/stats/tests/test_cluster_level.py @@ -610,7 +610,7 @@ def test_permutation_adjacency_equiv(numba_conditional): ) # make sure our output datatype is correct assert isinstance(clusters[0], np.ndarray) - assert clusters[0].dtype == bool + assert clusters[0].dtype == np.dtype(bool) assert_array_equal(clusters[0].shape, X.shape[1:]) # make sure all comparisons were done; for TFCE, no perm @@ -847,7 +847,7 @@ def test_output_equiv(shape, out_type, adjacency): assert isinstance(clu[0], slice) else: assert isinstance(clu, np.ndarray) - assert clu.dtype == bool + assert clu.dtype == np.dtype(bool) assert clu.shape == shape got_mask[clu] = n else: diff --git a/mne/tests/test_annotations.py b/mne/tests/test_annotations.py index 8f3124d6a30..eae1000cbdd 100644 --- a/mne/tests/test_annotations.py +++ b/mne/tests/test_annotations.py @@ -278,7 +278,9 @@ def test_crop(tmp_path): assert raw_read.annotations is not None assert len(raw_read.annotations.onset) == 0 # test saving and reloading cropped annotations in raw instance - info = create_info([f"EEG{i+1}" for i in range(3)], ch_types=["eeg"] * 3, sfreq=50) + info = create_info( + [f"EEG{i + 1}" for i in range(3)], ch_types=["eeg"] * 3, sfreq=50 + ) raw = RawArray(np.zeros((3, 50 * 20)), info) annotation = mne.Annotations([8, 12, 15], [2] * 3, [1, 2, 3]) raw = raw.set_annotations(annotation) diff --git a/mne/utils/numerics.py b/mne/utils/numerics.py index 64bc4515f93..33e313f362f 100644 --- a/mne/utils/numerics.py +++ b/mne/utils/numerics.py @@ -938,7 +938,7 @@ def _fit(self, X): def _mask_to_onsets_offsets(mask): """Group boolean mask into contiguous onset:offset pairs.""" - assert mask.dtype == bool and mask.ndim == 1 + assert mask.dtype == np.dtype(bool) and mask.ndim == 1 mask = mask.astype(int) diff = np.diff(mask) onsets = np.where(diff > 0)[0] + 1 diff --git a/mne/viz/_mpl_figure.py b/mne/viz/_mpl_figure.py index 9835afa4e2b..da19372d8bc 100644 --- a/mne/viz/_mpl_figure.py +++ b/mne/viz/_mpl_figure.py @@ -1847,7 +1847,7 @@ def _draw_one_scalebar(self, x, y, ch_type): color = "#AA3377" # purple kwargs = dict(color=color, zorder=self.mne.zorder["scalebar"]) if ch_type == "time": - label = f"{self.mne.boundary_times[1]/2:.2f} s" + label = f"{self.mne.boundary_times[1] / 2:.2f} s" text = self.mne.ax_main.text( x[0] + 0.015, y[1] - 0.05, diff --git a/mne/viz/evoked_field.py b/mne/viz/evoked_field.py index dd691fccf3c..31e87772e91 100644 --- a/mne/viz/evoked_field.py +++ b/mne/viz/evoked_field.py @@ -473,7 +473,9 @@ def _on_colormap_range(self, event): if self._show_density: surf_map["mesh"].update_overlay(name="field", rng=[vmin, vmax]) # Update the GUI widgets - if type == "meg": + # TODO: type is undefined here and only avoids a flake warning because it's + # a builtin! + if type == "meg": # noqa: E721 scaling = DEFAULTS["scalings"]["grad"] else: scaling = DEFAULTS["scalings"]["eeg"] From b8708b46499882062872cf7dea114dc08ad2b8cb Mon Sep 17 00:00:00 2001 From: Daniel McCloy Date: Mon, 15 Jan 2024 12:11:15 -0600 Subject: [PATCH 150/405] MAINT: workaround for pyvista / numpy dtypes in CIs (#12363) --- tools/github_actions_dependencies.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/github_actions_dependencies.sh b/tools/github_actions_dependencies.sh index b9b425c67fb..c56dd3d9ad0 100755 --- a/tools/github_actions_dependencies.sh +++ b/tools/github_actions_dependencies.sh @@ -44,7 +44,7 @@ else pip install $STD_ARGS --only-binary ":all:" --extra-index-url "https://wheels.vtk.org" vtk python -c "import vtk" echo "PyVista" - pip install $STD_ARGS git+https://github.com/pyvista/pyvista + pip install $STD_ARGS git+https://github.com/drammock/pyvista@numpy-2-compat echo "pyvistaqt" pip install $STD_ARGS git+https://github.com/pyvista/pyvistaqt echo "imageio-ffmpeg, xlrd, mffpy" From acf7b887976dc30f3edf93883911dfa9e49faae8 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 15 Jan 2024 20:27:50 +0000 Subject: [PATCH 151/405] [pre-commit.ci] pre-commit autoupdate (#12364) 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 e8d1a4ae4bf..427c5a09468 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.1.11 + rev: v0.1.13 hooks: - id: ruff name: ruff lint mne From 2040898ac14e79353b7a23a07e177d1633298c0f Mon Sep 17 00:00:00 2001 From: Clemens Brunner Date: Mon, 15 Jan 2024 22:05:43 +0100 Subject: [PATCH 152/405] Enable Ruff UP rules (#12358) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- doc/changes/devel/12358.other.rst | 1 + examples/datasets/limo_data.py | 2 +- examples/datasets/opm_data.py | 4 +- examples/decoding/decoding_csp_eeg.py | 4 +- examples/decoding/receptive_field_mtrf.py | 14 +- .../compute_mne_inverse_raw_in_label.py | 2 +- .../inverse/compute_mne_inverse_volume.py | 2 +- examples/inverse/evoked_ers_source_power.py | 12 +- examples/inverse/label_source_activations.py | 2 +- examples/inverse/mixed_norm_inverse.py | 4 +- examples/inverse/read_stc.py | 4 +- .../preprocessing/define_target_events.py | 2 +- examples/preprocessing/eeg_bridging.py | 11 +- examples/preprocessing/ica_comparison.py | 2 +- examples/preprocessing/otp.py | 10 +- examples/simulation/simulate_raw_data.py | 4 +- .../time_frequency_simulated.py | 2 +- examples/visualization/evoked_topomap.py | 2 +- examples/visualization/evoked_whitening.py | 2 +- mne/_fiff/_digitization.py | 30 ++--- mne/_fiff/meas_info.py | 74 ++++------ mne/_fiff/open.py | 8 +- mne/_fiff/pick.py | 32 ++--- mne/_fiff/proj.py | 2 +- mne/_fiff/reference.py | 10 +- mne/_fiff/tests/test_constants.py | 8 +- mne/_fiff/utils.py | 7 +- mne/_fiff/what.py | 2 +- mne/_freesurfer.py | 4 +- mne/_ola.py | 52 +++---- mne/annotations.py | 34 +++-- mne/baseline.py | 22 ++- mne/beamformer/_compute_beamformer.py | 23 ++-- mne/beamformer/_dics.py | 5 +- mne/beamformer/_lcmv.py | 6 +- mne/beamformer/_rap_music.py | 4 +- mne/bem.py | 90 ++++++------- mne/channels/_dig_montage_utils.py | 2 +- mne/channels/_standard_montage_utils.py | 14 +- mne/channels/channels.py | 10 +- mne/channels/interpolation.py | 2 +- mne/channels/layout.py | 8 +- mne/channels/montage.py | 22 ++- mne/channels/tests/test_montage.py | 43 +++--- mne/chpi.py | 94 ++++++------- mne/commands/mne_anonymize.py | 2 +- mne/commands/mne_compute_proj_ecg.py | 2 +- mne/commands/mne_compute_proj_eog.py | 2 +- mne/commands/mne_maxfilter.py | 2 +- mne/commands/tests/test_commands.py | 12 +- mne/commands/utils.py | 4 +- mne/conftest.py | 2 +- mne/coreg.py | 36 +++-- mne/cov.py | 66 +++++---- mne/cuda.py | 2 +- mne/datasets/sleep_physionet/_utils.py | 8 +- mne/datasets/tests/test_datasets.py | 2 +- mne/datasets/utils.py | 17 ++- mne/decoding/base.py | 23 ++-- mne/decoding/csp.py | 10 +- mne/decoding/mixin.py | 12 +- mne/decoding/receptive_field.py | 56 ++++---- mne/decoding/search_light.py | 9 +- mne/decoding/ssd.py | 3 +- mne/decoding/tests/test_receptive_field.py | 4 +- mne/decoding/tests/test_search_light.py | 2 +- mne/decoding/time_delaying_ridge.py | 8 +- mne/decoding/transformer.py | 2 +- mne/dipole.py | 63 +++++---- mne/epochs.py | 72 +++++----- mne/event.py | 6 +- mne/evoked.py | 51 +++---- mne/filter.py | 127 +++++++----------- mne/forward/_make_forward.py | 2 +- mne/io/array/array.py | 21 +-- mne/io/artemis123/artemis123.py | 6 +- mne/io/artemis123/utils.py | 10 +- mne/io/base.py | 7 +- mne/io/boxy/boxy.py | 6 +- mne/io/brainvision/brainvision.py | 18 ++- mne/io/brainvision/tests/test_brainvision.py | 8 +- mne/io/bti/bti.py | 6 +- mne/io/bti/tests/test_bti.py | 8 +- mne/io/cnt/cnt.py | 2 +- mne/io/ctf/ctf.py | 2 +- mne/io/ctf/eeg.py | 2 +- mne/io/ctf/info.py | 4 +- mne/io/curry/curry.py | 6 +- mne/io/curry/tests/test_curry.py | 2 +- mne/io/edf/edf.py | 16 +-- mne/io/edf/tests/test_gdf.py | 2 +- mne/io/eeglab/eeglab.py | 6 +- mne/io/eeglab/tests/test_eeglab.py | 2 +- mne/io/egi/egi.py | 2 +- mne/io/egi/egimff.py | 2 +- mne/io/egi/general.py | 2 +- mne/io/egi/tests/test_egi.py | 4 +- mne/io/eximia/eximia.py | 6 +- mne/io/eyelink/eyelink.py | 4 +- mne/io/eyelink/tests/test_eyelink.py | 2 +- mne/io/fiff/raw.py | 2 +- mne/io/fiff/tests/test_raw_fiff.py | 6 +- mne/io/fil/fil.py | 6 +- mne/io/hitachi/hitachi.py | 2 +- mne/io/kit/coreg.py | 9 +- mne/io/kit/kit.py | 4 +- mne/io/neuralynx/neuralynx.py | 4 +- mne/io/neuralynx/tests/test_neuralynx.py | 3 +- mne/io/nicolet/nicolet.py | 4 +- mne/io/nihon/nihon.py | 14 +- mne/io/nirx/nirx.py | 4 +- mne/io/nsx/nsx.py | 10 +- mne/io/persyst/persyst.py | 4 +- mne/io/persyst/tests/test_persyst.py | 12 +- mne/io/snirf/_snirf.py | 8 +- mne/io/snirf/tests/test_snirf.py | 10 +- mne/io/tests/test_raw.py | 2 +- mne/label.py | 6 +- mne/minimum_norm/inverse.py | 2 +- mne/misc.py | 6 +- mne/morph.py | 14 +- mne/preprocessing/_fine_cal.py | 2 +- .../eyetracking/_pupillometry.py | 4 +- mne/preprocessing/eyetracking/calibration.py | 2 +- mne/preprocessing/ica.py | 22 +-- .../nirs/tests/test_optical_density.py | 2 +- mne/preprocessing/tests/test_maxwell.py | 2 +- mne/preprocessing/tests/test_xdawn.py | 2 +- mne/preprocessing/xdawn.py | 4 +- mne/report/report.py | 8 +- mne/source_estimate.py | 4 +- mne/source_space/_source_space.py | 6 +- mne/stats/cluster_level.py | 4 +- mne/stats/parametric.py | 9 +- mne/stats/regression.py | 10 +- mne/surface.py | 6 +- mne/tests/test_annotations.py | 6 +- mne/tests/test_docstring_parameters.py | 2 +- mne/tests/test_epochs.py | 2 +- mne/tests/test_rank.py | 2 +- mne/time_frequency/_stockwell.py | 4 +- mne/time_frequency/csd.py | 4 +- mne/time_frequency/tests/test_stockwell.py | 2 +- mne/time_frequency/tfr.py | 10 +- mne/transforms.py | 12 +- mne/utils/check.py | 12 +- mne/utils/config.py | 2 +- mne/utils/docs.py | 18 +-- mne/utils/misc.py | 29 +--- mne/utils/progressbar.py | 5 +- mne/utils/tests/test_logging.py | 12 +- mne/viz/_3d.py | 4 +- mne/viz/_brain/_brain.py | 14 +- mne/viz/_brain/surface.py | 2 +- mne/viz/_brain/tests/test_brain.py | 2 +- mne/viz/backends/_pyvista.py | 5 +- mne/viz/backends/_utils.py | 6 +- mne/viz/epochs.py | 21 ++- mne/viz/evoked.py | 49 +++---- mne/viz/montage.py | 4 +- mne/viz/tests/test_3d.py | 8 +- mne/viz/tests/test_evoked.py | 2 +- mne/viz/topomap.py | 10 +- mne/viz/ui_events.py | 4 +- mne/viz/utils.py | 7 +- pyproject.toml | 3 +- tutorials/clinical/60_sleep.py | 2 +- tutorials/epochs/10_epochs_overview.py | 6 +- tutorials/epochs/30_epochs_metadata.py | 2 +- tutorials/evoked/10_evoked_overview.py | 6 +- tutorials/forward/20_source_alignment.py | 4 +- tutorials/inverse/10_stc_class.py | 5 +- tutorials/inverse/20_dipole_fit.py | 4 +- .../inverse/80_brainstorm_phantom_elekta.py | 6 +- tutorials/io/60_ctf_bst_auditory.py | 6 +- tutorials/machine-learning/30_strf.py | 4 +- tutorials/machine-learning/50_decoding.py | 4 +- .../preprocessing/25_background_filtering.py | 2 +- .../preprocessing/30_filtering_resampling.py | 8 +- .../40_artifact_correction_ica.py | 2 +- .../preprocessing/45_projectors_background.py | 4 +- .../50_artifact_correction_ssp.py | 6 +- .../preprocessing/55_setting_eeg_reference.py | 6 +- .../preprocessing/70_fnirs_processing.py | 4 +- tutorials/raw/10_raw_overview.py | 2 +- tutorials/raw/30_annotate_raw.py | 2 +- .../75_cluster_ftest_spatiotemporal.py | 4 +- tutorials/time-freq/50_ssvep.py | 32 ++--- .../visualization/10_publication_figure.py | 2 +- 189 files changed, 915 insertions(+), 1147 deletions(-) create mode 100644 doc/changes/devel/12358.other.rst diff --git a/doc/changes/devel/12358.other.rst b/doc/changes/devel/12358.other.rst new file mode 100644 index 00000000000..788db1d1a41 --- /dev/null +++ b/doc/changes/devel/12358.other.rst @@ -0,0 +1 @@ +Refresh code base to use Python 3.9 syntax using Ruff UP rules (pyupgrade), by `Clemens Brunner`_. \ No newline at end of file diff --git a/examples/datasets/limo_data.py b/examples/datasets/limo_data.py index 4a0f96ed8ff..54a2f34a530 100644 --- a/examples/datasets/limo_data.py +++ b/examples/datasets/limo_data.py @@ -190,7 +190,7 @@ # get levels of phase coherence levels = sorted(phase_coh.unique()) # create labels for levels of phase coherence (i.e., 0 - 85%) -labels = ["{0:.2f}".format(i) for i in np.arange(0.0, 0.90, 0.05)] +labels = [f"{i:.2f}" for i in np.arange(0.0, 0.90, 0.05)] # create dict of evokeds for each level of phase-coherence evokeds = { diff --git a/examples/datasets/opm_data.py b/examples/datasets/opm_data.py index 3f1903b3010..fcc60d80934 100644 --- a/examples/datasets/opm_data.py +++ b/examples/datasets/opm_data.py @@ -114,8 +114,8 @@ ) idx = np.argmax(dip_opm.gof) print( - "Best dipole at t=%0.1f ms with %0.1f%% GOF" - % (1000 * dip_opm.times[idx], dip_opm.gof[idx]) + f"Best dipole at t={1000 * dip_opm.times[idx]:0.1f} ms with " + f"{dip_opm.gof[idx]:0.1f}% GOF" ) # Plot N20m dipole as an example diff --git a/examples/decoding/decoding_csp_eeg.py b/examples/decoding/decoding_csp_eeg.py index cf588ebf18a..893e7969c7a 100644 --- a/examples/decoding/decoding_csp_eeg.py +++ b/examples/decoding/decoding_csp_eeg.py @@ -92,9 +92,7 @@ # Printing the results class_balance = np.mean(labels == labels[0]) class_balance = max(class_balance, 1.0 - class_balance) -print( - "Classification accuracy: %f / Chance level: %f" % (np.mean(scores), class_balance) -) +print(f"Classification accuracy: {np.mean(scores)} / Chance level: {class_balance}") # plot CSP patterns estimated on full data for visualization csp.fit_transform(epochs_data, labels) diff --git a/examples/decoding/receptive_field_mtrf.py b/examples/decoding/receptive_field_mtrf.py index 8dc04630753..6d20b9ac582 100644 --- a/examples/decoding/receptive_field_mtrf.py +++ b/examples/decoding/receptive_field_mtrf.py @@ -102,7 +102,7 @@ coefs = np.zeros((n_splits, n_channels, n_delays)) scores = np.zeros((n_splits, n_channels)) for ii, (train, test) in enumerate(cv.split(speech)): - print("split %s / %s" % (ii + 1, n_splits)) + print(f"split {ii + 1} / {n_splits}") rf.fit(speech[train], Y[train]) scores[ii] = rf.score(speech[test], Y[test]) # coef_ is shape (n_outputs, n_features, n_delays). we only have 1 feature @@ -212,7 +212,7 @@ patterns = coefs.copy() scores = np.zeros((n_splits,)) for ii, (train, test) in enumerate(cv.split(speech)): - print("split %s / %s" % (ii + 1, n_splits)) + print(f"split {ii + 1} / {n_splits}") sr.fit(Y[train], speech[train]) scores[ii] = sr.score(Y[test], speech[test])[0] # coef_ is shape (n_outputs, n_features, n_delays). We have 128 features @@ -272,9 +272,7 @@ show=False, vlim=(-max_coef, max_coef), ) -ax[0].set( - title="Model coefficients\nbetween delays %s and %s" % (time_plot[0], time_plot[1]) -) +ax[0].set(title=f"Model coefficients\nbetween delays {time_plot[0]} and {time_plot[1]}") mne.viz.plot_topomap( np.mean(mean_patterns[:, ix_plot], axis=1), @@ -284,8 +282,10 @@ vlim=(-max_patterns, max_patterns), ) ax[1].set( - title="Inverse-transformed coefficients\nbetween delays %s and %s" - % (time_plot[0], time_plot[1]) + title=( + f"Inverse-transformed coefficients\nbetween delays {time_plot[0]} and " + f"{time_plot[1]}" + ) ) # %% diff --git a/examples/inverse/compute_mne_inverse_raw_in_label.py b/examples/inverse/compute_mne_inverse_raw_in_label.py index d2d7b8be3d2..ac97df8ff4b 100644 --- a/examples/inverse/compute_mne_inverse_raw_in_label.py +++ b/examples/inverse/compute_mne_inverse_raw_in_label.py @@ -49,7 +49,7 @@ ) # Save result in stc files -stc.save("mne_%s_raw_inverse_%s" % (method, label_name), overwrite=True) +stc.save(f"mne_{method}_raw_inverse_{label_name}", overwrite=True) # %% # View activation time-series diff --git a/examples/inverse/compute_mne_inverse_volume.py b/examples/inverse/compute_mne_inverse_volume.py index 8283dfdeeca..39b455f464b 100644 --- a/examples/inverse/compute_mne_inverse_volume.py +++ b/examples/inverse/compute_mne_inverse_volume.py @@ -56,5 +56,5 @@ index_img(img, 61), str(t1_fname), threshold=8.0, - title="%s (t=%.1f s.)" % (method, stc.times[61]), + title=f"{method} (t={stc.times[61]:.1f} s.)", ) diff --git a/examples/inverse/evoked_ers_source_power.py b/examples/inverse/evoked_ers_source_power.py index 7ae7fa86424..f118a217c9e 100644 --- a/examples/inverse/evoked_ers_source_power.py +++ b/examples/inverse/evoked_ers_source_power.py @@ -34,12 +34,7 @@ data_path = somato.data_path() subject = "01" task = "somato" -raw_fname = ( - data_path - / "sub-{}".format(subject) - / "meg" - / "sub-{}_task-{}_meg.fif".format(subject, task) -) +raw_fname = data_path / f"sub-{subject}" / "meg" / f"sub-{subject}_task-{task}_meg.fif" # crop to 5 minutes to save memory raw = mne.io.read_raw_fif(raw_fname).crop(0, 300) @@ -59,10 +54,7 @@ # Read forward operator and point to freesurfer subject directory fname_fwd = ( - data_path - / "derivatives" - / "sub-{}".format(subject) - / "sub-{}_task-{}-fwd.fif".format(subject, task) + data_path / "derivatives" / f"sub-{subject}" / f"sub-{subject}_task-{task}-fwd.fif" ) subjects_dir = data_path / "derivatives" / "freesurfer" / "subjects" diff --git a/examples/inverse/label_source_activations.py b/examples/inverse/label_source_activations.py index 4a92ea27962..7640a468ebd 100644 --- a/examples/inverse/label_source_activations.py +++ b/examples/inverse/label_source_activations.py @@ -113,7 +113,7 @@ ax.set( xlabel="Time (ms)", ylabel="Source amplitude", - title="Mean vector activations in Label %r" % (label.name,), + title=f"Mean vector activations in Label {label.name!r}", xlim=xlim, ylim=ylim, ) diff --git a/examples/inverse/mixed_norm_inverse.py b/examples/inverse/mixed_norm_inverse.py index 038bbad0d8b..bc6b91bfeae 100644 --- a/examples/inverse/mixed_norm_inverse.py +++ b/examples/inverse/mixed_norm_inverse.py @@ -137,7 +137,7 @@ forward["src"], stc, bgcolor=(1, 1, 1), - fig_name="%s (cond %s)" % (solver, condition), + fig_name=f"{solver} (cond {condition})", opacity=0.1, ) @@ -159,7 +159,7 @@ src_fsaverage, stc_fsaverage, bgcolor=(1, 1, 1), - fig_name="Morphed %s (cond %s)" % (solver, condition), + fig_name=f"Morphed {solver} (cond {condition})", opacity=0.1, ) diff --git a/examples/inverse/read_stc.py b/examples/inverse/read_stc.py index 9b2823bd7a7..b06f61d14f8 100644 --- a/examples/inverse/read_stc.py +++ b/examples/inverse/read_stc.py @@ -29,9 +29,7 @@ stc = mne.read_source_estimate(fname) n_vertices, n_samples = stc.data.shape -print( - "stc data size: %s (nb of vertices) x %s (nb of samples)" % (n_vertices, n_samples) -) +print(f"stc data size: {n_vertices} (nb of vertices) x {n_samples} (nb of samples)") # View source activations plt.plot(stc.times, stc.data[::100, :].T) diff --git a/examples/preprocessing/define_target_events.py b/examples/preprocessing/define_target_events.py index 5672b8d69ad..5aa1becbb6b 100644 --- a/examples/preprocessing/define_target_events.py +++ b/examples/preprocessing/define_target_events.py @@ -100,7 +100,7 @@ # average epochs and get an Evoked dataset. -early, late = [epochs[k].average() for k in event_id] +early, late = (epochs[k].average() for k in event_id) # %% # View evoked response diff --git a/examples/preprocessing/eeg_bridging.py b/examples/preprocessing/eeg_bridging.py index 6c7052cb028..fbab43cfc5f 100644 --- a/examples/preprocessing/eeg_bridging.py +++ b/examples/preprocessing/eeg_bridging.py @@ -384,16 +384,7 @@ raw = raw_data[1] # typically impedances < 25 kOhm are acceptable for active systems and # impedances < 5 kOhm are desirable for a passive system -impedances = ( - rng.random( - ( - len( - raw.ch_names, - ) - ) - ) - * 30 -) +impedances = rng.random(len(raw.ch_names)) * 30 impedances[10] = 80 # set a few bad impendances impedances[25] = 99 cmap = LinearSegmentedColormap.from_list( diff --git a/examples/preprocessing/ica_comparison.py b/examples/preprocessing/ica_comparison.py index 02930174435..d4246b80362 100644 --- a/examples/preprocessing/ica_comparison.py +++ b/examples/preprocessing/ica_comparison.py @@ -55,7 +55,7 @@ def run_ica(method, fit_params=None): t0 = time() ica.fit(raw, reject=reject) fit_time = time() - t0 - title = "ICA decomposition using %s (took %.1fs)" % (method, fit_time) + title = f"ICA decomposition using {method} (took {fit_time:.1f}s)" ica.plot_components(title=title) diff --git a/examples/preprocessing/otp.py b/examples/preprocessing/otp.py index aa235e79a78..df3a6c74ffe 100644 --- a/examples/preprocessing/otp.py +++ b/examples/preprocessing/otp.py @@ -79,15 +79,9 @@ def compute_bias(raw): bias = compute_bias(raw) -print("Raw bias: %0.1fmm (worst: %0.1fmm)" % (np.mean(bias), np.max(bias))) +print(f"Raw bias: {np.mean(bias):0.1f}mm (worst: {np.max(bias):0.1f}mm)") bias_clean = compute_bias(raw_clean) -print( - "OTP bias: %0.1fmm (worst: %0.1fmm)" - % ( - np.mean(bias_clean), - np.max(bias_clean), - ) -) +print(f"OTP bias: {np.mean(bias_clean):0.1f}mm (worst: {np.max(bias_clean):0.1f}m)") # %% # References diff --git a/examples/simulation/simulate_raw_data.py b/examples/simulation/simulate_raw_data.py index ef375bfec38..e413a8deb75 100644 --- a/examples/simulation/simulate_raw_data.py +++ b/examples/simulation/simulate_raw_data.py @@ -55,9 +55,9 @@ def data_fun(times): global n n_samp = len(times) window = np.zeros(n_samp) - start, stop = [ + start, stop = ( int(ii * float(n_samp) / (2 * n_dipoles)) for ii in (2 * n, 2 * n + 1) - ] + ) window[start:stop] = 1.0 n += 1 data = 25e-9 * np.sin(2.0 * np.pi * 10.0 * n * times) diff --git a/examples/time_frequency/time_frequency_simulated.py b/examples/time_frequency/time_frequency_simulated.py index 9dfe38eab8b..85cc9a1f436 100644 --- a/examples/time_frequency/time_frequency_simulated.py +++ b/examples/time_frequency/time_frequency_simulated.py @@ -150,7 +150,7 @@ power.plot( [0], baseline=(0.0, 0.1), mode="mean", axes=ax, show=False, colorbar=False ) - ax.set_title("Sim: Using S transform, width = {:0.1f}".format(width)) + ax.set_title(f"Sim: Using S transform, width = {width:0.1f}") # %% # Morlet Wavelets diff --git a/examples/visualization/evoked_topomap.py b/examples/visualization/evoked_topomap.py index f75869383a9..c01cdd80d71 100644 --- a/examples/visualization/evoked_topomap.py +++ b/examples/visualization/evoked_topomap.py @@ -111,7 +111,7 @@ colorbar=False, sphere=(0.0, 0.0, 0.0, 0.09), ) - ax.set_title("%s %s" % (ch_type.upper(), extr), fontsize=14) + ax.set_title(f"{ch_type.upper()} {extr}", fontsize=14) # %% # More advanced usage diff --git a/examples/visualization/evoked_whitening.py b/examples/visualization/evoked_whitening.py index e213408276a..9a474d9ea36 100644 --- a/examples/visualization/evoked_whitening.py +++ b/examples/visualization/evoked_whitening.py @@ -84,7 +84,7 @@ print("Covariance estimates sorted from best to worst") for c in noise_covs: - print("%s : %s" % (c["method"], c["loglik"])) + print(f'{c["method"]} : {c["loglik"]}') # %% # Show the evoked data: diff --git a/mne/_fiff/_digitization.py b/mne/_fiff/_digitization.py index dab0427ac6a..dcbf9e8d24d 100644 --- a/mne/_fiff/_digitization.py +++ b/mne/_fiff/_digitization.py @@ -132,14 +132,15 @@ def __repr__(self): # noqa: D105 id_ = _cardinal_kind_rev.get(self["ident"], "Unknown cardinal") else: id_ = _dig_kind_proper[_dig_kind_rev.get(self["kind"], "unknown")] - id_ = "%s #%s" % (id_, self["ident"]) + id_ = f"{id_} #{self['ident']}" id_ = id_.rjust(10) cf = _coord_frame_name(self["coord_frame"]) + x, y, z = self["r"] if "voxel" in cf: - pos = ("(%0.1f, %0.1f, %0.1f)" % tuple(self["r"])).ljust(25) + pos = (f"({x:0.1f}, {y:0.1f}, {z:0.1f})").ljust(25) else: - pos = ("(%0.1f, %0.1f, %0.1f) mm" % tuple(1000 * self["r"])).ljust(25) - return "" % (id_, pos, cf) + pos = (f"({x * 1e3:0.1f}, {y * 1e3:0.1f}, {z * 1e3:0.1f}) mm").ljust(25) + return f"" # speed up info copy by only deep copying the mutable item def __deepcopy__(self, memodict): @@ -362,8 +363,8 @@ def _coord_frame_const(coord_frame): if not isinstance(coord_frame, str) or coord_frame not in _str_to_frame: raise ValueError( - "coord_frame must be one of %s, got %s" - % (sorted(_str_to_frame.keys()), coord_frame) + f"coord_frame must be one of {sorted(_str_to_frame.keys())}, got " + f"{coord_frame}" ) return _str_to_frame[coord_frame] @@ -414,9 +415,7 @@ def _make_dig_points( if lpa is not None: lpa = np.asarray(lpa) if lpa.shape != (3,): - raise ValueError( - "LPA should have the shape (3,) instead of %s" % (lpa.shape,) - ) + raise ValueError(f"LPA should have the shape (3,) instead of {lpa.shape}") dig.append( { "r": lpa, @@ -429,7 +428,7 @@ def _make_dig_points( nasion = np.asarray(nasion) if nasion.shape != (3,): raise ValueError( - "Nasion should have the shape (3,) instead of %s" % (nasion.shape,) + f"Nasion should have the shape (3,) instead of {nasion.shape}" ) dig.append( { @@ -442,9 +441,7 @@ def _make_dig_points( if rpa is not None: rpa = np.asarray(rpa) if rpa.shape != (3,): - raise ValueError( - "RPA should have the shape (3,) instead of %s" % (rpa.shape,) - ) + raise ValueError(f"RPA should have the shape (3,) instead of {rpa.shape}") dig.append( { "r": rpa, @@ -457,8 +454,7 @@ def _make_dig_points( hpi = np.asarray(hpi) if hpi.ndim != 2 or hpi.shape[1] != 3: raise ValueError( - "HPI should have the shape (n_points, 3) instead " - "of %s" % (hpi.shape,) + f"HPI should have the shape (n_points, 3) instead of {hpi.shape}" ) for idx, point in enumerate(hpi): dig.append( @@ -473,8 +469,8 @@ def _make_dig_points( extra_points = np.asarray(extra_points) if len(extra_points) and extra_points.shape[1] != 3: raise ValueError( - "Points should have the shape (n_points, 3) " - "instead of %s" % (extra_points.shape,) + "Points should have the shape (n_points, 3) instead of " + f"{extra_points.shape}" ) for idx, point in enumerate(extra_points): dig.append( diff --git a/mne/_fiff/meas_info.py b/mne/_fiff/meas_info.py index 483ddc34b52..462a34cb6d6 100644 --- a/mne/_fiff/meas_info.py +++ b/mne/_fiff/meas_info.py @@ -454,8 +454,8 @@ def _check_set(ch, projs, ch_type): for proj in projs: if ch["ch_name"] in proj["data"]["col_names"]: raise RuntimeError( - "Cannot change channel type for channel %s " - 'in projector "%s"' % (ch["ch_name"], proj["desc"]) + f'Cannot change channel type for channel {ch["ch_name"]} in ' + f'projector "{proj["desc"]}"' ) ch["kind"] = new_kind @@ -482,7 +482,7 @@ def _get_channel_positions(self, picks=None): n_zero = np.sum(np.sum(np.abs(pos), axis=1) == 0) if n_zero > 1: # XXX some systems have origin (0, 0, 0) raise ValueError( - "Could not extract channel positions for " "{} channels".format(n_zero) + f"Could not extract channel positions for {n_zero} channels" ) return pos @@ -507,8 +507,8 @@ def _set_channel_positions(self, pos, names): ) pos = np.asarray(pos, dtype=np.float64) if pos.shape[-1] != 3 or pos.ndim != 2: - msg = "Channel positions must have the shape (n_points, 3) " "not %s." % ( - pos.shape, + msg = ( + f"Channel positions must have the shape (n_points, 3) not {pos.shape}." ) raise ValueError(msg) for name, p in zip(names, pos): @@ -568,9 +568,9 @@ def set_channel_types(self, mapping, *, on_unit_change="warn", verbose=None): c_ind = ch_names.index(ch_name) if ch_type not in _human2fiff: raise ValueError( - "This function cannot change to this " - "channel type: %s. Accepted channel types " - "are %s." % (ch_type, ", ".join(sorted(_human2unit.keys()))) + f"This function cannot change to this channel type: {ch_type}. " + "Accepted channel types are " + f"{', '.join(sorted(_human2unit.keys()))}." ) # Set sensor type _check_set(info["chs"][c_ind], info["projs"], ch_type) @@ -578,8 +578,8 @@ def set_channel_types(self, mapping, *, on_unit_change="warn", verbose=None): unit_new = _human2unit[ch_type] if unit_old not in _unit2human: raise ValueError( - "Channel '%s' has unknown unit (%s). Please " - "fix the measurement info of your data." % (ch_name, unit_old) + f"Channel '{ch_name}' has unknown unit ({unit_old}). Please fix the" + " measurement info of your data." ) if unit_old != _human2unit[ch_type]: this_change = (_unit2human[unit_old], _unit2human[unit_new]) @@ -1659,7 +1659,7 @@ def __repr__(self): non_empty -= 1 # don't count as non-empty elif k == "bads": if v: - entr = "{} items (".format(len(v)) + entr = f"{len(v)} items (" entr += ", ".join(v) entr = shorten(entr, MAX_WIDTH, placeholder=" ...") + ")" else: @@ -1695,11 +1695,11 @@ def __repr__(self): if not np.allclose(v["trans"], np.eye(v["trans"].shape[0])): frame1 = _coord_frame_name(v["from"]) frame2 = _coord_frame_name(v["to"]) - entr = "%s -> %s transform" % (frame1, frame2) + entr = f"{frame1} -> {frame2} transform" else: entr = "" elif k in ["sfreq", "lowpass", "highpass"]: - entr = "{:.1f} Hz".format(v) + entr = f"{v:.1f} Hz" elif isinstance(v, str): entr = shorten(v, MAX_WIDTH, placeholder=" ...") elif k == "chs": @@ -1719,7 +1719,7 @@ def __repr__(self): try: this_len = len(v) except TypeError: - entr = "{}".format(v) if v is not None else "" + entr = f"{v}" if v is not None else "" else: if this_len > 0: entr = "%d item%s (%s)" % ( @@ -1731,7 +1731,7 @@ def __repr__(self): entr = "" if entr != "": non_empty += 1 - strs.append("%s: %s" % (k, entr)) + strs.append(f"{k}: {entr}") st = "\n ".join(sorted(strs)) st += "\n>" st %= non_empty @@ -1784,12 +1784,8 @@ def _check_consistency(self, prepend_error=""): or self["meas_date"].tzinfo is not datetime.timezone.utc ): raise RuntimeError( - '%sinfo["meas_date"] must be a datetime ' - "object in UTC or None, got %r" - % ( - prepend_error, - repr(self["meas_date"]), - ) + f'{prepend_error}info["meas_date"] must be a datetime object in UTC' + f' or None, got {repr(self["meas_date"])!r}' ) chs = [ch["ch_name"] for ch in self["chs"]] @@ -1799,8 +1795,8 @@ def _check_consistency(self, prepend_error=""): or self["nchan"] != len(chs) ): raise RuntimeError( - "%sinfo channel name inconsistency detected, " - "please notify mne-python developers" % (prepend_error,) + f"{prepend_error}info channel name inconsistency detected, please " + "notify MNE-Python developers" ) # make sure we have the proper datatypes @@ -2649,16 +2645,9 @@ def _check_dates(info, prepend_error=""): or value[key_2] > np.iinfo(">i4").max ): raise RuntimeError( - "%sinfo[%s][%s] must be between " - '"%r" and "%r", got "%r"' - % ( - prepend_error, - key, - key_2, - np.iinfo(">i4").min, - np.iinfo(">i4").max, - value[key_2], - ), + f"{prepend_error}info[{key}][{key_2}] must be between " + f'"{np.iinfo(">i4").min!r}" and "{np.iinfo(">i4").max!r}", got ' + f'"{value[key_2]!r}"' ) meas_date = info.get("meas_date") @@ -2671,14 +2660,9 @@ def _check_dates(info, prepend_error=""): or meas_date_stamp[0] > np.iinfo(">i4").max ): raise RuntimeError( - '%sinfo["meas_date"] seconds must be between "%r" ' - 'and "%r", got "%r"' - % ( - prepend_error, - (np.iinfo(">i4").min, 0), - (np.iinfo(">i4").max, 0), - meas_date_stamp[0], - ) + f'{prepend_error}info["meas_date"] seconds must be between ' + f'"{(np.iinfo(">i4").min, 0)!r}" and "{(np.iinfo(">i4").max, 0)!r}", got ' + f'"{meas_date_stamp[0]!r}"' ) @@ -2954,8 +2938,8 @@ def _merge_info_values(infos, key, verbose=None): """ values = [d[key] for d in infos] msg = ( - "Don't know how to merge '%s'. Make sure values are " - "compatible, got types:\n %s" % (key, [type(v) for v in values]) + f"Don't know how to merge '{key}'. Make sure values are compatible, got types:" + f"\n {[type(v) for v in values]}" ) def _flatten(lists): @@ -3218,8 +3202,8 @@ def create_info(ch_names, sfreq, ch_types="misc", verbose=None): ch_types = np.atleast_1d(np.array(ch_types, np.str_)) if ch_types.ndim != 1 or len(ch_types) != nchan: raise ValueError( - "ch_types and ch_names must be the same length " - "(%s != %s) for ch_types=%s" % (len(ch_types), nchan, ch_types) + f"ch_types and ch_names must be the same length ({len(ch_types)} != " + f"{nchan}) for ch_types={ch_types}" ) info = _empty_info(sfreq) ch_types_dict = get_channel_type_constants(include_defaults=True) diff --git a/mne/_fiff/open.py b/mne/_fiff/open.py index 1b3117dc63e..65a7bec33a8 100644 --- a/mne/_fiff/open.py +++ b/mne/_fiff/open.py @@ -347,9 +347,9 @@ def _show_tree( elif isinstance(tag.data, (list, tuple)): postpend += " ... list len=" + str(len(tag.data)) elif issparse(tag.data): - postpend += " ... sparse (%s) shape=%s" % ( - tag.data.getformat(), - tag.data.shape, + postpend += ( + f" ... sparse ({tag.data.getformat()}) shape=" + f"{tag.data.shape}" ) else: postpend += " ... type=" + str(type(tag.data)) @@ -357,7 +357,7 @@ def _show_tree( matrix_info = _matrix_info(tag) if matrix_info is not None: _, type_, _, _ = matrix_info - type_ = _call_dict_names.get(type_, "?%s?" % (type_,)) + type_ = _call_dict_names.get(type_, f"?{type_}?") this_type = "/".join(this_type) out += [ f"{next_idt}{prepend}{str(k).ljust(4)} = " diff --git a/mne/_fiff/pick.py b/mne/_fiff/pick.py index 4c5854f36fe..86790e6e3e8 100644 --- a/mne/_fiff/pick.py +++ b/mne/_fiff/pick.py @@ -250,7 +250,7 @@ def channel_type(info, idx): first_kind = _first_rule[ch["kind"]] except KeyError: raise ValueError( - 'Unknown channel type (%s) for channel "%s"' % (ch["kind"], ch["ch_name"]) + f'Unknown channel type ({ch["kind"]}) for channel "{ch["ch_name"]}"' ) if first_kind in _second_rules: key, second_rule = _second_rules[first_kind] @@ -322,8 +322,7 @@ def pick_channels(ch_names, include, exclude=[], ordered=None, *, verbose=None): ) elif ordered: raise ValueError( - "Missing channels from ch_names required by " - "include:\n%s" % (missing,) + f"Missing channels from ch_names required by include:\n{missing}" ) if not ordered: out_sel = np.unique(sel) @@ -436,7 +435,7 @@ def _check_meg_type(meg, allow_auto=False): allowed_types += ["auto"] if allow_auto else [] if meg not in allowed_types: raise ValueError( - "meg value must be one of %s or bool, not %s" % (allowed_types, meg) + f"meg value must be one of {allowed_types} or bool, not {meg}" ) @@ -983,8 +982,7 @@ def _contains_ch_type(info, ch_type): _check_option("ch_type", ch_type, valid_channel_types) if info is None: raise ValueError( - 'Cannot check for channels of type "%s" because info ' - "is None" % (ch_type,) + f'Cannot check for channels of type "{ch_type}" because info is None' ) return any(ch_type == channel_type(info, ii) for ii in range(info["nchan"])) @@ -1078,8 +1076,8 @@ def _check_excludes_includes(chs, info=None, allow_bads=False): chs = info["bads"] else: raise ValueError( - 'include/exclude must be list, tuple, ndarray, or "bads". ' - + "You provided type {}".format(type(chs)) + 'include/exclude must be list, tuple, ndarray, or "bads". You provided ' + f"type {type(chs)}." ) return chs @@ -1252,7 +1250,7 @@ def _picks_to_idx( extra_repr = ", treated as range(%d)" % (n_chan,) else: picks = none # let _picks_str_to_idx handle it - extra_repr = 'None, treated as "%s"' % (none,) + extra_repr = f'None, treated as "{none}"' # # slice @@ -1266,7 +1264,7 @@ def _picks_to_idx( picks = np.atleast_1d(picks) # this works even for picks == 'something' picks = np.array([], dtype=int) if len(picks) == 0 else picks if picks.ndim != 1: - raise ValueError("picks must be 1D, got %sD" % (picks.ndim,)) + raise ValueError(f"picks must be 1D, got {picks.ndim}D") if picks.dtype.char in ("S", "U"): picks = _picks_str_to_idx( info, @@ -1296,8 +1294,7 @@ def _picks_to_idx( # if len(picks) == 0 and not allow_empty: raise ValueError( - "No appropriate %s found for the given picks " - "(%r)" % (picks_on, orig_picks) + 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)) @@ -1341,8 +1338,8 @@ def _picks_str_to_idx( picks_generic = _pick_data_or_ica(info, exclude=exclude) if len(picks_generic) == 0 and orig_picks is None and not allow_empty: raise ValueError( - "picks (%s) yielded no channels, consider " - "passing picks explicitly" % (repr(orig_picks) + extra_repr,) + f"picks ({repr(orig_picks) + extra_repr}) yielded no channels, " + "consider passing picks explicitly" ) # @@ -1407,10 +1404,9 @@ def _picks_str_to_idx( if sum(any_found) == 0: if not allow_empty: raise ValueError( - "picks (%s) could not be interpreted as " - 'channel names (no channel "%s"), channel types (no ' - 'type "%s" present), or a generic type (just "all" or "data")' - % (repr(orig_picks) + extra_repr, str(bad_names), bad_type) + 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' "{bad_type}" present), or a generic type (just "all" or "data")' ) picks = np.array([], int) elif sum(any_found) > 1: diff --git a/mne/_fiff/proj.py b/mne/_fiff/proj.py index 26bba36bc13..0036257d00c 100644 --- a/mne/_fiff/proj.py +++ b/mne/_fiff/proj.py @@ -729,7 +729,7 @@ 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("projs must be a list or tuple, got %s" % (type(projs),)) + raise TypeError(f"projs must be a list or tuple, got {type(projs)}") for pi, p in enumerate(projs): if not isinstance(p, Projection): raise TypeError( diff --git a/mne/_fiff/reference.py b/mne/_fiff/reference.py index 6bd422637bc..5822e87e17b 100644 --- a/mne/_fiff/reference.py +++ b/mne/_fiff/reference.py @@ -67,7 +67,7 @@ def _check_before_reference(inst, ref_from, ref_to, ch_type): else: extra = "channels supplied" if len(ref_to) == 0: - raise ValueError("No %s to apply the reference to" % (extra,)) + raise ValueError(f"No {extra} to apply the reference to") # After referencing, existing SSPs might not be valid anymore. projs_to_remove = [] @@ -301,8 +301,8 @@ def _check_can_reref(inst): FIFF.FIFFV_MNE_CUSTOM_REF_OFF, ): raise RuntimeError( - "Cannot set new reference on data with custom " - "reference type %r" % (_ref_dict[current_custom],) + "Cannot set new reference on data with custom reference type " + f"{_ref_dict[current_custom]!r}" ) @@ -363,8 +363,8 @@ def set_eeg_reference( if projection: # average reference projector if ref_channels != "average": raise ValueError( - "Setting projection=True is only supported for " - 'ref_channels="average", got %r.' % (ref_channels,) + 'Setting projection=True is only supported for ref_channels="average", ' + f"got {ref_channels!r}." ) # We need verbose='error' here in case we add projs sequentially if _has_eeg_average_ref_proj(inst.info, ch_type=ch_type, verbose="error"): diff --git a/mne/_fiff/tests/test_constants.py b/mne/_fiff/tests/test_constants.py index 8f65e2609d5..45a9899423d 100644 --- a/mne/_fiff/tests/test_constants.py +++ b/mne/_fiff/tests/test_constants.py @@ -342,7 +342,7 @@ def test_constants(tmp_path): break else: if name not in _tag_ignore_names: - raise RuntimeError("Could not find %s" % (name,)) + raise RuntimeError(f"Could not find {name}") assert check in used_enums, name if "SSS" in check: raise RuntimeError @@ -353,13 +353,13 @@ def test_constants(tmp_path): else: unknowns.append((name, val)) if check is not None and name not in _tag_ignore_names: - assert val in fif[check], "%s: %s, %s" % (check, val, name) + assert val in fif[check], f"{check}: {val}, {name}" if val in con[check]: - msg = "%s='%s' ?" % (name, con[check][val]) + msg = f"{name}='{con[check][val]}' ?" assert _aliases.get(name) == con[check][val], msg else: con[check][val] = name - unknowns = "\n\t".join("%s (%s)" % u for u in unknowns) + unknowns = "\n\t".join("{} ({})".format(*u) for u in unknowns) assert len(unknowns) == 0, "Unknown types\n\t%s" % unknowns # Assert that all the FIF defs are in our constants diff --git a/mne/_fiff/utils.py b/mne/_fiff/utils.py index cdda8784e8a..09cc3046d6c 100644 --- a/mne/_fiff/utils.py +++ b/mne/_fiff/utils.py @@ -239,9 +239,8 @@ def _read_segments_file( block = np.fromfile(fid, dtype, count) if block.size != count: raise RuntimeError( - "Incorrect number of samples (%s != %s), " - "please report this error to MNE-Python " - "developers" % (block.size, count) + f"Incorrect number of samples ({block.size} != {count}), please " + "report this error to MNE-Python developers" ) block = block.reshape(n_channels, -1, order="F") n_samples = block.shape[1] # = count // n_channels @@ -340,7 +339,7 @@ def _construct_bids_filename(base, ext, part_idx, validate=True): ) suffix = deconstructed_base[-1] base = "_".join(deconstructed_base[:-1]) - use_fname = "{}_split-{:02}_{}{}".format(base, part_idx + 1, suffix, ext) + use_fname = f"{base}_split-{part_idx + 1:02}_{suffix}{ext}" if dirname: use_fname = op.join(dirname, use_fname) return use_fname diff --git a/mne/_fiff/what.py b/mne/_fiff/what.py index 9f0efa67453..5c248fe2c8f 100644 --- a/mne/_fiff/what.py +++ b/mne/_fiff/what.py @@ -65,7 +65,7 @@ def what(fname): try: func(fname, **kwargs) except Exception as exp: - logger.debug("Not %s: %s" % (what, exp)) + logger.debug(f"Not {what}: {exp}") else: return what return "unknown" diff --git a/mne/_freesurfer.py b/mne/_freesurfer.py index 6938e4f39fc..67a27d59860 100644 --- a/mne/_freesurfer.py +++ b/mne/_freesurfer.py @@ -554,7 +554,7 @@ def read_lta(fname, verbose=None): The affine transformation described by the lta file. """ _check_fname(fname, "read", must_exist=True) - with open(fname, "r") as fid: + with open(fname) as fid: lines = fid.readlines() # 0 is linear vox2vox, 1 is linear ras2ras trans_type = int(lines[0].split("=")[1].strip()[0]) @@ -715,7 +715,7 @@ def _get_lut(fname=None): ("A", "= 0).all(): raise ValueError( - "All control points must be positive (got %s)" - % (self.control_points[:3],) + f"All control points must be positive (got {self.control_points[:3]})" ) if isinstance(values, np.ndarray): values = [values] @@ -61,14 +60,13 @@ def __init__(self, control_points, values, interp="hann"): for v in values: if not (v is None or isinstance(v, np.ndarray)): raise TypeError( - 'All entries in "values" must be ndarray ' - "or None, got %s" % (type(v),) + 'All entries in "values" must be ndarray or None, got ' + f"{type(v)}" ) if v is not None and v.shape[0] != len(self.control_points): raise ValueError( - "Values, if provided, must be the same " - "length as the number of control points " - "(%s), got %s" % (len(self.control_points), v.shape[0]) + "Values, if provided, must be the same length as the number of " + f"control points ({len(self.control_points)}), got {v.shape[0]}" ) use_values = values @@ -84,9 +82,7 @@ def val(pt): self._left = self._right = self._use_interp = None known_types = ("cos2", "linear", "zero", "hann") if interp not in known_types: - raise ValueError( - 'interp must be one of %s, got "%s"' % (known_types, interp) - ) + raise ValueError(f'interp must be one of {known_types}, got "{interp}"') self._interp = interp def feed_generator(self, n_pts): @@ -95,10 +91,10 @@ def feed_generator(self, n_pts): n_pts = _ensure_int(n_pts, "n_pts") original_position = self._position stop = self._position + n_pts - logger.debug("Feed %s (%s-%s)" % (n_pts, self._position, stop)) + logger.debug(f"Feed {n_pts} ({self._position}-{stop})") used = np.zeros(n_pts, bool) if self._left is None: # first one - logger.debug(" Eval @ %s (%s)" % (0, self.control_points[0])) + logger.debug(f" Eval @ 0 ({self.control_points[0]})") self._left = self.values(self.control_points[0]) if len(self.control_points) == 1: self._right = self._left @@ -132,7 +128,7 @@ def feed_generator(self, n_pts): self._left_idx += 1 self._use_interp = None # need to recreate it eval_pt = self.control_points[self._left_idx + 1] - logger.debug(" Eval @ %s (%s)" % (self._left_idx + 1, eval_pt)) + logger.debug(f" Eval @ {self._left_idx + 1} ({eval_pt})") self._right = self.values(eval_pt) assert self._right is not None left_point = self.control_points[self._left_idx] @@ -153,8 +149,7 @@ def feed_generator(self, n_pts): n_use = min(stop, right_point) - self._position if n_use > 0: logger.debug( - " Interp %s %s (%s-%s)" - % (self._interp, n_use, left_point, right_point) + f" Interp {self._interp} {n_use} ({left_point}-{right_point})" ) interp_start = self._position - left_point assert interp_start >= 0 @@ -223,7 +218,7 @@ def _check_store(store): ): store = _Storer(*store) if not callable(store): - raise TypeError("store must be callable, got type %s" % (type(store),)) + raise TypeError(f"store must be callable, got type {type(store)}") return store @@ -288,11 +283,11 @@ def __init__( n_overlap = _ensure_int(n_overlap, "n_overlap") n_total = _ensure_int(n_total, "n_total") if n_samples <= 0: - raise ValueError("n_samples must be > 0, got %s" % (n_samples,)) + raise ValueError(f"n_samples must be > 0, got {n_samples}") if n_overlap < 0: - raise ValueError("n_overlap must be >= 0, got %s" % (n_overlap,)) + raise ValueError(f"n_overlap must be >= 0, got {n_overlap}") if n_total < 0: - raise ValueError("n_total must be >= 0, got %s" % (n_total,)) + raise ValueError(f"n_total must be >= 0, got {n_total}") self._n_samples = int(n_samples) self._n_overlap = int(n_overlap) del n_samples, n_overlap @@ -302,7 +297,7 @@ def __init__( "most the total number of samples (%s)" % (self._n_samples, n_total) ) if not callable(process): - raise TypeError("process must be callable, got type %s" % (type(process),)) + raise TypeError(f"process must be callable, got type {type(process)}") self._process = process self._step = self._n_samples - self._n_overlap self._store = _check_store(store) @@ -337,8 +332,7 @@ def __init__( del window, window_name if delta > 0: logger.info( - " The final %0.3f s will be lumped into the " - "final window" % (delta / sfreq,) + f" The final {delta / sfreq} s will be lumped into the final window" ) @property @@ -376,9 +370,8 @@ def feed(self, *datas, verbose=None, **kwargs): or self._in_buffers[di].dtype != data.dtype ): raise TypeError( - "data must dtype %s and shape[:-1]==%s, " - "got dtype %s shape[:-1]=%s" - % ( + "data must dtype {} and shape[:-1]=={}, got dtype {} shape[:-1]=" + "{}".format( self._in_buffers[di].dtype, self._in_buffers[di].shape[:-1], data.dtype, @@ -392,9 +385,8 @@ def feed(self, *datas, verbose=None, **kwargs): self._in_buffers[di] = np.concatenate([self._in_buffers[di], data], -1) if self._in_offset > self.stops[-1]: raise ValueError( - "data (shape %s) exceeded expected total " - "buffer size (%s > %s)" - % (data.shape, self._in_offset, self.stops[-1]) + f"data (shape {data.shape}) exceeded expected total buffer size (" + f"{self._in_offset} > {self.stops[-1]})" ) # Check to see if we can process the next chunk and dump outputs while self._idx < len(self.starts) and self._in_offset >= self.stops[self._idx]: @@ -411,7 +403,7 @@ def feed(self, *datas, verbose=None, **kwargs): if self._idx == 0: for offset in range(self._n_samples - self._step, 0, -self._step): this_window[:offset] += self._window[-offset:] - logger.debug(" * Processing %d->%d" % (start, stop)) + logger.debug(f" * Processing {start}->{stop}") this_proc = [in_[..., :this_len].copy() for in_ in self._in_buffers] if not all( proc.shape[-1] == this_len == this_window.size for proc in this_proc @@ -466,7 +458,7 @@ class _Storer: def __init__(self, *outs, picks=None): for oi, out in enumerate(outs): if not isinstance(out, np.ndarray) or out.ndim < 1: - raise TypeError("outs[oi] must be >= 1D ndarray, got %s" % (out,)) + raise TypeError(f"outs[oi] must be >= 1D ndarray, got {out}") self.outs = outs self.idx = 0 self.picks = picks diff --git a/mne/annotations.py b/mne/annotations.py index 783ee6e1901..cc4209bf898 100644 --- a/mne/annotations.py +++ b/mne/annotations.py @@ -65,15 +65,15 @@ def _check_o_d_s_c(onset, duration, description, ch_names): onset = np.atleast_1d(np.array(onset, dtype=float)) if onset.ndim != 1: raise ValueError( - "Onset must be a one dimensional array, got %s " - "(shape %s)." % (onset.ndim, onset.shape) + f"Onset must be a one dimensional array, got {onset.ndim} (shape " + f"{onset.shape})." ) duration = np.array(duration, dtype=float) if duration.ndim == 0 or duration.shape == (1,): duration = np.repeat(duration, len(onset)) if duration.ndim != 1: raise ValueError( - "Duration must be a one dimensional array, " "got %d." % (duration.ndim,) + f"Duration must be a one dimensional array, got {duration.ndim}." ) description = np.array(description, dtype=str) @@ -81,8 +81,7 @@ def _check_o_d_s_c(onset, duration, description, ch_names): description = np.repeat(description, len(onset)) if description.ndim != 1: raise ValueError( - "Description must be a one dimensional array, " - "got %d." % (description.ndim,) + f"Description must be a one dimensional array, got {description.ndim}." ) _safe_name_list(description, "write", "description") @@ -305,10 +304,10 @@ def __eq__(self, other): def __repr__(self): """Show the representation.""" counter = Counter(self.description) - kinds = ", ".join(["%s (%s)" % k for k in sorted(counter.items())]) + kinds = ", ".join(["{} ({})".format(*k) for k in sorted(counter.items())]) kinds = (": " if len(kinds) > 0 else "") + kinds ch_specific = ", channel-specific" if self._any_ch_names() else "" - s = "Annotations | %s segment%s%s%s" % ( + s = "Annotations | {} segment{}{}{}".format( len(self.onset), _pl(len(self.onset)), ch_specific, @@ -341,9 +340,8 @@ def __iadd__(self, other): self._orig_time = other.orig_time if self.orig_time != other.orig_time: raise ValueError( - "orig_time should be the same to " - "add/concatenate 2 annotations " - "(got %s != %s)" % (self.orig_time, other.orig_time) + "orig_time should be the same to add/concatenate 2 annotations (got " + f"{self.orig_time} != {other.orig_time})" ) return self.append( other.onset, other.duration, other.description, other.ch_names @@ -621,10 +619,10 @@ def crop( del tmin, tmax if absolute_tmin > absolute_tmax: raise ValueError( - "tmax should be greater than or equal to tmin " - "(%s < %s)." % (absolute_tmin, absolute_tmax) + f"tmax should be greater than or equal to tmin ({absolute_tmin} < " + f"{absolute_tmax})." ) - logger.debug("Cropping annotations %s - %s" % (absolute_tmin, absolute_tmax)) + logger.debug(f"Cropping annotations {absolute_tmin} - {absolute_tmax}") onsets, durations, descriptions, ch_names = [], [], [], [] out_of_bounds, clip_left_elem, clip_right_elem = [], [], [] @@ -1496,7 +1494,7 @@ def _check_event_id(event_id, raw): else: raise ValueError( "Invalid type for event_id (should be None, str, " - "dict or callable). Got {}".format(type(event_id)) + f"dict or callable). Got {type(event_id)}." ) @@ -1511,16 +1509,14 @@ def _check_event_description(event_desc, events): elif isinstance(event_desc, Iterable): event_desc = np.asarray(event_desc) if event_desc.ndim != 1: - raise ValueError( - "event_desc must be 1D, got shape {}".format(event_desc.shape) - ) + raise ValueError(f"event_desc must be 1D, got shape {event_desc.shape}") event_desc = dict(zip(event_desc, map(str, event_desc))) elif callable(event_desc): pass else: raise ValueError( "Invalid type for event_desc (should be None, list, " - "1darray, dict or callable). Got {}".format(type(event_desc)) + f"1darray, dict or callable). Got {type(event_desc)}." ) return event_desc @@ -1640,7 +1636,7 @@ def events_from_annotations( events = np.c_[inds, np.zeros(len(inds)), values].astype(int) - logger.info("Used Annotations descriptions: %s" % (list(event_id_.keys()),)) + logger.info(f"Used Annotations descriptions: {list(event_id_.keys())}") return events, event_id_ diff --git a/mne/baseline.py b/mne/baseline.py index 3994c5522e5..36ab0fc514f 100644 --- a/mne/baseline.py +++ b/mne/baseline.py @@ -77,7 +77,7 @@ def rescale(data, times, baseline, mode="mean", copy=True, picks=None, verbose=N imin = np.where(times >= bmin)[0] if len(imin) == 0: raise ValueError( - "bmin is too large (%s), it exceeds the largest " "time value" % (bmin,) + f"bmin is too large ({bmin}), it exceeds the largest time value" ) imin = int(imin[0]) if bmax is None: @@ -86,14 +86,13 @@ def rescale(data, times, baseline, mode="mean", copy=True, picks=None, verbose=N imax = np.where(times <= bmax)[0] if len(imax) == 0: raise ValueError( - "bmax is too small (%s), it is smaller than the " - "smallest time value" % (bmax,) + f"bmax is too small ({bmax}), it is smaller than the smallest time " + "value" ) imax = int(imax[-1]) + 1 if imin >= imax: raise ValueError( - "Bad rescaling slice (%s:%s) from time values %s, %s" - % (imin, imax, bmin, bmax) + f"Bad rescaling slice ({imin}:{imax}) from time values {bmin}, {bmax}" ) # technically this is inefficient when `picks` is given, but assuming @@ -188,8 +187,8 @@ def _check_baseline(baseline, times, sfreq, on_baseline_outside_data="raise"): # check default value of baseline and `tmin=0` if baseline == (None, 0) and tmin == 0: raise ValueError( - "Baseline interval is only one sample. Use " - "`baseline=(0, 0)` if this is desired." + "Baseline interval is only one sample. Use `baseline=(0, 0)` if this is " + "desired." ) baseline_tmin, baseline_tmax = baseline @@ -204,15 +203,14 @@ def _check_baseline(baseline, times, sfreq, on_baseline_outside_data="raise"): if baseline_tmin > baseline_tmax: raise ValueError( - "Baseline min (%s) must be less than baseline max (%s)" - % (baseline_tmin, baseline_tmax) + f"Baseline min ({baseline_tmin}) must be less than baseline max (" + f"{baseline_tmax})" ) if (baseline_tmin < tmin - tstep) or (baseline_tmax > tmax + tstep): msg = ( - f"Baseline interval [{baseline_tmin}, {baseline_tmax}] s " - f"is outside of epochs data [{tmin}, {tmax}] s. Epochs were " - f"probably cropped." + f"Baseline interval [{baseline_tmin}, {baseline_tmax}] s is outside of " + f"epochs data [{tmin}, {tmax}] s. Epochs were probably cropped." ) if on_baseline_outside_data == "raise": raise ValueError(msg) diff --git a/mne/beamformer/_compute_beamformer.py b/mne/beamformer/_compute_beamformer.py index 975f0852208..16cbc18e6d7 100644 --- a/mne/beamformer/_compute_beamformer.py +++ b/mne/beamformer/_compute_beamformer.py @@ -120,14 +120,13 @@ def _prepare_beamformer_input( nn[...] = [0, 0, 1] # align to local +Z coordinate if pick_ori is not None and not is_free_ori: raise ValueError( - "Normal or max-power orientation (got %r) can only be picked when " - "a forward operator with free orientation is used." % (pick_ori,) + f"Normal or max-power orientation (got {pick_ori!r}) can only be picked " + "when a forward operator with free orientation is used." ) if pick_ori == "normal" and not forward["surf_ori"]: raise ValueError( - "Normal orientation can only be picked when a " - "forward operator oriented in surface coordinates is " - "used." + "Normal orientation can only be picked when a forward operator oriented in " + "surface coordinates is used." ) _check_src_normal(pick_ori, forward["src"]) del forward, info @@ -505,21 +504,21 @@ def __repr__(self): # noqa: D105 if self["subject"] is None: subject = "unknown" else: - subject = '"%s"' % (self["subject"],) - out = "aso", projection, G) diff --git a/mne/bem.py b/mne/bem.py index b3f948fb123..dd9b5a1e24e 100644 --- a/mne/bem.py +++ b/mne/bem.py @@ -100,16 +100,16 @@ def __repr__(self): # noqa: D105 if rad is None: # no radius / MEG only extra = "Sphere (no layers): r0=[%s] mm" % center else: - extra = "Sphere (%s layer%s): r0=[%s] R=%1.f mm" % ( + extra = "Sphere ({} layer{}): r0=[{}] R={:1.0f} mm".format( len(self["layers"]) - 1, _pl(self["layers"]), center, rad * 1000.0, ) else: - extra = "BEM (%s layer%s)" % (len(self["surfs"]), _pl(self["surfs"])) - extra += " solver=%s" % self["solver"] - return "" % extra + extra = f"BEM ({len(self['surfs'])} layer{_pl(self['surfs'])})" + extra += f" solver={self['solver']}" + return f"" def copy(self): """Return copy of ConductorModel instance.""" @@ -542,8 +542,9 @@ def _assert_complete_surface(surf, incomplete="raise"): # Center of mass.... cm = surf["rr"].mean(axis=0) logger.info( - "%s CM is %6.2f %6.2f %6.2f mm" - % (_bem_surf_name[surf["id"]], 1000 * cm[0], 1000 * cm[1], 1000 * cm[2]) + "{} CM is {:6.2f} {:6.2f} {:6.2f} mm".format( + _bem_surf_name[surf["id"]], 1000 * cm[0], 1000 * cm[1], 1000 * cm[2] + ) ) tot_angle = _get_solids(surf["rr"][surf["tris"]], cm[np.newaxis, :])[0] prop = tot_angle / (2 * np.pi) @@ -897,18 +898,18 @@ def make_sphere_model( param = locals()[name] if isinstance(param, str): if param != "auto": - raise ValueError('%s, if str, must be "auto" not "%s"' % (name, param)) + raise ValueError(f'{name}, if str, must be "auto" not "{param}"') relative_radii = np.array(relative_radii, float).ravel() sigmas = np.array(sigmas, float).ravel() if len(relative_radii) != len(sigmas): raise ValueError( - "relative_radii length (%s) must match that of " - "sigmas (%s)" % (len(relative_radii), len(sigmas)) + f"relative_radii length ({len(relative_radii)}) must match that of sigmas (" + f"{len(sigmas)})" ) if len(sigmas) <= 1 and head_radius is not None: raise ValueError( - "at least 2 sigmas must be supplied if " - "head_radius is not None, got %s" % (len(sigmas),) + "at least 2 sigmas must be supplied if head_radius is not None, got " + f"{len(sigmas)}" ) if (isinstance(r0, str) and r0 == "auto") or ( isinstance(head_radius, str) and head_radius == "auto" @@ -964,8 +965,7 @@ def make_sphere_model( ) ) logger.info( - "Set up EEG sphere model with scalp radius %7.1f mm\n" - % (1000 * head_radius,) + f"Set up EEG sphere model with scalp radius {1000 * head_radius:7.1f} mm\n" ) return sphere @@ -1082,7 +1082,7 @@ def get_fitting_dig(info, dig_kinds="auto", exclude_frontal=True, verbose=None): if len(hsp) <= 10: kinds_str = ", ".join(['"%s"' % _dig_kind_rev[d] for d in sorted(dig_kinds)]) - msg = "Only %s head digitization points of the specified kind%s (%s,)" % ( + msg = "Only {} head digitization points of the specified kind{} ({},)".format( len(hsp), _pl(dig_kinds), kinds_str, @@ -1105,22 +1105,22 @@ def _fit_sphere_to_headshape(info, dig_kinds, verbose=None): dev_head_t = Transform("meg", "head") head_to_dev = _ensure_trans(dev_head_t, "head", "meg") origin_device = apply_trans(head_to_dev, origin_head) - logger.info("Fitted sphere radius:".ljust(30) + "%0.1f mm" % (radius * 1e3,)) + logger.info("Fitted sphere radius:".ljust(30) + f"{radius * 1e3:0.1f} mm") _check_head_radius(radius) # > 2 cm away from head center in X or Y is strange 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" % tuple(1e3 * origin_head[:2]) + "(X, Y) fit ({:0.1f}, {:0.1f}) more than 20 mm from head frame " + "origin".format(*tuple(1e3 * origin_head[:2])) ) logger.info( "Origin head coordinates:".ljust(30) - + "%0.1f %0.1f %0.1f mm" % tuple(1e3 * origin_head) + + "{:0.1f} {:0.1f} {:0.1f} mm".format(*tuple(1e3 * origin_head)) ) logger.info( "Origin device coordinates:".ljust(30) - + "%0.1f %0.1f %0.1f mm" % tuple(1e3 * origin_device) + + "{:0.1f} {:0.1f} {:0.1f} mm".format(*tuple(1e3 * origin_device)) ) return radius, origin_head, origin_device @@ -1163,15 +1163,13 @@ def _check_origin(origin, info, coord_frame="head", disp=False): if isinstance(origin, str): if origin != "auto": raise ValueError( - 'origin must be a numerical array, or "auto", ' "not %s" % (origin,) + f'origin must be a numerical array, or "auto", not {origin}' ) if coord_frame == "head": R, origin = fit_sphere_to_headshape( info, verbose=_verbose_safe_false(), units="m" )[:2] - logger.info( - " Automatic origin fit: head of radius %0.1f mm" % (R * 1000.0,) - ) + logger.info(f" Automatic origin fit: head of radius {R * 1000:0.1f} mm") del R else: origin = (0.0, 0.0, 0.0) @@ -1179,12 +1177,12 @@ def _check_origin(origin, info, coord_frame="head", disp=False): if origin.shape != (3,): raise ValueError("origin must be a 3-element array") if disp: - origin_str = ", ".join(["%0.1f" % (o * 1000) for o in origin]) - msg = " Using origin %s mm in the %s frame" % (origin_str, coord_frame) + origin_str = ", ".join([f"{o * 1000:0.1f}" for o in origin]) + msg = f" Using origin {origin_str} mm in the {coord_frame} frame" if coord_frame == "meg" and info["dev_head_t"] is not None: o_dev = apply_trans(info["dev_head_t"], origin) - origin_str = ", ".join("%0.1f" % (o * 1000,) for o in o_dev) - msg += " (%s mm in the head frame)" % (origin_str,) + origin_str = ", ".join(f"{o * 1000:0.1f}" for o in o_dev) + msg += f" ({origin_str} mm in the head frame)" logger.info(msg) return origin @@ -1299,7 +1297,7 @@ def make_watershed_bem( if gcaatlas: fname = op.join(env["FREESURFER_HOME"], "average", "RB_all_withskull_*.gca") fname = sorted(glob.glob(fname))[::-1][0] - logger.info("Using GCA atlas: %s" % (fname,)) + logger.info(f"Using GCA atlas: {fname}") cmd += [ "-atlas", "-brain_atlas", @@ -1326,9 +1324,8 @@ def make_watershed_bem( ] # report and run logger.info( - "\nRunning mri_watershed for BEM segmentation with the " - "following parameters:\n\nResults dir = %s\nCommand = %s\n" - % (ws_dir, " ".join(cmd)) + "\nRunning mri_watershed for BEM segmentation with the following parameters:\n" + f"\nResults dir = {ws_dir}\nCommand = {' '.join(cmd)}\n" ) os.makedirs(op.join(ws_dir)) run_subprocess_env(cmd) @@ -1337,12 +1334,12 @@ def make_watershed_bem( new_info = _extract_volume_info(T1_mgz) if not new_info: warn( - "nibabel is not available or the volume info is invalid." - "Volume info not updated in the written surface." + "nibabel is not available or the volume info is invalid. Volume info " + "not updated in the written surface." ) surfs = ["brain", "inner_skull", "outer_skull", "outer_skin"] for s in surfs: - surf_ws_out = op.join(ws_dir, "%s_%s_surface" % (subject, s)) + surf_ws_out = op.join(ws_dir, f"{subject}_{s}_surface") rr, tris, volume_info = read_surface(surf_ws_out, read_metadata=True) # replace volume info, 'head' stays @@ -1352,7 +1349,7 @@ def make_watershed_bem( ) # Create symbolic links - surf_out = op.join(bem_dir, "%s.surf" % s) + surf_out = op.join(bem_dir, f"{s}.surf") if not overwrite and op.exists(surf_out): skip_symlink = True else: @@ -1363,9 +1360,8 @@ def make_watershed_bem( if skip_symlink: logger.info( - "Unable to create all symbolic links to .surf files " - "in bem folder. Use --overwrite option to recreate " - "them." + "Unable to create all symbolic links to .surf files in bem folder. Use " + "--overwrite option to recreate them." ) dest = op.join(bem_dir, "watershed") else: @@ -1373,8 +1369,8 @@ def make_watershed_bem( dest = bem_dir logger.info( - "\nThank you for waiting.\nThe BEM triangulations for this " - "subject are now available at:\n%s." % dest + "\nThank you for waiting.\nThe BEM triangulations for this subject are now " + f"available at:\n{dest}." ) # Write a head file for coregistration @@ -1399,7 +1395,7 @@ def make_watershed_bem( show=True, ) - logger.info("Created %s\n\nComplete." % (fname_head,)) + logger.info(f"Created {fname_head}\n\nComplete.") def _extract_volume_info(mgz): @@ -1929,9 +1925,7 @@ def _prepare_env(subject, subjects_dir): subjects_dir = get_subjects_dir(subjects_dir, raise_error=True) subject_dir = subjects_dir / subject if not subject_dir.is_dir(): - raise RuntimeError( - 'Could not find the subject data directory "%s"' % (subject_dir,) - ) + raise RuntimeError(f'Could not find the subject data directory "{subject_dir}"') env.update(SUBJECT=subject, SUBJECTS_DIR=str(subjects_dir), FREESURFER_HOME=fs_home) mri_dir = subject_dir / "mri" bem_dir = subject_dir / "bem" @@ -2152,11 +2146,9 @@ def make_flash_bem( flash_path.mkdir(exist_ok=True, parents=True) logger.info( - "\nProcessing the flash MRI data to produce BEM meshes with " - "the following parameters:\n" - "SUBJECTS_DIR = %s\n" - "SUBJECT = %s\n" - "Result dir = %s\n" % (subjects_dir, subject, bem_dir / "flash") + "\nProcessing the flash MRI data to produce BEM meshes with the following " + f"parameters:\nSUBJECTS_DIR = {subjects_dir}\nSUBJECT = {subject}\nResult dir =" + f"{bem_dir / 'flash'}\n" ) # Step 4 : Register with MPRAGE flash5 = flash_path / "flash5.mgz" diff --git a/mne/channels/_dig_montage_utils.py b/mne/channels/_dig_montage_utils.py index 0f34af975d2..2136934972d 100644 --- a/mne/channels/_dig_montage_utils.py +++ b/mne/channels/_dig_montage_utils.py @@ -30,7 +30,7 @@ def _read_dig_montage_egi( defusedxml = _soft_import("defusedxml", "reading EGI montages") root = defusedxml.ElementTree.parse(fname).getroot() ns = root.tag[root.tag.index("{") : root.tag.index("}") + 1] - sensors = root.find("%ssensorLayout/%ssensors" % (ns, ns)) + sensors = root.find(f"{ns}sensorLayout/{ns}sensors") fids = dict() dig_ch_pos = dict() diff --git a/mne/channels/_standard_montage_utils.py b/mne/channels/_standard_montage_utils.py index 7b70c57881b..4df6c685912 100644 --- a/mne/channels/_standard_montage_utils.py +++ b/mne/channels/_standard_montage_utils.py @@ -99,7 +99,7 @@ def _mgh_or_standard(basename, head_size, coord_frame="unknown"): pos = np.array(pos) / 1000.0 ch_pos = _check_dupes_odict(ch_names_, pos) - nasion, lpa, rpa = [ch_pos.pop(n) for n in fid_names] + nasion, lpa, rpa = (ch_pos.pop(n) for n in fid_names) if head_size is None: scale = 1.0 else: @@ -109,7 +109,7 @@ def _mgh_or_standard(basename, head_size, coord_frame="unknown"): # if we are in MRI/MNI coordinates, we need to replace nasion, LPA, and RPA # with those of fsaverage for ``trans='fsaverage'`` to work if coord_frame == "mri": - lpa, nasion, rpa = [x["r"].copy() for x in get_mni_fiducials("fsaverage")] + lpa, nasion, rpa = (x["r"].copy() for x in get_mni_fiducials("fsaverage")) nasion *= scale lpa *= scale rpa *= scale @@ -184,7 +184,7 @@ def _read_sfp(fname, head_size): ch_pos = _check_dupes_odict(ch_names, pos) del xs, ys, zs, ch_names # no one grants that fid names are there. - nasion, lpa, rpa = [ch_pos.pop(n, None) for n in fid_names] + nasion, lpa, rpa = (ch_pos.pop(n, None) for n in fid_names) if head_size is not None: scale = head_size / np.median(np.linalg.norm(pos, axis=-1)) @@ -274,7 +274,7 @@ def _read_elc(fname, head_size): pos *= head_size / np.median(np.linalg.norm(pos, axis=1)) ch_pos = _check_dupes_odict(ch_names_, pos) - nasion, lpa, rpa = [ch_pos.pop(n, None) for n in fid_names] + nasion, lpa, rpa = (ch_pos.pop(n, None) for n in fid_names) return make_dig_montage( ch_pos=ch_pos, coord_frame="unknown", nasion=nasion, lpa=lpa, rpa=rpa @@ -304,7 +304,7 @@ def _read_theta_phi_in_degrees(fname, head_size, fid_names=None, add_fiducials=F nasion, lpa, rpa = None, None, None if fid_names is not None: - nasion, lpa, rpa = [ch_pos.pop(n, None) for n in fid_names] + nasion, lpa, rpa = (ch_pos.pop(n, None) for n in fid_names) return make_dig_montage( ch_pos=ch_pos, coord_frame="unknown", nasion=nasion, lpa=lpa, rpa=rpa @@ -332,7 +332,7 @@ def _read_elp_besa(fname, head_size): fid_names = ("Nz", "LPA", "RPA") # No one grants that the fid names actually exist. - nasion, lpa, rpa = [ch_pos.pop(n, None) for n in fid_names] + nasion, lpa, rpa = (ch_pos.pop(n, None) for n in fid_names) return make_dig_montage(ch_pos=ch_pos, nasion=nasion, lpa=lpa, rpa=rpa) @@ -383,7 +383,7 @@ def _read_xyz(fname): ch_names = [] pos = [] file_format = op.splitext(fname)[1].lower() - with open(fname, "r") as f: + with open(fname) as f: if file_format != ".xyz": f.readline() # skip header delimiter = "," if file_format == ".csv" else "\t" diff --git a/mne/channels/channels.py b/mne/channels/channels.py index 4b87f8131e6..aee085891c4 100644 --- a/mne/channels/channels.py +++ b/mne/channels/channels.py @@ -549,7 +549,7 @@ def reorder_channels(self, ch_names): for ch_name in ch_names: ii = self.ch_names.index(ch_name) if ii in idx: - raise ValueError("Channel name repeated: %s" % (ch_name,)) + raise ValueError(f"Channel name repeated: {ch_name}") idx.append(ii) return self._pick_drop_channels(idx) @@ -982,7 +982,7 @@ def rename_channels(info, mapping, allow_duplicates=False, *, verbose=None): elif callable(mapping): new_names = [(ci, mapping(ch_name)) for ci, ch_name in enumerate(ch_names)] else: - raise ValueError("mapping must be callable or dict, not %s" % (type(mapping),)) + raise ValueError(f"mapping must be callable or dict, not {type(mapping)}") # check we got all strings out of the mapping for new_name in new_names: @@ -2110,9 +2110,7 @@ def read_vectorview_selection(name, fname=None, info=None, verbose=None): else: spacing = "old" elif info is not None: - raise TypeError( - "info must be an instance of Info or None, not %s" % (type(info),) - ) + raise TypeError(f"info must be an instance of Info or None, not {type(info)}") else: # info is None spacing = "old" @@ -2124,7 +2122,7 @@ def read_vectorview_selection(name, fname=None, info=None, verbose=None): # use this to make sure we find at least one match for each name name_found = {n: False for n in name} - with open(fname, "r") as fid: + with open(fname) as fid: sel = [] for line in fid: line = line.strip() diff --git a/mne/channels/interpolation.py b/mne/channels/interpolation.py index f805d640258..6c5042d1d04 100644 --- a/mne/channels/interpolation.py +++ b/mne/channels/interpolation.py @@ -169,7 +169,7 @@ def _interpolate_bads_eeg(inst, origin, exclude=None, ecog=False, verbose=None): logger.info(f"Computing interpolation matrix from {len(pos_good)} sensor positions") interpolation = _make_interpolation_matrix(pos_good, pos_bad) - logger.info("Interpolating {} sensors".format(len(pos_bad))) + logger.info(f"Interpolating {len(pos_bad)} sensors") _do_interp_dots(inst, interpolation, goods_idx, bads_idx) diff --git a/mne/channels/layout.py b/mne/channels/layout.py index 043bb9c33b7..d19794115d7 100644 --- a/mne/channels/layout.py +++ b/mne/channels/layout.py @@ -85,7 +85,7 @@ def save(self, fname, overwrite=False): height = self.pos[:, 3] fname = _check_fname(fname, overwrite=overwrite, name=fname) if fname.suffix == ".lout": - out_str = "%8.2f %8.2f %8.2f %8.2f\n" % self.box + out_str = "{:8.2f} {:8.2f} {:8.2f} {:8.2f}\n".format(*self.box) elif fname.suffix == ".lay": out_str = "" else: @@ -107,7 +107,7 @@ def save(self, fname, overwrite=False): def __repr__(self): """Return the string representation.""" - return "" % ( + return "".format( self.kind, ", ".join(self.names[:3]), ) @@ -1181,7 +1181,7 @@ def generate_2d_layout( if ch_indices is None: ch_indices = np.arange(xy.shape[0]) if ch_names is None: - ch_names = ["{}".format(i) for i in ch_indices] + ch_names = list(map(str, ch_indices)) if len(ch_names) != len(ch_indices): raise ValueError("# channel names and indices must be equal") @@ -1205,7 +1205,7 @@ def generate_2d_layout( # Create box and pos variable box = _box_size(np.vstack([x, y]).T, padding=pad) box = (0, 0, box[0], box[1]) - w, h = [np.array([i] * x.shape[0]) for i in [w, h]] + w, h = (np.array([i] * x.shape[0]) for i in [w, h]) loc_params = np.vstack([x, y, w, h]).T layout = Layout(box, loc_params, ch_names, ch_indices, name) diff --git a/mne/channels/montage.py b/mne/channels/montage.py index 3d7ad340df8..6e63ec28cf5 100644 --- a/mne/channels/montage.py +++ b/mne/channels/montage.py @@ -785,7 +785,7 @@ def read_dig_dat(fname): fname = _check_fname(fname, overwrite="read", must_exist=True) - with open(fname, "r") as fid: + with open(fname) as fid: lines = fid.readlines() ch_names, poss = list(), list() @@ -796,8 +796,8 @@ def read_dig_dat(fname): continue elif len(items) != 5: raise ValueError( - "Error reading %s, line %s has unexpected number of entries:\n" - "%s" % (fname, i, line.rstrip()) + f"Error reading {fname}, line {i} has unexpected number of entries:\n" + f"{line.rstrip()}" ) num = items[1] if num == "67": @@ -1352,7 +1352,7 @@ def _read_isotrak_elp_points(fname): and 'points'. """ value_pattern = r"\-?\d+\.?\d*e?\-?\d*" - coord_pattern = r"({0})\s+({0})\s+({0})\s*$".format(value_pattern) + coord_pattern = rf"({value_pattern})\s+({value_pattern})\s+({value_pattern})\s*$" with open(fname) as fid: file_str = fid.read() @@ -1474,11 +1474,9 @@ def read_dig_polhemus_isotrak(fname, ch_names=None, unit="m"): data["ch_pos"] = OrderedDict(zip(ch_names, points)) else: raise ValueError( - ( - "Length of ``ch_names`` does not match the number of points" - " in {fname}. Expected ``ch_names`` length {n_points:d}," - " given {n_chnames:d}" - ).format(fname=fname, n_points=points.shape[0], n_chnames=len(ch_names)) + "Length of ``ch_names`` does not match the number of points in " + f"{fname}. Expected ``ch_names`` length {points.shape[0]}, given " + f"{len(ch_names)}" ) return make_dig_montage(**data) @@ -1486,7 +1484,7 @@ def read_dig_polhemus_isotrak(fname, ch_names=None, unit="m"): def _is_polhemus_fastscan(fname): header = "" - with open(fname, "r") as fid: + with open(fname) as fid: for line in fid: if not line.startswith("%"): break @@ -1621,7 +1619,7 @@ def read_custom_montage(fname, head_size=HEAD_SIZE_DEFAULT, coord_frame=None): if ext in SUPPORTED_FILE_EXT["eeglab"]: if head_size is None: - raise ValueError("``head_size`` cannot be None for '{}'".format(ext)) + raise ValueError(f"``head_size`` cannot be None for '{ext}'") ch_names, pos = _read_eeglab_locations(fname) scale = head_size / np.median(np.linalg.norm(pos, axis=-1)) pos *= scale @@ -1642,7 +1640,7 @@ def read_custom_montage(fname, head_size=HEAD_SIZE_DEFAULT, coord_frame=None): elif ext in SUPPORTED_FILE_EXT["generic (Theta-phi in degrees)"]: if head_size is None: - raise ValueError("``head_size`` cannot be None for '{}'".format(ext)) + raise ValueError(f"``head_size`` cannot be None for '{ext}'") montage = _read_theta_phi_in_degrees( fname, head_size=head_size, fid_names=("Nz", "LPA", "RPA") ) diff --git a/mne/channels/tests/test_montage.py b/mne/channels/tests/test_montage.py index 31320ed951c..08971ab803b 100644 --- a/mne/channels/tests/test_montage.py +++ b/mne/channels/tests/test_montage.py @@ -579,7 +579,7 @@ def test_read_dig_dat(tmp_path): for row in rows: name = row[0].rjust(10) data = "\t".join(map(str, row[1:])) - fid.write("%s\t%s\n" % (name, data)) + fid.write(f"{name}\t{data}\n") # construct expected value idents = { 78: FIFF.FIFFV_POINT_NASION, @@ -717,27 +717,23 @@ def isotrak_eeg(tmp_path_factory): fname = tmp_path_factory.mktemp("data") / "test.eeg" with open(str(fname), "w") as fid: fid.write( - ( - "3 200\n" - "//Shape file\n" - "//Minor revision number\n" - "2\n" - "//Subject Name\n" - "%N Name \n" - "////Shape code, number of digitized points\n" - ) + "3 200\n" + "//Shape file\n" + "//Minor revision number\n" + "2\n" + "//Subject Name\n" + "%N Name \n" + "////Shape code, number of digitized points\n" ) - fid.write("0 {rows:d}\n".format(rows=N_ROWS)) + fid.write(f"0 {N_ROWS:d}\n") fid.write( - ( - "//Position of fiducials X+, Y+, Y- on the subject\n" - "%F 0.11056 -5.421e-19 0 \n" - "%F -0.00021075 0.080793 -7.5894e-19 \n" - "%F 0.00021075 -0.080793 -2.8731e-18 \n" - "//No of rows, no of columns; position of digitized points\n" - ) + "//Position of fiducials X+, Y+, Y- on the subject\n" + "%F 0.11056 -5.421e-19 0 \n" + "%F -0.00021075 0.080793 -7.5894e-19 \n" + "%F 0.00021075 -0.080793 -2.8731e-18 \n" + "//No of rows, no of columns; position of digitized points\n" ) - fid.write("{rows:d} {cols:d}\n".format(rows=N_ROWS, cols=N_COLS)) + fid.write(f"{N_ROWS} {N_COLS}\n") for row in content: fid.write("\t".join("%0.18e" % cell for cell in row) + "\n") @@ -753,7 +749,7 @@ def test_read_dig_polhemus_isotrak_eeg(isotrak_eeg): "lpa": np.array([-2.1075e-04, 8.0793e-02, -7.5894e-19]), "rpa": np.array([2.1075e-04, -8.0793e-02, -2.8731e-18]), } - ch_names = ["eeg {:01d}".format(ii) for ii in range(N_CHANNELS)] + ch_names = [f"eeg {ii:01d}" for ii in range(N_CHANNELS)] EXPECTED_CH_POS = dict( zip(ch_names, np.random.RandomState(_SEED).randn(N_CHANNELS, 3)) ) @@ -786,7 +782,7 @@ def test_read_dig_polhemus_isotrak_error_handling(isotrak_eeg, tmp_path): with pytest.raises(ValueError, match=EXPECTED_ERR_MSG): _ = read_dig_polhemus_isotrak( fname=isotrak_eeg, - ch_names=["eeg {:01d}".format(ii) for ii in range(N_CHANNELS + 42)], + ch_names=[f"eeg {ii:01d}" for ii in range(N_CHANNELS + 42)], ) # Check fname extensions @@ -1458,10 +1454,7 @@ def _fake_montage(ch_names): "ignore:.*Could not parse meas date from the header. Setting to None." ), pytest.mark.filterwarnings( - ( - "ignore:.*Could not define the number of bytes automatically." - " Defaulting to 2." - ) + "ignore:.*Could not define the number of bytes automatically. Defaulting to 2." ), ] diff --git a/mne/chpi.py b/mne/chpi.py index 7512f184eb1..780c892e3d6 100644 --- a/mne/chpi.py +++ b/mne/chpi.py @@ -307,7 +307,7 @@ def extract_chpi_locs_kit(raw, stim_channel="MISC 064", *, verbose=None): assert dtype.itemsize == header["size"], (dtype.itemsize, header["size"]) all_data = list() for fname in raw._filenames: - with open(fname, "r") as fid: + with open(fname) as fid: fid.seek(header["offset"]) all_data.append( np.fromfile(fid, dtype, count=header["count"]).reshape(-1, n_coils) @@ -379,8 +379,8 @@ def get_chpi_info(info, on_missing="raise", verbose=None): # get frequencies hpi_freqs = np.array([float(x["coil_freq"]) for x in hpi_coils]) logger.info( - "Using %s HPI coils: %s Hz" - % (len(hpi_freqs), " ".join(str(int(s)) for s in hpi_freqs)) + f"Using {len(hpi_freqs)} HPI coils: {' '.join(str(int(s)) for s in hpi_freqs)} " + "Hz" ) # how cHPI active is indicated in the FIF file @@ -448,11 +448,11 @@ def _get_hpi_initial_fit(info, adjust=False, verbose=None): raise RuntimeError("cHPI coordinate frame incorrect") # Give the user some info logger.info( - "HPIFIT: %s coils digitized in order %s" - % (len(pos_order), " ".join(str(o + 1) for o in pos_order)) + f"HPIFIT: {len(pos_order)} coils digitized in order " + f"{' '.join(str(o + 1) for o in pos_order)}" ) logger.debug( - "HPIFIT: %s coils accepted: %s" % (len(used), " ".join(str(h) for h in used)) + f"HPIFIT: {len(used)} coils accepted: {' '.join(str(h) for h in used)}" ) hpi_rrs = np.array([d["r"] for d in hpi_dig])[pos_order] assert len(hpi_rrs) >= 3 @@ -470,11 +470,9 @@ 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" % tuple(moment)) + logger.debug("{:g} {:g} {:g}".format(*tuple(moment))) errors = np.linalg.norm(hpi_rrs - hpi_rrs_fit, axis=1) - logger.debug( - "HPIFIT errors: %s mm." % ", ".join("%0.1f" % (1000.0 * e) for e in errors) - ) + logger.debug(f"HPIFIT errors: {', '.join(f'{1000 * e:0.1f}' for e in errors)} mm.") if errors.sum() < len(errors) * dist_limit: logger.info("HPI consistency of isotrak and hpifit is OK.") elif not adjust and (len(used) == len(hpi_dig)): @@ -487,24 +485,22 @@ def _get_hpi_initial_fit(info, adjust=False, verbose=None): if not adjust: if err >= dist_limit: warn( - "Discrepancy of HPI coil %d isotrak and hpifit is " - "%.1f mm!" % (hi + 1, d) + f"Discrepancy of HPI coil {hi + 1} isotrak and hpifit is " + f"{d:.1f} mm!" ) elif hi + 1 not in used: if goodness[hi] >= good_limit: logger.info( - "Note: HPI coil %d isotrak is adjusted by " - "%.1f mm!" % (hi + 1, d) + f"Note: HPI coil {hi + 1} isotrak is adjusted by {d:.1f} mm!" ) hpi_rrs[hi] = r_fit else: warn( - "Discrepancy of HPI coil %d isotrak and hpifit of " - "%.1f mm was not adjusted!" % (hi + 1, d) + f"Discrepancy of HPI coil {hi + 1} isotrak and hpifit of " + f"{d:.1f} mm was not adjusted!" ) logger.debug( - "HP fitting limits: err = %.1f mm, gval = %.3f." - % (1000 * dist_limit, good_limit) + f"HP fitting limits: err = {1000 * dist_limit:.1f} mm, gval = {good_limit:.3f}." ) return hpi_rrs.astype(float) @@ -643,8 +639,9 @@ def _setup_hpi_amplitude_fitting( else: line_freqs = np.zeros([0]) logger.info( - "Line interference frequencies: %s Hz" - % " ".join(["%d" % lf for lf in line_freqs]) + "Line interference frequencies: {} Hz".format( + " ".join([f"{lf}" for lf in line_freqs]) + ) ) # worry about resampled/filtered data. # What to do e.g. if Raw has been resampled and some of our @@ -657,8 +654,8 @@ def _setup_hpi_amplitude_fitting( hpi_ons = hpi_ons[keepers] elif not keepers.all(): raise RuntimeError( - "Found HPI frequencies %s above the lowpass " - "(or Nyquist) frequency %0.1f" % (hpi_freqs[~keepers].tolist(), highest) + f"Found HPI frequencies {hpi_freqs[~keepers].tolist()} above the lowpass (" + f"or Nyquist) frequency {highest:0.1f}" ) # calculate optimal window length. if isinstance(t_window, str): @@ -671,8 +668,8 @@ def _setup_hpi_amplitude_fitting( t_window = 0.2 t_window = float(t_window) if t_window <= 0: - raise ValueError("t_window (%s) must be > 0" % (t_window,)) - logger.info("Using time window: %0.1f ms" % (1000 * t_window,)) + raise ValueError(f"t_window ({t_window}) must be > 0") + logger.info(f"Using time window: {1000 * t_window:0.1f} ms") window_nsamp = np.rint(t_window * info["sfreq"]).astype(int) model = _setup_hpi_glm(hpi_freqs, line_freqs, info["sfreq"], window_nsamp) inv_model = np.linalg.pinv(model) @@ -869,25 +866,22 @@ def _check_chpi_param(chpi_, name): want_keys = list(want_ndims.keys()) + extra_keys if set(want_keys).symmetric_difference(chpi_): raise ValueError( - "%s must be a dict with entries %s, got %s" - % (name, want_keys, sorted(chpi_.keys())) + f"{name} must be a dict with entries {want_keys}, got " + f"{sorted(chpi_.keys())}" ) n_times = None for key, want_ndim in want_ndims.items(): - key_str = "%s[%s]" % (name, key) + key_str = f"{name}[{key}]" val = chpi_[key] _validate_type(val, np.ndarray, key_str) shape = val.shape if val.ndim != want_ndim: - raise ValueError( - "%s must have ndim=%d, got %d" % (key_str, want_ndim, val.ndim) - ) + raise ValueError(f"{key_str} must have ndim={want_ndim}, got {val.ndim}") if n_times is None and key != "proj": n_times = shape[0] if n_times != shape[0] and key != "proj": raise ValueError( - "%s have inconsistent number of time " - "points in %s" % (name, want_keys) + f"{name} have inconsistent number of time points in {want_keys}" ) if name == "chpi_locs": n_coils = chpi_["rrs"].shape[1] @@ -895,15 +889,14 @@ def _check_chpi_param(chpi_, name): val = chpi_[key] if val.shape[1] != n_coils: raise ValueError( - 'chpi_locs["rrs"] had values for %d coils but' - ' chpi_locs["%s"] had values for %d coils' - % (n_coils, key, val.shape[1]) + f'chpi_locs["rrs"] had values for {n_coils} coils but ' + f'chpi_locs["{key}"] had values for {val.shape[1]} coils' ) for key in ("rrs", "moments"): val = chpi_[key] if val.shape[2] != 3: raise ValueError( - 'chpi_locs["%s"].shape[2] must be 3, got ' "shape %s" % (key, shape) + f'chpi_locs["{key}"].shape[2] must be 3, got shape {shape}' ) else: assert name == "chpi_amplitudes" @@ -912,8 +905,8 @@ def _check_chpi_param(chpi_, name): n_ch = len(proj["data"]["col_names"]) if slopes.shape[0] != n_times or slopes.shape[2] != n_ch: raise ValueError( - "slopes must have shape[0]==%d and shape[2]==%d," - " got shape %s" % (n_times, n_ch, slopes.shape) + f"slopes must have shape[0]=={n_times} and shape[2]=={n_ch}, got shape " + f"{slopes.shape}" ) @@ -1003,9 +996,9 @@ def compute_head_pos( n_good = ((g_coils >= gof_limit) & (errs < dist_limit)).sum() if n_good < 3: warn( - _time_prefix(fit_time) + "%s/%s good HPI fits, cannot " - "determine the transformation (%s mm/GOF)!" - % ( + _time_prefix(fit_time) + + "{}/{} good HPI fits, cannot " + "determine the transformation ({} mm/GOF)!".format( n_good, n_coils, ", ".join( @@ -1068,13 +1061,13 @@ def compute_head_pos( v = d / dt # m/s d = 100 * np.linalg.norm(this_quat[3:] - pos_0) # dis from 1st logger.debug( - " #t = %0.3f, #e = %0.2f cm, #g = %0.3f, " - "#v = %0.2f cm/s, #r = %0.2f rad/s, #d = %0.2f cm" - % (fit_time, 100 * errs.mean(), g, 100 * v, r, d) + 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 = %s " - % (fit_time, " ".join(map("{:8.5f}".format, this_quat))) + " #t = {:0.3f}, #q = {} ".format( + fit_time, " ".join(map("{:8.5f}".format, this_quat)) + ) ) quats.append( @@ -1504,7 +1497,7 @@ def filter_chpi( raise RuntimeError("raw data must be preloaded") t_step = float(t_step) if t_step <= 0: - raise ValueError("t_step (%s) must be > 0" % (t_step,)) + raise ValueError(f"t_step ({t_step}) must be > 0") n_step = int(np.ceil(t_step * raw.info["sfreq"])) if include_line and raw.info["line_freq"] is None: raise RuntimeError( @@ -1617,11 +1610,8 @@ def get_active_chpi(raw, *, on_missing="raise", verbose=None): # check whether we have a neuromag system if system not in ["122m", "306m"]: raise NotImplementedError( - ( - "Identifying active HPI channels" - " is not implemented for other systems" - " than neuromag." - ) + "Identifying active HPI channels is not implemented for other systems than " + "neuromag." ) # extract hpi info chpi_info = get_chpi_info(raw.info, on_missing=on_missing) diff --git a/mne/commands/mne_anonymize.py b/mne/commands/mne_anonymize.py index 3c0a7ebfd27..a282f016ede 100644 --- a/mne/commands/mne_anonymize.py +++ b/mne/commands/mne_anonymize.py @@ -52,7 +52,7 @@ def mne_anonymize(fif_fname, out_fname, keep_his, daysback, overwrite): dir_name = op.split(fif_fname)[0] if out_fname is None: fif_bname = op.basename(fif_fname) - out_fname = op.join(dir_name, "{}-{}".format(ANONYMIZE_FILE_PREFIX, fif_bname)) + out_fname = op.join(dir_name, f"{ANONYMIZE_FILE_PREFIX}-{fif_bname}") elif not op.isabs(out_fname): out_fname = op.join(dir_name, out_fname) diff --git a/mne/commands/mne_compute_proj_ecg.py b/mne/commands/mne_compute_proj_ecg.py index f5f798a4968..caab628bbb2 100644 --- a/mne/commands/mne_compute_proj_ecg.py +++ b/mne/commands/mne_compute_proj_ecg.py @@ -256,7 +256,7 @@ def run(): raise ValueError('qrsthr must be "auto" or a float') if bad_fname is not None: - with open(bad_fname, "r") as fid: + with open(bad_fname) as fid: bads = [w.rstrip() for w in fid.readlines()] print("Bad channels read : %s" % bads) else: diff --git a/mne/commands/mne_compute_proj_eog.py b/mne/commands/mne_compute_proj_eog.py index 456bf3b6080..165818facc4 100644 --- a/mne/commands/mne_compute_proj_eog.py +++ b/mne/commands/mne_compute_proj_eog.py @@ -253,7 +253,7 @@ def run(): ch_name = options.ch_name if bad_fname is not None: - with open(bad_fname, "r") as fid: + with open(bad_fname) as fid: bads = [w.rstrip() for w in fid.readlines()] print("Bad channels read : %s" % bads) else: diff --git a/mne/commands/mne_maxfilter.py b/mne/commands/mne_maxfilter.py index 5c631dcf457..4cbb1dc9522 100644 --- a/mne/commands/mne_maxfilter.py +++ b/mne/commands/mne_maxfilter.py @@ -222,7 +222,7 @@ def run(): out_fname = prefix + "_sss.fif" if origin is not None and os.path.exists(origin): - with open(origin, "r") as fid: + with open(origin) as fid: origin = fid.readlines()[0].strip() origin = mne.preprocessing.apply_maxfilter( diff --git a/mne/commands/tests/test_commands.py b/mne/commands/tests/test_commands.py index 26e1f7fa540..fced5272efc 100644 --- a/mne/commands/tests/test_commands.py +++ b/mne/commands/tests/test_commands.py @@ -130,7 +130,7 @@ def test_clean_eog_ecg(tmp_path): with ArgvSetter(("-i", use_fname, "--quiet")): mne_clean_eog_ecg.run() for key, count in (("proj", 2), ("-eve", 3)): - fnames = glob.glob(op.join(tempdir, "*%s.fif" % key)) + fnames = glob.glob(op.join(tempdir, f"*{key}.fif")) assert len(fnames) == count @@ -277,14 +277,14 @@ def test_watershed_bem(tmp_path): mne_watershed_bem.run() os.chmod(new_fname, old_mode) for s in ("outer_skin", "outer_skull", "inner_skull"): - assert not op.isfile(op.join(subject_path_new, "bem", "%s.surf" % s)) + assert not op.isfile(op.join(subject_path_new, "bem", f"{s}.surf")) with ArgvSetter(args): mne_watershed_bem.run() kwargs = dict(rtol=1e-5, atol=1e-5) for s in ("outer_skin", "outer_skull", "inner_skull"): rr, tris, vol_info = read_surface( - op.join(subject_path_new, "bem", "%s.surf" % s), read_metadata=True + op.join(subject_path_new, "bem", f"{s}.surf"), read_metadata=True ) assert_equal(len(tris), 20480) assert_equal(tris.min(), 0) @@ -372,14 +372,12 @@ def test_flash_bem(tmp_path): kwargs = dict(rtol=1e-5, atol=1e-5) for s in ("outer_skin", "outer_skull", "inner_skull"): - rr, tris = read_surface(op.join(subject_path_new, "bem", "%s.surf" % s)) + rr, tris = read_surface(op.join(subject_path_new, "bem", f"{s}.surf")) assert_equal(len(tris), 5120) assert_equal(tris.min(), 0) assert_equal(rr.shape[0], tris.max() + 1) # compare to the testing flash surfaces - rr_c, tris_c = read_surface( - op.join(subjects_dir, "sample", "bem", "%s.surf" % s) - ) + rr_c, tris_c = read_surface(op.join(subjects_dir, "sample", "bem", f"{s}.surf")) assert_allclose(rr, rr_c, **kwargs) assert_allclose(tris, tris_c, **kwargs) diff --git a/mne/commands/utils.py b/mne/commands/utils.py index 10334ce0acb..112ff27deca 100644 --- a/mne/commands/utils.py +++ b/mne/commands/utils.py @@ -68,7 +68,7 @@ def get_optparser(cmdpath, usage=None, prog_prefix="mne", version=None): command = command[len(prog_prefix) + 1 :] # +1 is for `_` character # Set prog - prog = prog_prefix + " {}".format(command) + prog = prog_prefix + f" {command}" # Set version if version is None: @@ -106,6 +106,6 @@ def print_help(): # noqa print_help() else: cmd = sys.argv[1] - cmd = importlib.import_module(".mne_%s" % (cmd,), "mne.commands") + cmd = importlib.import_module(f".mne_{cmd}", "mne.commands") sys.argv = sys.argv[1:] cmd.run() diff --git a/mne/conftest.py b/mne/conftest.py index b0882346586..a693b702935 100644 --- a/mne/conftest.py +++ b/mne/conftest.py @@ -274,7 +274,7 @@ def matplotlib_config(): class CallbackRegistryReraise(orig): def __init__(self, exception_handler=None, signals=None): - super(CallbackRegistryReraise, self).__init__(exception_handler) + super().__init__(exception_handler) cbook.CallbackRegistry = CallbackRegistryReraise diff --git a/mne/coreg.py b/mne/coreg.py index 32f2cb6d614..c83b8f3106f 100644 --- a/mne/coreg.py +++ b/mne/coreg.py @@ -226,8 +226,8 @@ def create_default_subject(fs_home=None, update=False, subjects_dir=None, verbos dirname = os.path.join(fs_src, name) if not os.path.isdir(dirname): raise OSError( - "Freesurfer fsaverage seems to be incomplete: No " - "directory named %s found in %s" % (name, fs_src) + "Freesurfer fsaverage seems to be incomplete: No directory named " + f"{name} found in {fs_src}" ) # make sure destination does not already exist @@ -241,9 +241,9 @@ def create_default_subject(fs_home=None, update=False, subjects_dir=None, verbos ) 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." % ("fsaverage", subjects_dir) + "Can not create fsaverage because {!r} already exists in " + "subjects_dir {!r}. Delete or rename the existing fsaverage " + "subject folder.".format("fsaverage", subjects_dir) ) # copy fsaverage from freesurfer @@ -429,12 +429,8 @@ def fit_matched_points( weights = np.asarray(weights, src_pts.dtype) if weights.ndim != 1 or weights.size not in (src_pts.shape[0], 1): raise ValueError( - "weights (shape=%s) must be None or have shape " - "(%s,)" - % ( - weights.shape, - src_pts.shape[0], - ) + f"weights (shape={weights.shape}) must be None or have shape " + f"({src_pts.shape[0]},)" ) weights = weights[:, np.newaxis] @@ -541,7 +537,7 @@ def error(x): else: raise NotImplementedError( "The specified parameter combination is not implemented: " - "rotate=%r, translate=%r, scale=%r" % param_info + "rotate={!r}, translate={!r}, scale={!r}".format(*param_info) ) x, _, _, _, _ = leastsq(error, x0, full_output=True) @@ -827,8 +823,8 @@ def read_mri_cfg(subject, subjects_dir=None): if not fname.exists(): raise OSError( - "%r does not seem to be a scaled mri subject: %r does " - "not exist." % (subject, fname) + f"{subject!r} does not seem to be a scaled mri subject: {fname!r} does not" + "exist." ) logger.info("Reading MRI cfg file %s" % fname) @@ -916,8 +912,8 @@ def _scale_params(subject_to, subject_from, scale, subjects_dir): scale = np.atleast_1d(scale) if scale.ndim != 1 or scale.shape[0] not in (1, 3): raise ValueError( - "Invalid shape for scale parameter. Need scalar " - "or array of length 3. Got shape %s." % (scale.shape,) + "Invalid shape for scale parameter. Need scalar or array of length 3. Got " + f"shape {scale.shape}." ) n_params = len(scale) return str(subjects_dir), subject_from, scale, n_params == 1 @@ -1105,14 +1101,14 @@ def scale_mri( if np.isclose(scale[1], scale[0]) and np.isclose(scale[2], scale[0]): scale = scale[0] # speed up scaling conditionals using a singleton elif scale.shape != (1,): - raise ValueError("scale must have shape (3,) or (1,), got %s" % (scale.shape,)) + raise ValueError(f"scale must have shape (3,) or (1,), got {scale.shape}") # make sure we have an empty target directory dest = subject_dirname.format(subject=subject_to, subjects_dir=subjects_dir) if os.path.exists(dest): if not overwrite: raise OSError( - "Subject directory for %s already exists: %r" % (subject_to, dest) + f"Subject directory for {subject_to} already exists: {dest!r}" ) shutil.rmtree(dest) @@ -2014,12 +2010,12 @@ def _setup_icp(self, n_scale_params): self._processed_high_res_mri_points[ getattr( self, - "_nearest_transformed_high_res_mri_idx_%s" % (key,), + f"_nearest_transformed_high_res_mri_idx_{key}", ) ] ) weights.append( - np.full(len(mri_pts[-1]), getattr(self, "_%s_weight" % key)) + np.full(len(mri_pts[-1]), getattr(self, f"_{key}_weight")) ) if self._has_eeg_data and self._eeg_weight > 0: head_pts.append(self._dig_dict["dig_ch_pos_location"]) diff --git a/mne/cov.py b/mne/cov.py index 1b2d4cd8ebe..311121c8f87 100644 --- a/mne/cov.py +++ b/mne/cov.py @@ -704,7 +704,7 @@ def compute_raw_covariance( tstep = tmax - tmin if tstep is None else float(tstep) tstep_m1 = tstep - dt # inclusive! events = make_fixed_length_events(raw, 1, tmin, tmax, tstep) - logger.info("Using up to %s segment%s" % (len(events), _pl(events))) + logger.info(f"Using up to {len(events)} segment{_pl(events)}") # don't exclude any bad channels, inverses expect all channels present if picks is None: @@ -819,13 +819,13 @@ def _check_method_params( for key, values in method_params.items(): if key not in _method_params: raise ValueError( - 'key (%s) must be "%s"' % (key, '" or "'.join(_method_params)) + 'key ({}) must be "{}"'.format(key, '" or "'.join(_method_params)) ) _method_params[key].update(method_params[key]) shrinkage = method_params.get("shrinkage", {}).get("shrinkage", 0.1) if not 0 <= shrinkage <= 1: - raise ValueError("shrinkage must be between 0 and 1, got %s" % (shrinkage,)) + raise ValueError(f"shrinkage must be between 0 and 1, got {shrinkage}") was_auto = False if method is None: @@ -839,10 +839,8 @@ def _check_method_params( if not all(k in accepted_methods for k in method): raise ValueError( - "Invalid {name} ({method}). Accepted values (individually or " - 'in a list) are any of "{accepted_methods}" or None.'.format( - name=name, method=method, accepted_methods=accepted_methods - ) + f"Invalid {name} ({method}). Accepted values (individually or " + f"in a list) are any of '{accepted_methods}' or None." ) if not (isinstance(rank, str) and rank == "full"): if was_auto: @@ -850,14 +848,13 @@ def _check_method_params( for method_ in method: if method_ in ("pca", "factor_analysis"): raise ValueError( - '%s can so far only be used with rank="full",' - " got rank=%r" % (method_, rank) + f'{method_} can so far only be used with rank="full", got rank=' + f"{rank!r}" ) if not keep_sample_mean: if len(method) != 1 or "empirical" not in method: raise ValueError( - "`keep_sample_mean=False` is only supported" - 'with %s="empirical"' % (name,) + f'`keep_sample_mean=False` is only supported with {name}="empirical"' ) for p, v in _method_params.items(): if v.get("assume_centered", None) is False: @@ -1090,8 +1087,8 @@ def _unpack_epochs(epochs): and not np.allclose(orig["trans"], epoch.info["dev_head_t"]["trans"]) ): msg = ( - "MEG<->Head transform mismatch between epochs[0]:\n%s\n\n" - "and epochs[%s]:\n%s" % (orig, ei, epoch.info["dev_head_t"]) + "MEG<->Head transform mismatch between epochs[0]:\n{}\n\n" + "and epochs[{}]:\n{}".format(orig, ei, epoch.info["dev_head_t"]) ) _on_missing(on_mismatch, msg, "on_mismatch") @@ -1196,7 +1193,7 @@ def _unpack_epochs(epochs): if len(covs) > 1: msg = ["log-likelihood on unseen data (descending order):"] for c in covs: - msg.append("%s: %0.3f" % (c["method"], c["loglik"])) + msg.append(f"{c['method']}: {c['loglik']:0.3f}") logger.info("\n ".join(msg)) if return_estimators: out = covs @@ -1216,7 +1213,7 @@ def _check_scalings_user(scalings): _check_option("the keys in `scalings`", k, ["mag", "grad", "eeg"]) elif scalings is not None and not isinstance(scalings, np.ndarray): raise TypeError( - "scalings must be a dict, ndarray, or None, got %s" % type(scalings) + f"scalings must be a dict, ndarray, or None, got {type(scalings)}" ) scalings = _handle_default("scalings", scalings) return scalings @@ -1266,21 +1263,21 @@ def _compute_covariance_auto( (key, np.searchsorted(used, picks)) for key, picks in picks_list ] sub_info = pick_info(info, used) if len(used) != len(mask) else info - logger.info("Reducing data rank from %s -> %s" % (len(mask), eigvec.shape[0])) + logger.info(f"Reducing data rank from {len(mask)} -> {eigvec.shape[0]}") estimator_cov_info = list() - msg = "Estimating covariance using %s" + msg = "Estimating covariance using {}" ok_sklearn = check_version("sklearn") if not ok_sklearn and (len(method) != 1 or method[0] != "empirical"): raise ValueError( - "scikit-learn is not installed, `method` must be " - "`empirical`, got %s" % (method,) + "scikit-learn is not installed, `method` must be `empirical`, got " + f"{method}" ) for method_ in method: data_ = data.copy() name = method_.__name__ if callable(method_) else method_ - logger.info(msg % name.upper()) + logger.info(msg.format(name.upper())) mp = method_params[method_] _info = {} @@ -1696,8 +1693,9 @@ def _get_ch_whitener(A, pca, ch_type, rank): mask[:-rank] = False logger.info( - " Setting small %s eigenvalues to zero (%s)" - % (ch_type, "using PCA" if pca else "without PCA") + " Setting small {} eigenvalues to zero ({})".format( + ch_type, "using PCA" if pca else "without PCA" + ) ) if pca: # No PCA case. # This line will reduce the actual number of variables in data @@ -1991,9 +1989,8 @@ def regularize( if len(picks_dict.get("meg", [])) > 0 and rank != "full": # combined if mag != grad: raise ValueError( - "On data where magnetometers and gradiometers " - "are dependent (e.g., SSSed data), mag (%s) must " - "equal grad (%s)" % (mag, grad) + "On data where magnetometers and gradiometers are dependent (e.g., " + f"SSSed data), mag ({mag}) must equal grad ({grad})" ) logger.info("Regularizing MEG channels jointly") regs["meg"] = mag @@ -2039,9 +2036,9 @@ def regularize( continue reg = regs[ch_type] if reg == 0.0: - logger.info(" %s regularization : None" % desc) + logger.info(f" {desc} regularization : None") continue - logger.info(" %s regularization : %s" % (desc, reg)) + logger.info(f" {desc} regularization : {reg}") this_C = C[np.ix_(idx, idx)] U = np.eye(this_C.shape[0]) @@ -2053,8 +2050,7 @@ def regularize( # This adjustment ends up being redundant if rank is None: U = _safe_svd(P)[0][:, :-ncomp] logger.info( - " Created an SSP operator for %s " - "(dimension = %d)" % (desc, ncomp) + f" Created an SSP operator for {desc} (dimension = {ncomp})" ) else: this_picks = pick_channels(info["ch_names"], this_ch_names) @@ -2095,8 +2091,8 @@ def _regularized_covariance(data, reg=None, method_params=None, info=None, rank= reg = float(reg) if method_params is not None: raise ValueError( - "If reg is a float, method_params must be None " - "(got %s)" % (type(method_params),) + "If reg is a float, method_params must be None (got " + f"{type(method_params)})" ) method_params = dict( shrinkage=dict(shrinkage=reg, assume_centered=True, store_precision=False) @@ -2190,12 +2186,12 @@ def compute_whitener( _validate_type(pca, (str, bool), "space") _valid_pcas = (True, "white", False) if pca not in _valid_pcas: - raise ValueError("space must be one of %s, got %s" % (_valid_pcas, pca)) + raise ValueError(f"space must be one of {_valid_pcas}, got {pca}") if info is None: if "eig" not in noise_cov: raise ValueError( - "info can only be None if the noise cov has " - "already been prepared with prepare_noise_cov" + "info can only be None if the noise cov has already been prepared with " + "prepare_noise_cov" ) ch_names = deepcopy(noise_cov["names"]) else: @@ -2488,7 +2484,7 @@ def _write_cov(fid, cov): @verbose def _ensure_cov(cov, name="cov", *, verbose=None): _validate_type(cov, ("path-like", Covariance), name) - logger.info("Noise covariance : %s" % (cov,)) + logger.info(f"Noise covariance : {cov}") if not isinstance(cov, Covariance): cov = read_cov(cov, verbose=_verbose_safe_false()) return cov diff --git a/mne/cuda.py b/mne/cuda.py index 7d7634a6e4e..be645506de3 100644 --- a/mne/cuda.py +++ b/mne/cuda.py @@ -120,7 +120,7 @@ def _set_cuda_device(device_id, verbose=None): import cupy cupy.cuda.Device(device_id).use() - logger.info("Now using CUDA device {}".format(device_id)) + logger.info(f"Now using CUDA device {device_id}") ############################################################################### diff --git a/mne/datasets/sleep_physionet/_utils.py b/mne/datasets/sleep_physionet/_utils.py index 8e4506a1be5..acff836366c 100644 --- a/mne/datasets/sleep_physionet/_utils.py +++ b/mne/datasets/sleep_physionet/_utils.py @@ -132,9 +132,7 @@ def _update_sleep_temazepam_records(fname=TEMAZEPAM_SLEEP_RECORDS): "level_3": "drug", } ) - data["id"] = [ - "ST7{:02d}{:1d}".format(s, n) for s, n in zip(data.subject, data["night nr"]) - ] + data["id"] = [f"ST7{s:02d}{n:1d}" for s, n in zip(data.subject, data["night nr"])] data = pd.merge(sha1_df, data, how="outer", on="id") data["record type"] = ( @@ -198,9 +196,7 @@ def _update_sleep_age_records(fname=AGE_SLEEP_RECORDS): {1: "female", 2: "male"} ) - data["id"] = [ - "SC4{:02d}{:1d}".format(s, n) for s, n in zip(data.subject, data.night) - ] + data["id"] = [f"SC4{s:02d}{n:1d}" for s, n in zip(data.subject, data.night)] data = data.set_index("id").join(sha1_df.set_index("id")).dropna() diff --git a/mne/datasets/tests/test_datasets.py b/mne/datasets/tests/test_datasets.py index b84b3a2f367..d3a361786d7 100644 --- a/mne/datasets/tests/test_datasets.py +++ b/mne/datasets/tests/test_datasets.py @@ -60,7 +60,7 @@ def test_datasets_basic(tmp_path, monkeypatch): else: assert dataset.get_version() is None assert not datasets.has_dataset(dname) - print("%s: %s" % (dname, datasets.has_dataset(dname))) + print(f"{dname}: {datasets.has_dataset(dname)}") tempdir = str(tmp_path) # Explicitly test one that isn't preset (given the config) monkeypatch.setenv("MNE_DATASETS_SAMPLE_PATH", tempdir) diff --git a/mne/datasets/utils.py b/mne/datasets/utils.py index eddee6f5684..d4a8f4af459 100644 --- a/mne/datasets/utils.py +++ b/mne/datasets/utils.py @@ -87,7 +87,7 @@ def _dataset_version(path, name): """Get the version of the dataset.""" ver_fname = op.join(path, "version.txt") if op.exists(ver_fname): - with open(ver_fname, "r") as fid: + with open(ver_fname) as fid: version = fid.readline().strip() # version is on first line else: logger.debug(f"Version file missing: {ver_fname}") @@ -147,8 +147,8 @@ def _do_path_update(path, update_path, key, name): answer = "y" else: msg = ( - "Do you want to set the path:\n %s\nas the default " - "%s dataset path in the mne-python config [y]/n? " % (path, name) + f"Do you want to set the path:\n {path}\nas the default {name} " + "dataset path in the mne-python config [y]/n? " ) answer = _safe_input(msg, alt="pass update_path=True") if answer.lower() == "n": @@ -747,7 +747,7 @@ def fetch_hcp_mmp_parcellation( assert used.all() assert len(labels_out) == 46 for hemi, side in (("lh", "left"), ("rh", "right")): - table_name = "./%s.fsaverage164.label.gii" % (side,) + table_name = f"./{side}.fsaverage164.label.gii" write_labels_to_annot( labels_out, "fsaverage", @@ -762,7 +762,7 @@ def fetch_hcp_mmp_parcellation( def _manifest_check_download(manifest_path, destination, url, hash_): import pooch - with open(manifest_path, "r") as fid: + with open(manifest_path) as fid: names = [name.strip() for name in fid.readlines()] manifest_path = op.basename(manifest_path) need = list() @@ -787,18 +787,17 @@ def _manifest_check_download(manifest_path, destination, url, hash_): fname=op.basename(fname_path), ) - logger.info("Extracting missing file%s" % (_pl(need),)) + logger.info(f"Extracting missing file{_pl(need)}") with zipfile.ZipFile(fname_path, "r") as ff: members = set(f for f in ff.namelist() if not f.endswith("/")) missing = sorted(members.symmetric_difference(set(names))) if len(missing): raise RuntimeError( - "Zip file did not have correct names:" - "\n%s" % ("\n".join(missing)) + "Zip file did not have correct names:\n{'\n'.join(missing)}" ) for name in need: ff.extract(name, path=destination) - logger.info("Successfully extracted %d file%s" % (len(need), _pl(need))) + logger.info(f"Successfully extracted {len(need)} file{_pl(need)}") def _log_time_size(t0, sz): diff --git a/mne/decoding/base.py b/mne/decoding/base.py index 8caea981194..e44fcd13f29 100644 --- a/mne/decoding/base.py +++ b/mne/decoding/base.py @@ -109,13 +109,12 @@ def fit(self, X, y, **fit_params): X, y = np.asarray(X), np.asarray(y) if X.ndim != 2: raise ValueError( - "LinearModel only accepts 2-dimensional X, got " - "%s instead." % (X.shape,) + f"LinearModel only accepts 2-dimensional X, got {X.shape} instead." ) if y.ndim > 2: raise ValueError( - "LinearModel only accepts up to 2-dimensional y, " - "got %s instead." % (y.shape,) + f"LinearModel only accepts up to 2-dimensional y, got {y.shape} " + "instead." ) # fit the Model @@ -267,9 +266,7 @@ def get_coef(estimator, attr="filters_", inverse_transform=False): coef = coef[np.newaxis] # fake a sample dimension squeeze_first_dim = True elif not hasattr(est, attr): - raise ValueError( - "This estimator does not have a %s attribute:\n%s" % (attr, est) - ) + raise ValueError(f"This estimator does not have a {attr} attribute:\n{est}") else: coef = getattr(est, attr) @@ -281,7 +278,7 @@ def get_coef(estimator, attr="filters_", inverse_transform=False): if inverse_transform: if not hasattr(estimator, "steps") and not hasattr(est, "estimators_"): raise ValueError( - "inverse_transform can only be applied onto " "pipeline estimators." + "inverse_transform can only be applied onto pipeline estimators." ) # The inverse_transform parameter will call this method on any # estimator contained in the pipeline, in reverse order. @@ -458,15 +455,13 @@ def _fit_and_score( if return_train_score: train_score = error_score warn( - "Classifier fit failed. The score on this train-test" - " partition for these parameters will be set to %f. " - "Details: \n%r" % (error_score, e) + "Classifier fit failed. The score on this train-test partition for " + f"these parameters will be set to {error_score}. Details: \n{e!r}" ) else: raise ValueError( - "error_score must be the string 'raise' or a" - " numeric value. (Hint: if using 'raise', please" - " make sure that it has been spelled correctly.)" + "error_score must be the string 'raise' or a numeric value. (Hint: if " + "using 'raise', please make sure that it has been spelled correctly.)" ) else: diff --git a/mne/decoding/csp.py b/mne/decoding/csp.py index 1656db50b36..ac3983e4617 100644 --- a/mne/decoding/csp.py +++ b/mne/decoding/csp.py @@ -181,10 +181,8 @@ def fit(self, X, y): raise ValueError("n_classes must be >= 2.") if n_classes > 2 and self.component_order == "alternate": raise ValueError( - "component_order='alternate' requires two " - "classes, but data contains {} classes; use " - "component_order='mutual_info' " - "instead.".format(n_classes) + "component_order='alternate' requires two classes, but data contains " + f"{n_classes} classes; use component_order='mutual_info' instead." ) covs, sample_weights = self._compute_covariance_matrices(X, y) @@ -773,7 +771,7 @@ def __init__( rank=None, ): """Init of SPoC.""" - super(SPoC, self).__init__( + super().__init__( n_components=n_components, reg=reg, log=log, @@ -873,4 +871,4 @@ def transform(self, X): If self.transform_into == 'csp_space' then returns the data in CSP space and shape is (n_epochs, n_sources, n_times). """ - return super(SPoC, self).transform(X) + return super().transform(X) diff --git a/mne/decoding/mixin.py b/mne/decoding/mixin.py index 2a0adee19eb..3916c156873 100644 --- a/mne/decoding/mixin.py +++ b/mne/decoding/mixin.py @@ -69,9 +69,8 @@ def set_params(self, **params): name, sub_name = split if name not in valid_params: raise ValueError( - "Invalid parameter %s for estimator %s. " - "Check the list of available parameters " - "with `estimator.get_params().keys()`." % (name, self) + f"Invalid parameter {name} for estimator {self}. Check the list" + " of available parameters with `estimator.get_params().keys()`." ) sub_object = valid_params[name] sub_object.set_params(**{sub_name: value}) @@ -79,10 +78,9 @@ def set_params(self, **params): # simple objects case if key not in valid_params: raise ValueError( - "Invalid parameter %s for estimator %s. " - "Check the list of available parameters " - "with `estimator.get_params().keys()`." - % (key, self.__class__.__name__) + f"Invalid parameter {key} for estimator " + f"{self.__class__.__name__}. Check the list of available " + "parameters with `estimator.get_params().keys()`." ) setattr(self, key, value) return self diff --git a/mne/decoding/receptive_field.py b/mne/decoding/receptive_field.py index c3c07cfa42f..fdf7dea9211 100644 --- a/mne/decoding/receptive_field.py +++ b/mne/decoding/receptive_field.py @@ -134,24 +134,24 @@ def _more_tags(self): return {"no_validation": True} def __repr__(self): # noqa: D105 - s = "tmin, tmax : (%.3f, %.3f), " % (self.tmin, self.tmax) + s = f"tmin, tmax : ({self.tmin:.3f}, {self.tmax:.3f}), " estimator = self.estimator if not isinstance(estimator, str): estimator = type(self.estimator) - s += "estimator : %s, " % (estimator,) + s += f"estimator : {estimator}, " if hasattr(self, "coef_"): if self.feature_names is not None: feats = self.feature_names if len(feats) == 1: - s += "feature: %s, " % feats[0] + s += f"feature: {feats[0]}, " else: - s += "features : [%s, ..., %s], " % (feats[0], feats[-1]) + s += f"features : [{feats[0]}, ..., {feats[-1]}], " s += "fit: True" else: s += "fit: False" if hasattr(self, "scores_"): - s += "scored (%s)" % self.scoring - return "" % s + s += f"scored ({self.scoring})" + return f"" def _delay_and_reshape(self, X, y=None): """Delay and reshape the variables.""" @@ -187,17 +187,14 @@ def fit(self, X, y): """ if self.scoring not in _SCORERS.keys(): raise ValueError( - "scoring must be one of %s, got" - "%s " % (sorted(_SCORERS.keys()), self.scoring) + f"scoring must be one of {sorted(_SCORERS.keys())}, got {self.scoring} " ) from sklearn.base import clone, is_regressor X, y, _, self._y_dim = self._check_dimensions(X, y) if self.tmin > self.tmax: - raise ValueError( - "tmin (%s) must be at most tmax (%s)" % (self.tmin, self.tmax) - ) + raise ValueError(f"tmin ({self.tmin}) must be at most tmax ({self.tmax})") # Initialize delays self.delays_ = _times_to_delays(self.tmin, self.tmax, self.sfreq) @@ -225,17 +222,16 @@ def fit(self, X, y): and estimator.fit_intercept != self.fit_intercept ): raise ValueError( - "Estimator fit_intercept (%s) != initialization " - "fit_intercept (%s), initialize ReceptiveField with the " - "same fit_intercept value or use fit_intercept=None" - % (estimator.fit_intercept, self.fit_intercept) + f"Estimator fit_intercept ({estimator.fit_intercept}) != " + f"initialization fit_intercept ({self.fit_intercept}), initialize " + "ReceptiveField with the same fit_intercept value or use " + "fit_intercept=None" ) self.fit_intercept_ = estimator.fit_intercept else: raise ValueError( - "`estimator` must be a float or an instance" - " of `BaseEstimator`," - " got type %s." % type(self.estimator) + "`estimator` must be a float or an instance of `BaseEstimator`, got " + f"type {self.estimator}." ) self.estimator_ = estimator del estimator @@ -249,8 +245,8 @@ def fit(self, X, y): # Update feature names if we have none if (self.feature_names is not None) and (len(self.feature_names) != n_feats): raise ValueError( - "n_features in X does not match feature names " - "(%s != %s)" % (n_feats, len(self.feature_names)) + f"n_features in X does not match feature names ({n_feats} != " + f"{len(self.feature_names)})" ) # Create input features @@ -377,8 +373,8 @@ def _check_dimensions(self, X, y, predict=False): y = y[:, np.newaxis, :] # epochs else: raise ValueError( - "y must be shape (n_times[, n_epochs]" - "[,n_outputs], got %s" % (y.shape,) + "y must be shape (n_times[, n_epochs][,n_outputs], got " + f"{y.shape}" ) elif X.ndim == 3: if y is not None: @@ -390,24 +386,22 @@ def _check_dimensions(self, X, y, predict=False): ) else: raise ValueError( - "X must be shape (n_times[, n_epochs]," - " n_features), got %s" % (X.shape,) + f"X must be shape (n_times[, n_epochs], n_features), got {X.shape}" ) if y is not None: if X.shape[0] != y.shape[0]: raise ValueError( - "X and y do not have the same n_times\n" - "%s != %s" % (X.shape[0], y.shape[0]) + f"X and y do not have the same n_times\n{X.shape[0]} != " + f"{y.shape[0]}" ) if X.shape[1] != y.shape[1]: raise ValueError( - "X and y do not have the same n_epochs\n" - "%s != %s" % (X.shape[1], y.shape[1]) + f"X and y do not have the same n_epochs\n{X.shape[1]} != " + f"{y.shape[1]}" ) if predict and y.shape[-1] != len(self.estimator_.coef_): raise ValueError( - "Number of outputs does not match" - " estimator coefficients dimensions" + "Number of outputs does not match estimator coefficients dimensions" ) return X, y, X_dim, y_dim @@ -517,7 +511,7 @@ def _corr_score(y_true, y, multioutput=None): for this_y in (y_true, y): if this_y.ndim != 2: raise ValueError( - "inputs must be shape (samples, outputs), got %s" % (this_y.shape,) + f"inputs must be shape (samples, outputs), got {this_y.shape}" ) return np.array([pearsonr(y_true[:, ii], y[:, ii])[0] for ii in range(y.shape[-1])]) diff --git a/mne/decoding/search_light.py b/mne/decoding/search_light.py index 873efe89465..369efd7bba3 100644 --- a/mne/decoding/search_light.py +++ b/mne/decoding/search_light.py @@ -63,7 +63,7 @@ def _estimator_type(self): return getattr(self.base_estimator, "_estimator_type", None) def __repr__(self): # noqa: D105 - repr_str = "<" + super(SlidingEstimator, self).__repr__() + repr_str = "<" + super().__repr__() if hasattr(self, "estimators_"): repr_str = repr_str[:-1] repr_str += ", fitted with %i estimators" % len(self.estimators_) @@ -320,9 +320,8 @@ def score(self, X, y): def classes_(self): if not hasattr(self.estimators_[0], "classes_"): raise AttributeError( - "classes_ attribute available only if " - "base_estimator has it, and estimator %s does" - " not" % (self.estimators_[0],) + "classes_ attribute available only if base_estimator has it, and " + f"estimator {self.estimators_[0]} does not" ) return self.estimators_[0].classes_ @@ -466,7 +465,7 @@ class GeneralizingEstimator(SlidingEstimator): """ def __repr__(self): # noqa: D105 - repr_str = super(GeneralizingEstimator, self).__repr__() + repr_str = super().__repr__() if hasattr(self, "estimators_"): repr_str = repr_str[:-1] repr_str += ", fitted with %i estimators>" % len(self.estimators_) diff --git a/mne/decoding/ssd.py b/mne/decoding/ssd.py index 961444b122c..64e84cdbde9 100644 --- a/mne/decoding/ssd.py +++ b/mne/decoding/ssd.py @@ -112,8 +112,7 @@ def __init__( key = ("signal", "noise")[dd] if param + "_freq" not in dicts[key]: raise ValueError( - "%s must be defined in filter parameters for %s" - % (param + "_freq", key) + f"{param + '_freq'} must be defined in filter parameters for {key}" ) val = dicts[key][param + "_freq"] if not isinstance(val, (int, float)): diff --git a/mne/decoding/tests/test_receptive_field.py b/mne/decoding/tests/test_receptive_field.py index dc0d823dd32..8585aa0170e 100644 --- a/mne/decoding/tests/test_receptive_field.py +++ b/mne/decoding/tests/test_receptive_field.py @@ -73,7 +73,7 @@ def test_compute_reg_neighbors(): reg_direct, reg_csgraph, atol=1e-7, - err_msg="%s: %s" % (reg_type, (n_ch_x, n_delays)), + err_msg=f"{reg_type}: {(n_ch_x, n_delays)}", ) @@ -155,7 +155,7 @@ def test_time_delay(): del_zero = int(round(-tmin * isfreq)) for ii in range(-2, 3): idx = del_zero + ii - err_msg = "[%s,%s] (%s): %s %s" % (tmin, tmax, isfreq, ii, idx) + err_msg = f"[{tmin},{tmax}] ({isfreq}): {ii} {idx}" if 0 <= idx < X_delayed.shape[-1]: if ii == 0: assert_array_equal(X_delayed[:, :, idx], X, err_msg=err_msg) diff --git a/mne/decoding/tests/test_search_light.py b/mne/decoding/tests/test_search_light.py index 6b445972d5f..296c4ba4bea 100644 --- a/mne/decoding/tests/test_search_light.py +++ b/mne/decoding/tests/test_search_light.py @@ -146,7 +146,7 @@ def test_search_light(): # pipeline class _LogRegTransformer(LogisticRegression): def transform(self, X): - return super(_LogRegTransformer, self).predict_proba(X)[..., 1] + return super().predict_proba(X)[..., 1] logreg_transformer = _LogRegTransformer( random_state=0, multi_class="ovr", solver="liblinear" diff --git a/mne/decoding/time_delaying_ridge.py b/mne/decoding/time_delaying_ridge.py index b89b4e98ac2..3ef2403bf34 100644 --- a/mne/decoding/time_delaying_ridge.py +++ b/mne/decoding/time_delaying_ridge.py @@ -157,12 +157,10 @@ def _compute_reg_neighbors(n_ch_x, n_delays, reg_type, method="direct", normed=F if isinstance(reg_type, str): reg_type = (reg_type,) * 2 if len(reg_type) != 2: - raise ValueError("reg_type must have two elements, got %s" % (len(reg_type),)) + raise ValueError(f"reg_type must have two elements, got {len(reg_type)}") for r in reg_type: if r not in known_types: - raise ValueError( - "reg_type entries must be one of %s, got %s" % (known_types, r) - ) + raise ValueError(f"reg_type entries must be one of {known_types}, got {r}") reg_time = reg_type[0] == "laplacian" and n_delays > 1 reg_chs = reg_type[1] == "laplacian" and n_ch_x > 1 if not reg_time and not reg_chs: @@ -290,7 +288,7 @@ def __init__( edge_correction=True, ): if tmin > tmax: - raise ValueError("tmin must be <= tmax, got %s and %s" % (tmin, tmax)) + raise ValueError(f"tmin must be <= tmax, got {tmin} and {tmax}") self.tmin = float(tmin) self.tmax = float(tmax) self.sfreq = float(sfreq) diff --git a/mne/decoding/transformer.py b/mne/decoding/transformer.py index 184bfca8f53..3ba47b99700 100644 --- a/mne/decoding/transformer.py +++ b/mne/decoding/transformer.py @@ -331,7 +331,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 " "%s" % (X.shape,) + "X should be of 2 or 3 dimensions but has shape " f"{X.shape}" ) return X.reshape(X.shape[:-1] + self.features_shape_) diff --git a/mne/dipole.py b/mne/dipole.py index 42e27438b4a..5c1d6423c91 100644 --- a/mne/dipole.py +++ b/mne/dipole.py @@ -626,7 +626,7 @@ def _read_dipole_text(fname): # There is a bug in older np.loadtxt regarding skipping fields, # so just read the data ourselves (need to get name and header anyway) data = list() - with open(fname, "r") as fid: + with open(fname) as fid: for line in fid: if not (line.startswith("%") or line.startswith("#")): need_header = False @@ -642,8 +642,8 @@ def _read_dipole_text(fname): data = np.atleast_2d(np.array(data, float)) if def_line is None: raise OSError( - "Dipole text file is missing field definition " - "comment, cannot parse %s" % (fname,) + "Dipole text file is missing field definition comment, cannot parse " + f"{fname}" ) # actually parse the fields def_line = def_line.lstrip("%").lstrip("#").strip() @@ -690,20 +690,20 @@ def _read_dipole_text(fname): missing_fields = sorted(set(required_fields) - set(fields)) if len(missing_fields) > 0: raise RuntimeError( - "Could not find necessary fields in header: %s" % (missing_fields,) + f"Could not find necessary fields in header: {missing_fields}" ) handled_fields = set(required_fields) | set(optional_fields) assert len(handled_fields) == len(required_fields) + len(optional_fields) ignored_fields = sorted(set(fields) - set(handled_fields) - {"end/ms"}) if len(ignored_fields) > 0: - warn("Ignoring extra fields in dipole file: %s" % (ignored_fields,)) + warn(f"Ignoring extra fields in dipole file: {ignored_fields}") if len(fields) != data.shape[1]: raise OSError( - "More data fields (%s) found than data columns (%s): %s" - % (len(fields), data.shape[1], fields) + f"More data fields ({len(fields)}) found than data columns ({data.shape[1]}" + f"): {fields}" ) - logger.info("%d dipole(s) found" % len(data)) + logger.info(f"{len(data)} dipole(s) found") if "end/ms" in fields: if np.diff( @@ -776,7 +776,7 @@ def _write_dipole_text(fname, dip): # NB CoordinateSystem is hard-coded as Head here with open(fname, "wb") as fid: - fid.write('# CoordinateSystem "Head"\n'.encode("utf-8")) + fid.write(b'# CoordinateSystem "Head"\n') fid.write((header + "\n").encode("utf-8")) np.savetxt(fid, out, fmt=fmt) if dip.name is not None: @@ -888,13 +888,15 @@ def _make_guesses(surf, grid, exclude, mindist, n_jobs=None, verbose=None): """Make a guess space inside a sphere or BEM surface.""" if "rr" in surf: logger.info( - "Guess surface (%s) is in %s coordinates" - % (_bem_surf_name[surf["id"]], _coord_frame_name(surf["coord_frame"])) + "Guess surface ({}) is in {} coordinates".format( + _bem_surf_name[surf["id"]], _coord_frame_name(surf["coord_frame"]) + ) ) else: logger.info( - "Making a spherical guess space with radius %7.1f mm..." - % (1000 * surf["R"]) + "Making a spherical guess space with radius {:7.1f} mm...".format( + 1000 * surf["R"] + ) ) logger.info("Filtering (grid = %6.f mm)..." % (1000 * grid)) src = _make_volume_source_space( @@ -1510,9 +1512,8 @@ def fit_dipole( r0 = apply_trans(mri_head_t["trans"], r0[np.newaxis, :])[0] inner_skull["r0"] = r0 logger.info( - "Head origin : " - "%6.1f %6.1f %6.1f mm rad = %6.1f mm." - % (1000 * r0[0], 1000 * r0[1], 1000 * r0[2], 1000 * R) + f"Head origin : {1000 * r0[0]:6.1f} {1000 * r0[1]:6.1f} " + f"{1000 * r0[2]:6.1f} mm rad = {1000 * R:6.1f} mm." ) del R, r0 else: @@ -1536,8 +1537,8 @@ def fit_dipole( R = np.min(np.sqrt(np.sum(R * R, axis=1))) # use dist to sensors kind = "max_rad" logger.info( - "Sphere model : origin at (% 7.2f % 7.2f % 7.2f) mm, " - "%s = %6.1f mm" % (1000 * r0[0], 1000 * r0[1], 1000 * r0[2], kind, R) + "Sphere model : origin at ({: 7.2f} {: 7.2f} {: 7.2f}) mm, " + "{} = {:6.1f} mm".format(1000 * r0[0], 1000 * r0[1], 1000 * r0[2], kind, R) ) inner_skull = dict(R=R, r0=r0) # NB sphere model defined in head frame del R, r0 @@ -1548,19 +1549,23 @@ def fit_dipole( pos = np.array(pos, float) if pos.shape != (3,): raise ValueError( - "pos must be None or a 3-element array-like," " got %s" % (pos,) + "pos must be None or a 3-element array-like," f" got {pos}" ) - logger.info("Fixed position : %6.1f %6.1f %6.1f mm" % tuple(1000 * pos)) + logger.info( + "Fixed position : {:6.1f} {:6.1f} {:6.1f} mm".format(*tuple(1000 * pos)) + ) if ori is not None: ori = np.array(ori, float) if ori.shape != (3,): raise ValueError( - "oris must be None or a 3-element array-like," " got %s" % (ori,) + "oris must be None or a 3-element array-like," f" got {ori}" ) norm = np.sqrt(np.sum(ori * ori)) if not np.isclose(norm, 1): - raise ValueError("ori must be a unit vector, got length %s" % (norm,)) - logger.info("Fixed orientation : %6.4f %6.4f %6.4f mm" % tuple(ori)) + raise ValueError(f"ori must be a unit vector, got length {norm}") + logger.info( + "Fixed orientation : {:6.4f} {:6.4f} {:6.4f} mm".format(*tuple(ori)) + ) else: logger.info("Free orientation : ") fit_n_jobs = 1 # only use 1 job to do the guess fitting @@ -1572,11 +1577,11 @@ def fit_dipole( guess_mindist = max(0.005, min_dist_to_inner_skull) guess_exclude = 0.02 - logger.info("Guess grid : %6.1f mm" % (1000 * guess_grid,)) + logger.info(f"Guess grid : {1000 * guess_grid:6.1f} mm") if guess_mindist > 0.0: - logger.info("Guess mindist : %6.1f mm" % (1000 * guess_mindist,)) + logger.info(f"Guess mindist : {1000 * guess_mindist:6.1f} mm") if guess_exclude > 0: - logger.info("Guess exclude : %6.1f mm" % (1000 * guess_exclude,)) + logger.info(f"Guess exclude : {1000 * guess_exclude:6.1f} mm") logger.info(f"Using {accuracy} MEG coil definitions.") fit_n_jobs = n_jobs cov = _ensure_cov(cov) @@ -1584,7 +1589,7 @@ def fit_dipole( _print_coord_trans(mri_head_t) _print_coord_trans(info["dev_head_t"]) - logger.info("%d bad channels total" % len(info["bads"])) + logger.info(f"{len(info['bads'])} bad channels total") # Forward model setup (setup_forward_model from setup.c) ch_types = evoked.get_channel_types() @@ -1645,8 +1650,8 @@ def fit_dipole( ) if check <= 0: raise ValueError( - "fixed position is %0.1fmm outside the inner " - "skull boundary" % (-1000 * check,) + f"fixed position is {-1000 * check:0.1f}mm outside the inner skull " + "boundary" ) # C code computes guesses w/sphere model for speed, don't bother here diff --git a/mne/epochs.py b/mne/epochs.py index 53cab1c81f0..d1f76609356 100644 --- a/mne/epochs.py +++ b/mne/epochs.py @@ -508,8 +508,8 @@ def __init__( selection = np.array(selection, int) if selection.shape != (len(selected),): raise ValueError( - "selection must be shape %s got shape %s" - % (selected.shape, selection.shape) + f"selection must be shape {selected.shape} got shape " + f"{selection.shape}" ) self.selection = selection if drop_log is None: @@ -665,7 +665,7 @@ def __init__( # do the rest valid_proj = [True, "delayed", False] if proj not in valid_proj: - raise ValueError('"proj" must be one of %s, not %s' % (valid_proj, proj)) + raise ValueError(f'"proj" must be one of {valid_proj}, not {proj}') if proj == "delayed": self._do_delayed_proj = True logger.info("Entering delayed SSP mode.") @@ -696,7 +696,7 @@ def _check_consistency(self): if hasattr(self, "events"): assert len(self.selection) == len(self.events) assert len(self.drop_log) >= len(self.events) - assert len(self.selection) == sum((len(dl) == 0 for dl in self.drop_log)) + assert len(self.selection) == sum(len(dl) == 0 for dl in self.drop_log) assert hasattr(self, "_times_readonly") assert not self.times.flags["WRITEABLE"] assert isinstance(self.drop_log, tuple) @@ -799,7 +799,7 @@ def _reject_setup(self, reject, flat): ) bads = set(rej.keys()) - set(idx.keys()) if len(bads) > 0: - raise KeyError("Unknown channel types found in %s: %s" % (kind, bads)) + raise KeyError(f"Unknown channel types found in {kind}: {bads}") for key in idx.keys(): # don't throw an error if rejection/flat would do nothing @@ -810,17 +810,15 @@ def _reject_setup(self, reject, flat): # self.allow_missing_reject_keys check to allow users to # provide keys that don't exist in data raise ValueError( - "No %s channel found. Cannot reject based on " - "%s." % (key.upper(), key.upper()) + f"No {key.upper()} channel found. Cannot reject based on " + f"{key.upper()}." ) # check for invalid values for rej, kind in zip((reject, flat), ("Rejection", "Flat")): for key, val in rej.items(): if val is None or val < 0: - raise ValueError( - '%s value must be a number >= 0, not "%s"' % (kind, val) - ) + raise ValueError(f'{kind} value must be a number >= 0, not "{val}"') # now check to see if our rejection and flat are getting more # restrictive @@ -1984,9 +1982,9 @@ def filename(self): def __repr__(self): """Build string representation.""" - s = " %s events " % len(self.events) + s = f" {len(self.events)} events " s += "(all good)" if self._bad_dropped else "(good & bad)" - s += ", %g – %g s" % (self.tmin, self.tmax) + s += f", {self.tmin:g} – {self.tmax:g} s" s += ", baseline " if self.baseline is None: s += "off" @@ -2000,12 +1998,12 @@ def __repr__(self): ): s += " (baseline period was cropped after baseline correction)" - s += ", ~%s" % (sizeof_fmt(self._size),) - s += ", data%s loaded" % ("" if self.preload else " not") + 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 counts = [ - "%r: %i" % (k, sum(self.events[:, 2] == v)) + f"{k!r}: {sum(self.events[:, 2] == v)}" for k, v in list(self.event_id.items())[:max_events] ] if len(self.event_id) > 0: @@ -2015,7 +2013,7 @@ def __repr__(self): s += f"\n and {not_shown_events} more events ..." class_name = self.__class__.__name__ class_name = "Epochs" if class_name == "BaseEpochs" else class_name - return "<%s | %s>" % (class_name, s) + return f"<{class_name} | {s}>" @repr_html def _repr_html_(self): @@ -2400,7 +2398,7 @@ def equalize_event_counts(self, event_ids=None, method="mintime"): # 3. do this for every input event_ids = [ [ - k for k in ids if all((tag in k.split("/") for tag in id_)) + k for k in ids if all(tag in k.split("/") for tag in id_) ] # ids matching all tags if all(id__ not in ids for id__ in id_) else id_ # straight pass for non-tag inputs @@ -3298,7 +3296,7 @@ def __init__( ) # call BaseEpochs constructor - super(Epochs, self).__init__( + super().__init__( info, None, events, @@ -3467,7 +3465,7 @@ def __init__( info = info.copy() # do not modify original info tmax = (data.shape[2] - 1) / info["sfreq"] + tmin - super(EpochsArray, self).__init__( + super().__init__( info, data, events, @@ -3632,7 +3630,7 @@ def _minimize_time_diff(t_shorter, t_longer): idx = np.argmin(np.abs(t_longer - t_shorter)) keep[idx] = True return keep - scores = np.ones((len(t_longer))) + scores = np.ones(len(t_longer)) x1 = np.arange(len(t_shorter)) # The first set of keep masks to test kwargs = dict(copy=False, bounds_error=False, assume_sorted=True) @@ -3693,8 +3691,7 @@ def _is_good( bad_names = [ch_names[idx[i]] for i in idx_deltas] if not has_printed: logger.info( - " Rejecting %s epoch based on %s : " - "%s" % (t, name, bad_names) + f" Rejecting {t} epoch based on {name} : {bad_names}" ) has_printed = True if not full_report: @@ -3810,12 +3807,12 @@ def _read_one_epoch_file(f, tree, preload): n_samp = last - first + 1 logger.info(" Found the data of interest:") logger.info( - " t = %10.2f ... %10.2f ms" - % (1000 * first / info["sfreq"], 1000 * last / info["sfreq"]) + f" t = {1000 * first / info['sfreq']:10.2f} ... " + f"{1000 * last / info['sfreq']:10.2f} ms" ) if info["comps"] is not None: logger.info( - " %d CTF compensation matrices available" % len(info["comps"]) + f" {len(info['comps'])} CTF compensation matrices available" ) # Inspect the data @@ -4081,7 +4078,7 @@ def __init__(self, fname, proj=True, preload=True, verbose=None): # call BaseEpochs constructor # again, ensure we're retaining the baseline period originally loaded # from disk without trying to re-apply baseline correction - super(EpochsFIF, self).__init__( + super().__init__( info, data, events, @@ -4204,9 +4201,7 @@ def _concatenate_epochs( ): """Auxiliary function for concatenating epochs.""" if not isinstance(epochs_list, (list, tuple)): - raise TypeError( - "epochs_list must be a list or tuple, got %s" % (type(epochs_list),) - ) + raise TypeError(f"epochs_list must be a list or tuple, got {type(epochs_list)}") # to make warning messages only occur once during concatenation warned = False @@ -4214,8 +4209,7 @@ def _concatenate_epochs( for ei, epochs in enumerate(epochs_list): if not isinstance(epochs, BaseEpochs): raise TypeError( - "epochs_list[%d] must be an instance of Epochs, " - "got %s" % (ei, type(epochs)) + f"epochs_list[{ei}] must be an instance of Epochs, got {type(epochs)}" ) if ( @@ -4225,8 +4219,8 @@ def _concatenate_epochs( ): warned = True warn( - "Concatenation of Annotations within Epochs is not supported " - "yet. All annotations will be dropped." + "Concatenation of Annotations within Epochs is not supported yet. All " + "annotations will be dropped." ) # create a copy, so that the Annotations are not modified in place @@ -4518,9 +4512,7 @@ def average_movements( from .chpi import head_pos_to_trans_rot_t if not isinstance(epochs, BaseEpochs): - raise TypeError( - "epochs must be an instance of Epochs, not %s" % (type(epochs),) - ) + raise TypeError(f"epochs must be an instance of Epochs, not {type(epochs)}") orig_sfreq = epochs.info["sfreq"] if orig_sfreq is None else orig_sfreq orig_sfreq = float(orig_sfreq) if isinstance(head_pos, np.ndarray): @@ -4531,7 +4523,7 @@ def average_movements( origin = _check_origin(origin, epochs.info, "head") recon_trans = _check_destination(destination, epochs.info, True) - logger.info("Aligning and averaging up to %s epochs" % (len(epochs.events))) + logger.info(f"Aligning and averaging up to {len(epochs.events)} epochs") if not np.array_equal(epochs.events[:, 0], np.unique(epochs.events[:, 0])): raise RuntimeError("Epochs must have monotonically increasing events") info_to = epochs.info.copy() @@ -4573,12 +4565,12 @@ def average_movements( loc_str = ", ".join("%0.1f" % tr for tr in (trans[:3, 3] * 1000)) if last_trans is None or not np.allclose(last_trans, trans): logger.info( - " Processing epoch %s (device location: %s mm)" % (ei + 1, loc_str) + f" Processing epoch {ei + 1} (device location: {loc_str} mm)" ) reuse = False last_trans = trans else: - logger.info(" Processing epoch %s (device location: same)" % (ei + 1,)) + logger.info(f" Processing epoch {ei + 1} (device location: same)") reuse = True epoch = epoch.copy() # because we operate inplace if not reuse: @@ -4627,7 +4619,7 @@ def average_movements( data, info_to, picks, n_events=count, kind="average", comment=epochs._name ) _remove_meg_projs_comps(evoked, ignore_ref) - logger.info("Created Evoked dataset from %s epochs" % (count,)) + logger.info(f"Created Evoked dataset from {count} epochs") return (evoked, mapping) if return_mapping else evoked diff --git a/mne/event.py b/mne/event.py index fca229f2884..6bb8a1f604a 100644 --- a/mne/event.py +++ b/mne/event.py @@ -321,9 +321,7 @@ def read_events( event_list = _mask_trigs(event_list, mask, mask_type) masked_len = event_list.shape[0] if masked_len < unmasked_len: - warn( - "{} of {} events masked".format(unmasked_len - masked_len, unmasked_len) - ) + warn(f"{unmasked_len - masked_len} of {unmasked_len} events masked") out = event_list if return_event_id: if event_id is None: @@ -969,7 +967,7 @@ def make_fixed_length_events( duration, overlap = float(duration), float(overlap) if not 0 <= overlap < duration: raise ValueError( - "overlap must be >=0 but < duration (%s), got %s" % (duration, overlap) + f"overlap must be >=0 but < duration ({duration}), got {overlap}" ) start = raw.time_as_index(start, use_rounding=True)[0] diff --git a/mne/evoked.py b/mne/evoked.py index b23a4fc112c..a3f530076ac 100644 --- a/mne/evoked.py +++ b/mne/evoked.py @@ -9,7 +9,7 @@ # Copyright the MNE-Python contributors. from copy import deepcopy -from typing import List, Union +from typing import Union import numpy as np @@ -400,8 +400,8 @@ def __repr__(self): # noqa: D105 comment += "..." else: comment = self.comment - s = "'%s' (%s, N=%s)" % (comment, self.kind, self.nave) - s += ", %0.5g – %0.5g s" % (self.times[0], self.times[-1]) + s = f"'{comment}' ({self.kind}, N={self.nave})" + s += f", {self.times[0]:0.5g} – {self.times[-1]:0.5g} s" s += ", baseline " if self.baseline is None: s += "off" @@ -415,8 +415,8 @@ def __repr__(self): # noqa: D105 ): s += " (baseline period was cropped after baseline correction)" s += ", %s ch" % self.data.shape[0] - s += ", ~%s" % (sizeof_fmt(self._size),) - return "" % s + s += f", ~{sizeof_fmt(self._size)}" + return f"" @repr_html def _repr_html_(self): @@ -1323,14 +1323,14 @@ def __init__( if data.ndim != 2: raise ValueError( - "Data must be a 2D array of shape (n_channels, " - "n_samples), got shape %s" % (data.shape,) + "Data must be a 2D array of shape (n_channels, n_samples), got shape " + f"{data.shape}" ) if len(info["ch_names"]) != np.shape(data)[0]: raise ValueError( - "Info (%s) and data (%s) must have same number " - "of channels." % (len(info["ch_names"]), np.shape(data)[0]) + f"Info ({len(info['ch_names'])}) and data ({np.shape(data)[0]}) must " + "have same number of channels." ) self.data = data @@ -1352,8 +1352,7 @@ def __init__( _validate_type(self.kind, "str", "kind") if self.kind not in _aspect_dict: raise ValueError( - 'unknown kind "%s", should be "average" or ' - '"standard_error"' % (self.kind,) + f'unknown kind "{self.kind}", should be "average" or "standard_error"' ) self._aspect_kind = _aspect_dict[self.kind] @@ -1421,18 +1420,14 @@ def _check_evokeds_ch_names_times(all_evoked): for ii, ev in enumerate(all_evoked[1:]): if ev.ch_names != ch_names: if set(ev.ch_names) != set(ch_names): - raise ValueError( - "%s and %s do not contain the same channels." % (evoked, ev) - ) + raise ValueError(f"{evoked} and {ev} do not contain the same channels.") else: warn("Order of channels differs, reordering channels ...") ev = ev.copy() ev.reorder_channels(ch_names) all_evoked[ii + 1] = ev if not np.max(np.abs(ev.times - evoked.times)) < 1e-7: - raise ValueError( - "%s and %s do not contain the same time instants" % (evoked, ev) - ) + raise ValueError(f"{evoked} and {ev} do not contain the same time instants") return all_evoked @@ -1539,7 +1534,7 @@ def read_evokeds( proj=True, allow_maxshield=False, verbose=None, -) -> Union[List[Evoked], Evoked]: +) -> Union[list[Evoked], Evoked]: """Read evoked dataset(s). Parameters @@ -1661,17 +1656,16 @@ def _read_evoked(fname, condition=None, kind="average", allow_maxshield=False): found_cond = np.where(goods)[0] if len(found_cond) != 1: raise ValueError( - 'condition "%s" (%s) not found, out of ' - "found datasets:\n%s" % (condition, kind, t) + f'condition "{condition}" ({kind}) not found, out of found ' + f"datasets:\n{t}" ) condition = found_cond[0] elif condition is None: if len(evoked_node) > 1: _, _, conditions = _get_entries(fid, evoked_node, allow_maxshield) raise TypeError( - "Evoked file has more than one " - "condition, the condition parameters " - "must be specified from:\n%s" % conditions + "Evoked file has more than one condition, the condition parameters " + f"must be specified from:\n{conditions}" ) else: condition = 0 @@ -1805,19 +1799,18 @@ def _read_evoked(fname, condition=None, kind="average", allow_maxshield=False): del first, last if nsamp is not None and data.shape[1] != nsamp: raise ValueError( - "Incorrect number of samples (%d instead of " - " %d)" % (data.shape[1], nsamp) + f"Incorrect number of samples ({data.shape[1]} instead of {nsamp})" ) logger.info(" Found the data of interest:") logger.info( - " t = %10.2f ... %10.2f ms (%s)" - % (1000 * times[0], 1000 * times[-1], comment) + f" t = {1000 * times[0]:10.2f} ... {1000 * times[-1]:10.2f} ms (" + f"{comment})" ) if info["comps"] is not None: logger.info( - " %d CTF compensation matrices available" % len(info["comps"]) + f" {len(info['comps'])} CTF compensation matrices available" ) - logger.info(" nave = %d - aspect type = %d" % (nave, aspect_kind)) + logger.info(f" nave = {nave} - aspect type = {aspect_kind}") # Calibrate cals = np.array( diff --git a/mne/filter.py b/mne/filter.py index b9bc92aa9ce..99fdf7f3b00 100644 --- a/mne/filter.py +++ b/mne/filter.py @@ -380,8 +380,8 @@ def _overlap_add_filter( logger.debug("FFT block length: %s" % n_fft) if n_fft < min_fft: raise ValueError( - "n_fft is too short, has to be at least " - "2 * len(h) - 1 (%s), got %s" % (min_fft, n_fft) + f"n_fft is too short, has to be at least 2 * len(h) - 1 ({min_fft}), got " + f"{n_fft}" ) # Figure out if we should use CUDA @@ -493,9 +493,9 @@ def _firwin_design(N, freq, gain, window, sfreq): this_N += 1 - this_N % 2 # make it odd if this_N > N: raise ValueError( - "The requested filter length %s is too short " - "for the requested %0.2f Hz transition band, " - "which requires %s samples" % (N, transition * sfreq / 2.0, this_N) + f"The requested filter length {N} is too short for the requested " + f"{transition * sfreq / 2.0:0.2f} Hz transition band, which " + f"requires {this_N} samples" ) # Construct a lowpass this_h = signal.firwin( @@ -568,7 +568,7 @@ def _construct_fir_filter( freq = np.array(freq) / (sfreq / 2.0) if freq[0] != 0 or freq[-1] != 1: raise ValueError( - "freq must start at 0 and end an Nyquist (%s), got %s" % (sfreq / 2.0, freq) + f"freq must start at 0 and end an Nyquist ({sfreq / 2.0}), got {freq}" ) gain = np.array(gain) @@ -587,8 +587,8 @@ def _construct_fir_filter( if att_db < min_att_db: att_freq *= sfreq / 2.0 warn( - "Attenuation at stop frequency %0.2f Hz is only %0.2f dB. " - "Increase filter_length for higher attenuation." % (att_freq, att_db) + f"Attenuation at stop frequency {att_freq:0.2f} Hz is only {att_db:0.2f} " + "dB. Increase filter_length for higher attenuation." ) return h @@ -597,9 +597,7 @@ def _check_zero_phase_length(N, phase, gain_nyq=0): N = int(N) if N % 2 == 0: if phase == "zero": - raise RuntimeError( - 'filter_length must be odd if phase="zero", ' "got %s" % N - ) + raise RuntimeError(f'filter_length must be odd if phase="zero", got {N}') elif phase == "zero-double" and gain_nyq == 1: N += 1 return N @@ -885,16 +883,15 @@ def construct_iir_filter( ftype = iir_params["ftype"] if ftype not in known_filters: raise RuntimeError( - "ftype must be in filter_dict from " - "scipy.signal (e.g., butter, cheby1, etc.) not " - "%s" % ftype + "ftype must be in filter_dict from scipy.signal (e.g., butter, cheby1, " + f"etc.) not {ftype}" ) # use order-based design f_pass = np.atleast_1d(f_pass) if f_pass.ndim > 1: raise ValueError("frequencies must be 1D, got %dD" % f_pass.ndim) - edge_freqs = ", ".join("%0.2f" % (f,) for f in f_pass) + edge_freqs = ", ".join(f"{f:0.2f}" for f in f_pass) Wp = f_pass / (float(sfreq) / 2) # IT will de designed ftype_nice = _ftype_dict.get(ftype, ftype) @@ -968,8 +965,8 @@ def construct_iir_filter( # 2 * 20 here because we do forward-backward filtering if phase in ("zero", "zero-double"): cutoffs *= 2 - cutoffs = ", ".join(["%0.2f" % (c,) for c in cutoffs]) - logger.info("- Cutoff%s at %s Hz: %s dB" % (_pl(f_pass), edge_freqs, cutoffs)) + cutoffs = ", ".join([f"{c:0.2f}" for c in cutoffs]) + logger.info(f"- Cutoff{_pl(f_pass)} at {edge_freqs} Hz: {cutoffs} dB") # now deal with padding if "padlen" not in iir_params: padlen = estimate_ringing_samples(system) @@ -1254,16 +1251,15 @@ def create_filter( # If no data specified, sanity checking will be skipped if data is None: logger.info( - "No data specified. Sanity checks related to the length of" - " the signal relative to the filter order will be" - " skipped." + "No data specified. Sanity checks related to the length of the signal " + "relative to the filter order will be skipped." ) if h_freq is not None: h_freq = np.array(h_freq, float).ravel() if (h_freq > (sfreq / 2.0)).any(): raise ValueError( - "h_freq (%s) must be less than the Nyquist " - "frequency %s" % (h_freq, sfreq / 2.0) + f"h_freq ({h_freq}) must be less than the Nyquist frequency " + f"{sfreq / 2.0}" ) if l_freq is not None: l_freq = np.array(l_freq, float).ravel() @@ -1303,7 +1299,7 @@ def create_filter( gain = [1.0, 1.0] if l_freq is None and h_freq is not None: h_freq = h_freq.item() - logger.info("Setting up low-pass filter at %0.2g Hz" % (h_freq,)) + logger.info(f"Setting up low-pass filter at {h_freq:0.2g} Hz") ( data, sfreq, @@ -1340,7 +1336,7 @@ def create_filter( gain += [0] elif l_freq is not None and h_freq is None: l_freq = l_freq.item() - logger.info("Setting up high-pass filter at %0.2g Hz" % (l_freq,)) + logger.info(f"Setting up high-pass filter at {l_freq:0.2g} Hz") ( data, sfreq, @@ -1379,7 +1375,7 @@ def create_filter( if (l_freq < h_freq).any(): l_freq, h_freq = l_freq.item(), h_freq.item() logger.info( - "Setting up band-pass filter from %0.2g - %0.2g Hz" % (l_freq, h_freq) + f"Setting up band-pass filter from {l_freq:0.2g} - {h_freq:0.2g} Hz" ) ( data, @@ -1431,7 +1427,7 @@ def create_filter( msg = "Setting up band-stop filter" if len(l_freq) == 1: l_freq, h_freq = l_freq.item(), h_freq.item() - msg += " from %0.2g - %0.2g Hz" % (h_freq, l_freq) + msg += f" from {h_freq:0.2g} - {l_freq:0.2g} Hz" logger.info(msg) # Note: order of outputs is intentionally switched here! ( @@ -1871,21 +1867,14 @@ def _check_filterable(x, kind="filtered", alternative="filter"): pass else: raise TypeError( - "This low-level function only operates on np.ndarray " - f"instances. To get a {kind} {name} instance, use a method " - f"like `inst_new = inst.copy().{alternative}(...)` " - "instead." + "This low-level function only operates on np.ndarray instances. To get " + f"a {kind} {name} instance, use a method like `inst_new = inst.copy()." + f"{alternative}(...)` instead." ) _validate_type(x, (np.ndarray, list, tuple), f"Data to be {kind}") x = np.asanyarray(x) if x.dtype != np.float64: - raise ValueError( - "Data to be %s must be real floating, got %s" - % ( - kind, - x.dtype, - ) - ) + raise ValueError(f"Data to be {kind} must be real floating, got {x.dtype}") return x @@ -2279,15 +2268,12 @@ def float_array(c): if l_freq is not None: l_freq = cast(l_freq) if np.any(l_freq <= 0): - raise ValueError( - "highpass frequency %s must be greater than zero" % (l_freq,) - ) + raise ValueError(f"highpass frequency {l_freq} must be greater than zero") if h_freq is not None: h_freq = cast(h_freq) if np.any(h_freq >= sfreq / 2.0): raise ValueError( - "lowpass frequency %s must be less than Nyquist " - "(%s)" % (h_freq, sfreq / 2.0) + f"lowpass frequency {h_freq} must be less than Nyquist ({sfreq / 2.0})" ) dB_cutoff = False # meaning, don't try to compute or report @@ -2307,12 +2293,9 @@ def float_array(c): logger.info("FIR filter parameters") logger.info("---------------------") logger.info( - "Designing a %s, %s, %s %s filter:" - % (report_pass, report_phase, causality, kind) - ) - logger.info( - "- %s design (%s) method" % (_fir_design_dict[fir_design], fir_design) + f"Designing a {report_pass}, {report_phase}, {causality} {kind} filter:" ) + logger.info(f"- {_fir_design_dict[fir_design]} design ({fir_design}) method") this_dict = _fir_window_dict[fir_window] if fir_design == "firwin": logger.info( @@ -2326,8 +2309,8 @@ def float_array(c): if isinstance(l_trans_bandwidth, str): if l_trans_bandwidth != "auto": raise ValueError( - 'l_trans_bandwidth must be "auto" if ' - 'string, got "%s"' % l_trans_bandwidth + 'l_trans_bandwidth must be "auto" if string, got "' + f'{l_trans_bandwidth}"' ) l_trans_bandwidth = np.minimum(np.maximum(0.25 * l_freq, 2.0), l_freq) l_trans_rep = np.array(l_trans_bandwidth, float) @@ -2349,7 +2332,7 @@ def float_array(c): l_trans_bandwidth = cast(l_trans_bandwidth) if np.any(l_trans_bandwidth <= 0): raise ValueError( - "l_trans_bandwidth must be positive, got %s" % (l_trans_bandwidth,) + f"l_trans_bandwidth must be positive, got {l_trans_bandwidth}" ) l_stop = l_freq - l_trans_bandwidth if reverse: # band-stop style @@ -2357,10 +2340,9 @@ def float_array(c): l_freq += l_trans_bandwidth if np.any(l_stop < 0): raise ValueError( - "Filter specification invalid: Lower stop " - "frequency negative (%0.2f Hz). Increase pass" - " frequency or reduce the transition " - "bandwidth (l_trans_bandwidth)" % l_stop + "Filter specification invalid: Lower stop frequency negative (" + f"{l_stop:0.2f} Hz). Increase pass frequency or reduce the " + "transition bandwidth (l_trans_bandwidth)" ) if h_freq is not None: # low-pass component if isinstance(h_trans_bandwidth, str): @@ -2390,7 +2372,7 @@ def float_array(c): h_trans_bandwidth = cast(h_trans_bandwidth) if np.any(h_trans_bandwidth <= 0): raise ValueError( - "h_trans_bandwidth must be positive, got %s" % (h_trans_bandwidth,) + f"h_trans_bandwidth must be positive, got {h_trans_bandwidth}" ) h_stop = h_freq + h_trans_bandwidth if reverse: # band-stop style @@ -2398,8 +2380,8 @@ def float_array(c): h_freq -= h_trans_bandwidth if np.any(h_stop > sfreq / 2): raise ValueError( - "Effective band-stop frequency (%s) is too " - "high (maximum based on Nyquist is %s)" % (h_stop, sfreq / 2.0) + f"Effective band-stop frequency ({h_stop}) is too high (maximum " + f"based on Nyquist is {sfreq / 2.0})" ) if isinstance(filter_length, str) and filter_length.lower() == "auto": @@ -2410,7 +2392,7 @@ def float_array(c): if l_freq is not None: l_check = min(np.atleast_1d(l_trans_bandwidth)) mult_fact = 2.0 if fir_design == "firwin2" else 1.0 - filter_length = "%ss" % ( + filter_length = "{}s".format( _length_factors[fir_window] * mult_fact / float(min(h_check, l_check)), ) next_pow_2 = False # disable old behavior @@ -2425,15 +2407,12 @@ def float_array(c): filter_length += (filter_length - 1) % 2 logger.info( - "- Filter length: %s samples (%0.3f s)" - % (filter_length, filter_length / sfreq) + f"- Filter length: {filter_length} samples ({filter_length / sfreq:0.3f} s)" ) logger.info("") if filter_length <= 0: - raise ValueError( - "filter_length must be positive, got %s" % (filter_length,) - ) + raise ValueError(f"filter_length must be positive, got {filter_length}") if next_pow_2: filter_length = 2 ** int(np.ceil(np.log2(filter_length))) @@ -2448,9 +2427,8 @@ def float_array(c): filter_length = len_x if filter_length > len_x and not (l_freq is None and h_freq is None): warn( - "filter_length (%s) is longer than the signal (%s), " - "distortion is likely. Reduce filter length or filter a " - "longer signal." % (filter_length, len_x) + f"filter_length ({filter_length}) is longer than the signal ({len_x}), " + "distortion is likely. Reduce filter length or filter a longer signal." ) logger.debug("Using filter length: %s" % filter_length) @@ -2471,10 +2449,8 @@ def float_array(c): def _check_resamp_noop(sfreq, o_sfreq, rtol=1e-6): if np.isclose(sfreq, o_sfreq, atol=0, rtol=rtol): logger.info( - ( - f"Sampling frequency of the instance is already {sfreq}, " - "returning unmodified." - ) + f"Sampling frequency of the instance is already {sfreq}, returning " + "unmodified." ) return True return False @@ -2833,15 +2809,14 @@ def apply_hilbert( elif isinstance(n_fft, str): if n_fft != "auto": raise ValueError( - "n_fft must be an integer, string, or None, " - "got %s" % (type(n_fft),) + f"n_fft must be an integer, string, or None, got {type(n_fft)}" ) n_fft = next_fast_len(len(self.times)) n_fft = int(n_fft) if n_fft < len(self.times): raise ValueError( - "n_fft (%d) must be at least the number of time " - "points (%d)" % (n_fft, len(self.times)) + f"n_fft ({n_fft}) must be at least the number of time points (" + f"{len(self.times)})" ) dtype = None if envelope else np.complex128 picks = _picks_to_idx(self.info, picks, exclude=(), with_ref_meg=False) @@ -2875,9 +2850,7 @@ def _check_fun(fun, d, *args, **kwargs): if not isinstance(d, np.ndarray): raise TypeError("Return value must be an ndarray") if d.shape != want_shape: - raise ValueError( - "Return data must have shape %s not %s" % (want_shape, d.shape) - ) + raise ValueError(f"Return data must have shape {want_shape} not {d.shape}") return d diff --git a/mne/forward/_make_forward.py b/mne/forward/_make_forward.py index 0b3ce69fe57..313da3a4922 100644 --- a/mne/forward/_make_forward.py +++ b/mne/forward/_make_forward.py @@ -93,7 +93,7 @@ def _read_coil_def_file(fname, use_registry=True): if not use_registry or fname not in _coil_registry: big_val = 0.5 coils = list() - with open(fname, "r") as fid: + with open(fname) as fid: lines = fid.readlines() lines = lines[::-1] while len(lines) > 0: diff --git a/mne/io/array/array.py b/mne/io/array/array.py index a0df061821f..16f4888ec72 100644 --- a/mne/io/array/array.py +++ b/mne/io/array/array.py @@ -60,13 +60,14 @@ def __init__(self, data, info, first_samp=0, copy="auto", verbose=None): data = np.asanyarray(orig_data, dtype=dtype) if data.ndim != 2: raise ValueError( - "Data must be a 2D array of shape (n_channels, " - "n_samples), got shape %s" % (data.shape,) + "Data must be a 2D array of shape (n_channels, n_samples), got shape " + f"{data.shape}" ) if len(data) != len(info["ch_names"]): raise ValueError( - "len(data) (%s) does not match " - 'len(info["ch_names"]) (%s)' % (len(data), len(info["ch_names"])) + 'len(data) ({}) does not match len(info["ch_names"]) ({})'.format( + len(data), len(info["ch_names"]) + ) ) assert len(info["ch_names"]) == info["nchan"] if copy in ("auto", "info", "both"): @@ -76,15 +77,15 @@ def __init__(self, data, info, first_samp=0, copy="auto", verbose=None): data = data.copy() elif copy != "auto" and data is not orig_data: raise ValueError( - "data copying was not requested by copy=%r but " - "it was required to get to double floating point " - "precision" % (copy,) + f"data copying was not requested by copy={copy!r} but it was required " + "to get to double floating point precision" ) logger.info( - "Creating RawArray with %s data, n_channels=%s, n_times=%s" - % (dtype.__name__, data.shape[0], data.shape[1]) + "Creating RawArray with {} data, n_channels={}, n_times={}".format( + dtype.__name__, data.shape[0], data.shape[1] + ) ) - super(RawArray, self).__init__( + super().__init__( info, data, first_samps=(int(first_samp),), dtype=dtype, verbose=verbose ) logger.info( diff --git a/mne/io/artemis123/artemis123.py b/mne/io/artemis123/artemis123.py index 64d98c54dc2..4ecd524f73d 100644 --- a/mne/io/artemis123/artemis123.py +++ b/mne/io/artemis123/artemis123.py @@ -83,7 +83,7 @@ def _get_artemis123_info(fname, pos_fname=None): header_info["comments"] = "" header_info["channels"] = [] - with open(header, "r") as fid: + with open(header) as fid: # section flag # 0 - None # 1 - main header @@ -173,7 +173,7 @@ def _get_artemis123_info(fname, pos_fname=None): # build description desc = "" for k in ["Purpose", "Notes"]: - desc += "{} : {}\n".format(k, header_info[k]) + desc += f"{k} : {header_info[k]}\n" desc += "Comments : {}".format(header_info["comments"]) info.update( @@ -363,7 +363,7 @@ def __init__( last_samps = [header_info.get("num_samples", 1) - 1] - super(RawArtemis123, self).__init__( + super().__init__( info, preload, filenames=[input_fname], diff --git a/mne/io/artemis123/utils.py b/mne/io/artemis123/utils.py index 95f307058ea..432e593553d 100644 --- a/mne/io/artemis123/utils.py +++ b/mne/io/artemis123/utils.py @@ -19,9 +19,9 @@ def _load_mne_locs(fname=None): if not op.exists(fname): raise OSError('MNE locs file "%s" does not exist' % (fname)) - logger.info("Loading mne loc file {}".format(fname)) + logger.info(f"Loading mne loc file {fname}") locs = dict() - with open(fname, "r") as fid: + with open(fname) as fid: for line in fid: vals = line.strip().split(",") locs[vals[0]] = np.array(vals[1::], np.float64) @@ -50,7 +50,7 @@ def _generate_mne_locs_file(output_fname): def _load_tristan_coil_locs(coil_loc_path): """Load the Coil locations from Tristan CAD drawings.""" channel_info = dict() - with open(coil_loc_path, "r") as fid: + with open(coil_loc_path) as fid: # skip 2 Header lines fid.readline() fid.readline() @@ -72,7 +72,7 @@ def _compute_mne_loc(coil_loc): Note input coil locations are in inches. """ - loc = np.zeros((12)) + loc = np.zeros(12) if (np.linalg.norm(coil_loc["inner_coil"]) == 0) and ( np.linalg.norm(coil_loc["outer_coil"]) == 0 ): @@ -91,7 +91,7 @@ def _compute_mne_loc(coil_loc): def _read_pos(fname): """Read the .pos file and return positions as dig points.""" nas, lpa, rpa, hpi, extra = None, None, None, None, None - with open(fname, "r") as fid: + with open(fname) as fid: for line in fid: line = line.strip() if len(line) > 0: diff --git a/mne/io/base.py b/mne/io/base.py index 94cd2ffcdd0..bb40075335c 100644 --- a/mne/io/base.py +++ b/mne/io/base.py @@ -658,8 +658,7 @@ def time_as_index(self, times, use_rounding=False, origin=None): delta = 0 elif self.info["meas_date"] is None: raise ValueError( - 'origin must be None when info["meas_date"] ' - "is None, got %s" % (origin,) + f'origin must be None when info["meas_date"] is None, got {origin}' ) else: first_samp_in_abs_time = self.info["meas_date"] + timedelta( @@ -668,7 +667,7 @@ def time_as_index(self, times, use_rounding=False, origin=None): delta = (origin - first_samp_in_abs_time).total_seconds() times = np.atleast_1d(times) + delta - return super(BaseRaw, self).time_as_index(times, use_rounding) + return super().time_as_index(times, use_rounding) @property def _raw_lengths(self): @@ -2696,7 +2695,7 @@ def _check_start_stop_within_bounds(self): # we've done something wrong if we hit this n_times_max = len(self.raw.times) error_msg = ( - "Can't write raw file with no data: {0} -> {1} (max: {2}) requested" + "Can't write raw file with no data: {} -> {} (max: {}) requested" ).format(self.start, self.stop, n_times_max) if self.start >= self.stop or self.stop > n_times_max: raise RuntimeError(error_msg) diff --git a/mne/io/boxy/boxy.py b/mne/io/boxy/boxy.py index a240a1f387e..a3beefc218c 100644 --- a/mne/io/boxy/boxy.py +++ b/mne/io/boxy/boxy.py @@ -68,7 +68,7 @@ def __init__(self, fname, preload=False, verbose=None): raw_extras["offsets"] = list() # keep track of our offsets sfreq = None fname = str(_check_fname(fname, "read", True, "fname")) - with open(fname, "r") as fid: + with open(fname) as fid: line_num = 0 i_line = fid.readline() while i_line: @@ -170,7 +170,7 @@ def __init__(self, fname, preload=False, verbose=None): assert len(raw_extras["offsets"]) == delta + 1 if filetype == "non-parsed": delta //= raw_extras["source_num"] - super(RawBOXY, self).__init__( + super().__init__( info, preload, filenames=[fname], @@ -235,7 +235,7 @@ def _read_segment_file(self, data, idx, fi, start, stop, cals, mult): # Loop through our data. one = np.zeros((len(col_names), stop_read - start_read)) - with open(boxy_file, "r") as fid: + with open(boxy_file) as fid: # Just a more efficient version of this: # ii = 0 # for line_num, i_line in enumerate(fid): diff --git a/mne/io/brainvision/brainvision.py b/mne/io/brainvision/brainvision.py index e0f4e5a5c57..5dea11dd35d 100644 --- a/mne/io/brainvision/brainvision.py +++ b/mne/io/brainvision/brainvision.py @@ -111,7 +111,7 @@ def __init__( orig_format = "single" if isinstance(fmt, dict) else fmt raw_extras = dict(offsets=offsets, fmt=fmt, order=order, n_samples=n_samples) - super(RawBrainVision, self).__init__( + super().__init__( info, last_samps=[n_samples - 1], filenames=[data_fname], @@ -124,7 +124,7 @@ def __init__( self.set_montage(montage) - settings, cfg, cinfo, _ = _aux_hdr_info(hdr_fname) + settings, _, _, _ = _aux_hdr_info(hdr_fname) split_settings = settings.splitlines() self.impedances = _parse_impedance(split_settings, self.info["meas_date"]) @@ -542,7 +542,7 @@ def _get_hdr_info(hdr_fname, eog, misc, scale): # Try to get measurement date from marker file # Usually saved with a marker "New Segment", see BrainVision documentation regexp = r"^Mk\d+=New Segment,.*,\d+,\d+,-?\d+,(\d{20})$" - with open(mrk_fname, "r") as tmp_mrk_f: + with open(mrk_fname) as tmp_mrk_f: lines = tmp_mrk_f.readlines() for line in lines: @@ -636,7 +636,7 @@ def _get_hdr_info(hdr_fname, eog, misc, scale): ch_name = ch_dict[ch[0]] montage_names.append(ch_name) # 1: radius, 2: theta, 3: phi - rad, theta, phi = [float(c) for c in ch[1].split(",")] + rad, theta, phi = (float(c) for c in ch[1].split(",")) pol = np.deg2rad(theta) az = np.deg2rad(phi) # Coordinates could be "idealized" (spherical head model) @@ -656,9 +656,9 @@ def _get_hdr_info(hdr_fname, eog, misc, scale): if len(to_misc) > 0: misc += to_misc warn( - "No coordinate information found for channels {}. " - "Setting channel types to misc. To avoid this warning, set " - "channel types explicitly.".format(to_misc) + f"No coordinate information found for channels {to_misc}. Setting " + "channel types to misc. To avoid this warning, set channel types " + "explicitly." ) if np.isnan(cals).any(): @@ -988,9 +988,7 @@ def __call__(self, description): elif description in _OTHER_ACCEPTED_MARKERS: code = _OTHER_ACCEPTED_MARKERS[description] else: - code = super(_BVEventParser, self).__call__( - description, offset=_OTHER_OFFSET - ) + code = super().__call__(description, offset=_OTHER_OFFSET) return code diff --git a/mne/io/brainvision/tests/test_brainvision.py b/mne/io/brainvision/tests/test_brainvision.py index 1688963296a..9c48be78a23 100644 --- a/mne/io/brainvision/tests/test_brainvision.py +++ b/mne/io/brainvision/tests/test_brainvision.py @@ -133,14 +133,14 @@ def _mocked_meas_date_data(tmp_path_factory): """Prepare files for mocked_meas_date_file fixture.""" # Prepare the files tmp_path = tmp_path_factory.mktemp("brainvision_mocked_meas_date") - vhdr_fname, vmrk_fname, eeg_fname = [ + vhdr_fname, vmrk_fname, eeg_fname = ( tmp_path / ff.name for ff in [vhdr_path, vmrk_path, eeg_path] - ] + ) for orig, dest in zip([vhdr_path, eeg_path], [vhdr_fname, eeg_fname]): shutil.copyfile(orig, dest) # Get the marker information - with open(vmrk_path, "r") as fin: + with open(vmrk_path) as fin: lines = fin.readlines() return vhdr_fname, vmrk_fname, lines @@ -331,7 +331,7 @@ def test_ch_names_comma(tmp_path): shutil.copyfile(src, tmp_path / dest) comma_vhdr = tmp_path / "test.vhdr" - with open(comma_vhdr, "r") as fin: + with open(comma_vhdr) as fin: lines = fin.readlines() new_lines = [] diff --git a/mne/io/bti/bti.py b/mne/io/bti/bti.py index 8b9a6ac973f..71c85880069 100644 --- a/mne/io/bti/bti.py +++ b/mne/io/bti/bti.py @@ -138,7 +138,7 @@ def _rename_channels(names, ecg_ch="E31", eog_ch=("E63", "E64")): List of names, channel names in Neuromag style """ new = list() - ref_mag, ref_grad, eog, eeg, ext = [count(1) for _ in range(5)] + ref_mag, ref_grad, eog, eeg, ext = (count(1) for _ in range(5)) for i, name in enumerate(names, 1): if name.startswith("A"): name = "MEG %3.3d" % i @@ -176,7 +176,7 @@ def _read_head_shape(fname): dig_points = read_double_matrix(fid, _n_dig_points, 3) # reorder to lpa, rpa, nasion so = is direct. - nasion, lpa, rpa = [idx_points[_, :] for _ in [2, 0, 1]] + nasion, lpa, rpa = (idx_points[_, :] for _ in [2, 0, 1]) hpi = idx_points[3 : len(idx_points), :] return nasion, lpa, rpa, hpi, dig_points @@ -1096,7 +1096,7 @@ def __init__( filename = bti_info["pdf"] if isinstance(filename, BytesIO): filename = repr(filename) - super(RawBTi, self).__init__( + super().__init__( info, preload, filenames=[filename], diff --git a/mne/io/bti/tests/test_bti.py b/mne/io/bti/tests/test_bti.py index de2a5fdd79c..afe387b8769 100644 --- a/mne/io/bti/tests/test_bti.py +++ b/mne/io/bti/tests/test_bti.py @@ -155,16 +155,16 @@ def test_raw(pdf, config, hs, exported, tmp_path): ) assert len(ex.info["dig"]) in (3563, 5154) assert_dig_allclose(ex.info, ra.info, limit=100) - coil1, coil2 = [ + coil1, coil2 = ( np.concatenate([d["loc"].flatten() for d in r_.info["chs"][:NCH]]) for r_ in (ra, ex) - ] + ) assert_array_almost_equal(coil1, coil2, 7) - loc1, loc2 = [ + loc1, loc2 = ( np.concatenate([d["loc"].flatten() for d in r_.info["chs"][:NCH]]) for r_ in (ra, ex) - ] + ) assert_allclose(loc1, loc2) assert_allclose(ra[:NCH][0], ex[:NCH][0]) diff --git a/mne/io/cnt/cnt.py b/mne/io/cnt/cnt.py index 78bc15db580..e217324f437 100644 --- a/mne/io/cnt/cnt.py +++ b/mne/io/cnt/cnt.py @@ -520,7 +520,7 @@ def __init__( input_fname, eog, ecg, emg, misc, data_format, _date_format, header ) last_samps = [cnt_info["n_samples"] - 1] - super(RawCNT, self).__init__( + super().__init__( info, preload, filenames=[input_fname], diff --git a/mne/io/ctf/ctf.py b/mne/io/ctf/ctf.py index 65983258db5..c50459e0e0a 100644 --- a/mne/io/ctf/ctf.py +++ b/mne/io/ctf/ctf.py @@ -164,7 +164,7 @@ def __init__( f"file(s): {missing_names}, and the following file(s) had no " f"valid samples: {no_samps}" ) - super(RawCTF, self).__init__( + super().__init__( info, preload, first_samps=first_samps, diff --git a/mne/io/ctf/eeg.py b/mne/io/ctf/eeg.py index 29ece5e9f74..36ce3321b31 100644 --- a/mne/io/ctf/eeg.py +++ b/mne/io/ctf/eeg.py @@ -79,7 +79,7 @@ def _read_pos(directory, transformations): fname = fname[0] digs = list() i = 2000 - with open(fname, "r") as fid: + with open(fname) as fid: for line in fid: line = line.strip() if len(line) > 0: diff --git a/mne/io/ctf/info.py b/mne/io/ctf/info.py index b177e29bf9d..d49c9709f9c 100644 --- a/mne/io/ctf/info.py +++ b/mne/io/ctf/info.py @@ -535,7 +535,7 @@ def _read_bad_chans(directory, info): if not op.exists(fname): return [] mapping = dict(zip(_clean_names(info["ch_names"]), info["ch_names"])) - with open(fname, "r") as fid: + with open(fname) as fid: bad_chans = [mapping[f.strip()] for f in fid.readlines()] return bad_chans @@ -549,7 +549,7 @@ def _annotate_bad_segments(directory, start_time, meas_date): onsets = [] durations = [] desc = [] - with open(fname, "r") as fid: + with open(fname) as fid: for f in fid.readlines(): tmp = f.strip().split() desc.append("bad_%s" % tmp[0]) diff --git a/mne/io/curry/curry.py b/mne/io/curry/curry.py index 27fdc3ce7bc..021bc729ebf 100644 --- a/mne/io/curry/curry.py +++ b/mne/io/curry/curry.py @@ -464,7 +464,7 @@ def _make_trans_dig(curry_paths, info, curry_dev_dev_t): def _first_hpi(fname): # Get the first HPI result - with open(fname, "r") as fid: + with open(fname) as fid: for line in fid: line = line.strip() if any(x in line for x in ("FileVersion", "NumCoils")) or not line: @@ -472,7 +472,7 @@ def _first_hpi(fname): hpi = np.array(line.split(), float) break else: - raise RuntimeError("Could not find valid HPI in %s" % (fname,)) + raise RuntimeError(f"Could not find valid HPI in {fname}") # t is the first entry assert hpi.ndim == 1 hpi = hpi[1:] @@ -596,7 +596,7 @@ def __init__(self, fname, preload=False, verbose=None): last_samps = [n_samples - 1] raw_extras = dict(is_ascii=is_ascii) - super(RawCurry, self).__init__( + super().__init__( info, preload, filenames=[data_fname], diff --git a/mne/io/curry/tests/test_curry.py b/mne/io/curry/tests/test_curry.py index c4710ecb679..de5247fb3de 100644 --- a/mne/io/curry/tests/test_curry.py +++ b/mne/io/curry/tests/test_curry.py @@ -325,7 +325,7 @@ def test_check_missing_files(): def _mock_info_file(src, dst, sfreq, time_step): - with open(src, "r") as in_file, open(dst, "w") as out_file: + with open(src) as in_file, open(dst, "w") as out_file: for line in in_file: if "SampleFreqHz" in line: out_file.write(line.replace("500", str(sfreq))) diff --git a/mne/io/edf/edf.py b/mne/io/edf/edf.py index d9a9c7f2711..62987ac19fc 100644 --- a/mne/io/edf/edf.py +++ b/mne/io/edf/edf.py @@ -150,7 +150,7 @@ def __init__( *, verbose=None, ): - logger.info("Extracting EDF parameters from {}...".format(input_fname)) + logger.info(f"Extracting EDF parameters from {input_fname}...") input_fname = os.path.abspath(input_fname) info, edf_info, orig_units = _get_info( input_fname, stim_channel, eog, misc, exclude, infer_types, preload, include @@ -284,7 +284,7 @@ def __init__( include=None, verbose=None, ): - logger.info("Extracting EDF parameters from {}...".format(input_fname)) + logger.info(f"Extracting EDF parameters from {input_fname}...") input_fname = os.path.abspath(input_fname) info, edf_info, orig_units = _get_info( input_fname, stim_channel, eog, misc, exclude, True, preload, include @@ -846,11 +846,11 @@ def _read_edf_header(fname, exclude, infer_types, include=None): fid.read(8) # skip file's meas_date else: meas_date = fid.read(8).decode("latin-1") - day, month, year = [int(x) for x in meas_date.split(".")] + day, month, year = (int(x) for x in meas_date.split(".")) year = year + 2000 if year < 85 else year + 1900 meas_time = fid.read(8).decode("latin-1") - hour, minute, sec = [int(x) for x in meas_time.split(".")] + hour, minute, sec = (int(x) for x in meas_time.split(".")) try: meas_date = datetime( year, month, day, hour, minute, sec, tzinfo=timezone.utc @@ -1498,10 +1498,10 @@ def _check_stim_channel( ] if len(tal_ch_names_found): _msg = ( - "The synthesis of the stim channel is not supported" - " since 0.18. Please remove {} from `stim_channel`" - " and use `mne.events_from_annotations` instead" - ).format(tal_ch_names_found) + "The synthesis of the stim channel is not supported since 0.18. Please " + f"remove {tal_ch_names_found} from `stim_channel` and use " + "`mne.events_from_annotations` instead." + ) raise ValueError(_msg) ch_names_low = [ch.lower() for ch in ch_names] diff --git a/mne/io/edf/tests/test_gdf.py b/mne/io/edf/tests/test_gdf.py index c029cc3280c..70801a22cce 100644 --- a/mne/io/edf/tests/test_gdf.py +++ b/mne/io/edf/tests/test_gdf.py @@ -38,7 +38,7 @@ def test_gdf_data(): # Test Status is added as event EXPECTED_EVS_ONSETS = raw._raw_extras[0]["events"][1] EXPECTED_EVS_ID = { - "{}".format(evs): i + f"{evs}": i for i, evs in enumerate( [ 32769, diff --git a/mne/io/eeglab/eeglab.py b/mne/io/eeglab/eeglab.py index cd383c6ddb4..50cfc39c820 100644 --- a/mne/io/eeglab/eeglab.py +++ b/mne/io/eeglab/eeglab.py @@ -467,7 +467,7 @@ def __init__( data_fname = _check_eeglab_fname(input_fname, eeg.data) logger.info("Reading %s" % data_fname) - super(RawEEGLAB, self).__init__( + super().__init__( info, preload, filenames=[data_fname], @@ -491,7 +491,7 @@ def __init__( data = np.empty((n_chan, n_times), dtype=float) data[:n_chan] = eeg.data data *= CAL - super(RawEEGLAB, self).__init__( + super().__init__( info, data, filenames=[input_fname], @@ -694,7 +694,7 @@ def __init__( assert data.shape == (eeg.trials, eeg.nbchan, eeg.pnts) tmin, tmax = eeg.xmin, eeg.xmax - super(EpochsEEGLAB, self).__init__( + super().__init__( info, data, events, diff --git a/mne/io/eeglab/tests/test_eeglab.py b/mne/io/eeglab/tests/test_eeglab.py index ce34a186910..35f9fea741b 100644 --- a/mne/io/eeglab/tests/test_eeglab.py +++ b/mne/io/eeglab/tests/test_eeglab.py @@ -71,7 +71,7 @@ def test_io_set_raw(fname): """Test importing EEGLAB .set files.""" montage = read_custom_montage(montage_path) - montage.ch_names = ["EEG {0:03d}".format(ii) for ii in range(len(montage.ch_names))] + montage.ch_names = [f"EEG {ii:03d}" for ii in range(len(montage.ch_names))] kws = dict(reader=read_raw_eeglab, input_fname=fname) if fname.name == "test_raw_chanloc.set": diff --git a/mne/io/egi/egi.py b/mne/io/egi/egi.py index 455c47ae726..b0124bdc541 100644 --- a/mne/io/egi/egi.py +++ b/mne/io/egi/egi.py @@ -307,7 +307,7 @@ def __init__( orig_format = ( egi_info["orig_format"] if egi_info["orig_format"] != "float" else "single" ) - super(RawEGI, self).__init__( + super().__init__( info, preload, orig_format=orig_format, diff --git a/mne/io/egi/egimff.py b/mne/io/egi/egimff.py index dfc35d08048..e241208d1cc 100644 --- a/mne/io/egi/egimff.py +++ b/mne/io/egi/egimff.py @@ -647,7 +647,7 @@ def __init__( self._filenames = [file_bin] self._raw_extras = [egi_info] - super(RawMff, self).__init__( + super().__init__( info, preload=preload, orig_format="single", diff --git a/mne/io/egi/general.py b/mne/io/egi/general.py index facbdc7a9e9..9ca6dc7f0b9 100644 --- a/mne/io/egi/general.py +++ b/mne/io/egi/general.py @@ -151,7 +151,7 @@ def _get_signalfname(filepath): elif len(infobj.getElementsByTagName("PNSData")): signal_type = "PNS" all_files[signal_type] = { - "signal": "signal{}.bin".format(bin_num_str), + "signal": f"signal{bin_num_str}.bin", "info": infofile, } if "EEG" not in all_files: diff --git a/mne/io/egi/tests/test_egi.py b/mne/io/egi/tests/test_egi.py index ee0071f2819..71120d8d6f7 100644 --- a/mne/io/egi/tests/test_egi.py +++ b/mne/io/egi/tests/test_egi.py @@ -297,7 +297,7 @@ def test_io_egi_pns_mff(tmp_path): egi_fname_mat = testing_path / "EGI" / "test_egi_pns.mat" mc = sio.loadmat(egi_fname_mat) for ch_name, ch_idx, mat_name in zip(pns_names, pns_chans, mat_names): - print("Testing {}".format(ch_name)) + print(f"Testing {ch_name}") mc_key = [x for x in mc.keys() if mat_name in x][0] cal = raw.info["chs"][ch_idx]["cal"] mat_data = mc[mc_key] * cal @@ -349,7 +349,7 @@ def test_io_egi_pns_mff_bug(preload): "EMGLeg", ] for ch_name, ch_idx, mat_name in zip(pns_names, pns_chans, mat_names): - print("Testing {}".format(ch_name)) + print(f"Testing {ch_name}") mc_key = [x for x in mc.keys() if mat_name in x][0] cal = raw.info["chs"][ch_idx]["cal"] mat_data = mc[mc_key] * cal diff --git a/mne/io/eximia/eximia.py b/mne/io/eximia/eximia.py index 8b85768fedc..1d253f369d1 100644 --- a/mne/io/eximia/eximia.py +++ b/mne/io/eximia/eximia.py @@ -87,12 +87,12 @@ def __init__(self, fname, preload=False, verbose=None): n_samples, extra = divmod(n_bytes, (n_chan * 2)) if extra != 0: warn( - "Incorrect number of samples in file (%s), the file is " - "likely truncated" % (n_samples,) + f"Incorrect number of samples in file ({n_samples}), the file is likely" + " truncated" ) for ch, cal in zip(info["chs"], cals): ch["cal"] = cal - super(RawEximia, self).__init__( + super().__init__( info, preload=preload, last_samps=(n_samples - 1,), diff --git a/mne/io/eyelink/eyelink.py b/mne/io/eyelink/eyelink.py index 1eaf82500ae..2ab00d22b58 100644 --- a/mne/io/eyelink/eyelink.py +++ b/mne/io/eyelink/eyelink.py @@ -99,7 +99,7 @@ def __init__( overlap_threshold=0.05, verbose=None, ): - logger.info("Loading {}".format(fname)) + logger.info(f"Loading {fname}") fname = Path(fname) @@ -108,7 +108,7 @@ def __init__( fname, find_overlaps, overlap_threshold, apply_offsets ) # ======================== Create Raw Object ========================= - super(RawEyelink, self).__init__( + super().__init__( info, preload=eye_ch_data, filenames=[fname], diff --git a/mne/io/eyelink/tests/test_eyelink.py b/mne/io/eyelink/tests/test_eyelink.py index 47b25e94489..653be12564b 100644 --- a/mne/io/eyelink/tests/test_eyelink.py +++ b/mne/io/eyelink/tests/test_eyelink.py @@ -295,7 +295,7 @@ def test_annotations_without_offset(tmp_path): out_file = tmp_path / "tmp_eyelink.asc" # create fake dataset - with open(fname_href, "r") as file: + with open(fname_href) as file: lines = file.readlines() ts = lines[-3].split("\t")[0] line = f"MSG\t{ts} test string\n" diff --git a/mne/io/fiff/raw.py b/mne/io/fiff/raw.py index 39f0466e1eb..54bfe9e1921 100644 --- a/mne/io/fiff/raw.py +++ b/mne/io/fiff/raw.py @@ -124,7 +124,7 @@ def __init__( fname = None # noqa _check_raw_compatibility(raws) - super(Raw, self).__init__( + super().__init__( copy.deepcopy(raws[0].info), False, [r.first_samp for r in raws], diff --git a/mne/io/fiff/tests/test_raw_fiff.py b/mne/io/fiff/tests/test_raw_fiff.py index 154d70b0dee..fa3f04be0c7 100644 --- a/mne/io/fiff/tests/test_raw_fiff.py +++ b/mne/io/fiff/tests/test_raw_fiff.py @@ -658,9 +658,9 @@ def test_split_files(tmp_path, mod, monkeypatch): m.setattr(base, "MAX_N_SPLITS", 2) with pytest.raises(RuntimeError, match="Exceeded maximum number of splits"): raw.save(fname, split_naming="bids", **kwargs) - fname_1, fname_2, fname_3 = [ + fname_1, fname_2, fname_3 = ( (tmp_path / f"test_split-{ii:02d}_{mod}.fif") for ii in range(1, 4) - ] + ) assert not fname.is_file() assert fname_1.is_file() assert fname_2.is_file() @@ -669,7 +669,7 @@ def test_split_files(tmp_path, mod, monkeypatch): m.setattr(base, "MAX_N_SPLITS", 2) with pytest.raises(RuntimeError, match="Exceeded maximum number of splits"): raw.save(fname, split_naming="neuromag", **kwargs) - fname_2, fname_3 = [(tmp_path / f"test_{mod}-{ii}.fif") for ii in range(1, 3)] + fname_2, fname_3 = ((tmp_path / f"test_{mod}-{ii}.fif") for ii in range(1, 3)) assert fname.is_file() assert fname_2.is_file() assert not fname_3.is_file() diff --git a/mne/io/fil/fil.py b/mne/io/fil/fil.py index 99e2b77b2d8..eba8662f342 100644 --- a/mne/io/fil/fil.py +++ b/mne/io/fil/fil.py @@ -117,11 +117,11 @@ def __init__(self, binfile, precision="single", preload=False): else: warn("No sensor position information found.") - with open(files["meg"], "r") as fid: + with open(files["meg"]) as fid: meg = json.load(fid) info = _compose_meas_info(meg, chans) - super(RawFIL, self).__init__( + super().__init__( info, preload, filenames=[files["bin"]], @@ -131,7 +131,7 @@ def __init__(self, binfile, precision="single", preload=False): ) if files["coordsystem"].is_file(): - with open(files["coordsystem"], "r") as fid: + with open(files["coordsystem"]) as fid: csys = json.load(fid) hc = csys["HeadCoilCoordinates"] diff --git a/mne/io/hitachi/hitachi.py b/mne/io/hitachi/hitachi.py index a81095712d1..4b5c0b9fac6 100644 --- a/mne/io/hitachi/hitachi.py +++ b/mne/io/hitachi/hitachi.py @@ -268,7 +268,7 @@ def _get_hitachi_info(fname, S_offset, D_offset, ignore_names): "3x11": "ETG-4000", } _check_option("Hitachi mode", mode, sorted(names)) - n_row, n_col = [int(x) for x in mode.split("x")] + n_row, n_col = (int(x) for x in mode.split("x")) logger.info(f"Constructing pairing matrix for {names[mode]} ({mode})") pairs = _compute_pairs(n_row, n_col, n=1 + (mode == "3x3")) assert n_nirs == len(pairs) * 2 diff --git a/mne/io/kit/coreg.py b/mne/io/kit/coreg.py index 4e5bd0bdf8f..7a113c9f3e6 100644 --- a/mne/io/kit/coreg.py +++ b/mne/io/kit/coreg.py @@ -154,12 +154,9 @@ def _set_dig_kit(mrk, elp, hsp, eeg): hsp = _decimate_points(hsp, res=0.005) n_new = len(hsp) warn( - "The selected head shape contained {n_in} points, which is " - "more than recommended ({n_rec}), and was automatically " - "downsampled to {n_new} points. The preferred way to " - "downsample is using FastScan.".format( - n_in=n_pts, n_rec=KIT.DIG_POINTS, n_new=n_new - ) + f"The selected head shape contained {n_pts} points, which is more than " + f"recommended ({KIT.DIG_POINTS}), and was automatically downsampled to " + f"{n_new} points. The preferred way to downsample is using FastScan." ) if isinstance(elp, (str, Path, PathLike)): diff --git a/mne/io/kit/kit.py b/mne/io/kit/kit.py index 2aaa79017ba..c89ee66c253 100644 --- a/mne/io/kit/kit.py +++ b/mne/io/kit/kit.py @@ -151,7 +151,7 @@ def __init__( last_samps = [kit_info["n_samples"] - 1] self._raw_extras = [kit_info] _set_stimchannels(self, info, stim, stim_code) - super(RawKIT, self).__init__( + super().__init__( info, preload, last_samps=last_samps, @@ -422,7 +422,7 @@ def __init__( self._raw_extras[0]["frame_length"], ) tmax = ((data.shape[2] - 1) / self.info["sfreq"]) + tmin - super(EpochsKIT, self).__init__( + super().__init__( self.info, data, events, diff --git a/mne/io/neuralynx/neuralynx.py b/mne/io/neuralynx/neuralynx.py index 1c007ba5787..1d3a0a48ca8 100644 --- a/mne/io/neuralynx/neuralynx.py +++ b/mne/io/neuralynx/neuralynx.py @@ -12,7 +12,7 @@ from ..base import BaseRaw -class AnalogSignalGap(object): +class AnalogSignalGap: """Dummy object to represent gaps in Neuralynx data. Creates a AnalogSignalProxy-like object. @@ -236,7 +236,7 @@ def __init__( description=["BAD_ACQ_SKIP"] * len(gap_start_ids), ) - super(RawNeuralynx, self).__init__( + super().__init__( info=info, last_samps=[sizes_sorted.sum() - 1], filenames=[fname], diff --git a/mne/io/neuralynx/tests/test_neuralynx.py b/mne/io/neuralynx/tests/test_neuralynx.py index 1532845ab7a..614e5021e69 100644 --- a/mne/io/neuralynx/tests/test_neuralynx.py +++ b/mne/io/neuralynx/tests/test_neuralynx.py @@ -2,7 +2,6 @@ # Copyright the MNE-Python contributors. import os from ast import literal_eval -from typing import Dict import numpy as np import pytest @@ -18,7 +17,7 @@ pytest.importorskip("neo") -def _nlxheader_to_dict(matdict: Dict) -> Dict: +def _nlxheader_to_dict(matdict: dict) -> dict: """Convert the read-in "Header" field into a dict. All the key-value pairs of Header entries are formatted as strings diff --git a/mne/io/nicolet/nicolet.py b/mne/io/nicolet/nicolet.py index 9b5fa2b3ae5..0ef0c0a4f4a 100644 --- a/mne/io/nicolet/nicolet.py +++ b/mne/io/nicolet/nicolet.py @@ -84,7 +84,7 @@ def _get_nicolet_info(fname, ch_type, eog, ecg, emg, misc): logger.info("Reading header...") header_info = dict() - with open(header, "r") as fid: + with open(header) as fid: for line in fid: var, value = line.split("=") if var == "elec_names": @@ -187,7 +187,7 @@ def __init__( input_fname = path.abspath(input_fname) info, header_info = _get_nicolet_info(input_fname, ch_type, eog, ecg, emg, misc) last_samps = [header_info["num_samples"] - 1] - super(RawNicolet, self).__init__( + super().__init__( info, preload, filenames=[input_fname], diff --git a/mne/io/nihon/nihon.py b/mne/io/nihon/nihon.py index fb7855e5323..ef14a735ca9 100644 --- a/mne/io/nihon/nihon.py +++ b/mne/io/nihon/nihon.py @@ -70,7 +70,7 @@ def _read_nihon_metadata(fname): warn("No PNT file exists. Metadata will be blank") return metadata logger.info("Found PNT file, reading metadata.") - with open(pnt_fname, "r") as fid: + with open(pnt_fname) as fid: version = np.fromfile(fid, "|S16", 1).astype("U16")[0] if version not in _valid_headers: raise ValueError(f"Not a valid Nihon Kohden PNT file ({version})") @@ -135,7 +135,7 @@ def _read_21e_file(fname): logger.info("Found 21E file, reading channel names.") for enc in _encodings: try: - with open(e_fname, "r", encoding=enc) as fid: + with open(e_fname, encoding=enc) as fid: keep_parsing = False for line in fid: if line.startswith("["): @@ -169,10 +169,10 @@ def _read_nihon_header(fname): _chan_labels = _read_21e_file(fname) header = {} logger.info(f"Reading header from {fname}") - with open(fname, "r") as fid: + with open(fname) as fid: version = np.fromfile(fid, "|S16", 1).astype("U16")[0] if version not in _valid_headers: - raise ValueError("Not a valid Nihon Kohden EEG file ({})".format(version)) + raise ValueError(f"Not a valid Nihon Kohden EEG file ({version})") fid.seek(0x0081) control_block = np.fromfile(fid, "|S16", 1).astype("U16")[0] @@ -284,10 +284,10 @@ def _read_nihon_annotations(fname): warn("No LOG file exists. Annotations will not be read") return dict(onset=[], duration=[], description=[]) logger.info("Found LOG file, reading events.") - with open(log_fname, "r") as fid: + with open(log_fname) as fid: version = np.fromfile(fid, "|S16", 1).astype("U16")[0] if version not in _valid_headers: - raise ValueError("Not a valid Nihon Kohden LOG file ({})".format(version)) + raise ValueError(f"Not a valid Nihon Kohden LOG file ({version})") fid.seek(0x91) n_logblocks = np.fromfile(fid, np.uint8, 1)[0] @@ -415,7 +415,7 @@ def __init__(self, fname, preload=False, verbose=None): info["chs"][i_ch]["range"] = t_range info["chs"][i_ch]["cal"] = 1 / t_range - super(RawNihon, self).__init__( + super().__init__( info, preload=preload, last_samps=(n_samples - 1,), diff --git a/mne/io/nirx/nirx.py b/mne/io/nirx/nirx.py index 1fb51b50380..0eb2565cb32 100644 --- a/mne/io/nirx/nirx.py +++ b/mne/io/nirx/nirx.py @@ -65,7 +65,7 @@ def read_raw_nirx( def _open(fname): - return open(fname, "r", encoding="latin-1") + return open(fname, encoding="latin-1") @fill_doc @@ -476,7 +476,7 @@ def __init__(self, fname, saturated, preload=False, verbose=None): annot_mask |= mask nan_mask[key] = None # shouldn't need again - super(RawNIRX, self).__init__( + super().__init__( info, preload, filenames=[fname], diff --git a/mne/io/nsx/nsx.py b/mne/io/nsx/nsx.py index 2a39efa2989..c20e19b29ed 100644 --- a/mne/io/nsx/nsx.py +++ b/mne/io/nsx/nsx.py @@ -178,7 +178,7 @@ def __init__( preload=False, verbose=None, ): - logger.info("Extracting NSX parameters from {}...".format(input_fname)) + logger.info(f"Extracting NSX parameters from {input_fname}...") input_fname = os.path.abspath(input_fname) ( info, @@ -191,7 +191,7 @@ def __init__( ) = _get_hdr_info(input_fname, stim_channel=stim_channel, eog=eog, misc=misc) raw_extras["orig_format"] = orig_format first_samps = (raw_extras["timestamp"][0],) - super(RawNSX, self).__init__( + super().__init__( info, first_samps=first_samps, last_samps=[first_samps[0] + n_samples - 1], @@ -311,7 +311,7 @@ def _read_header_22_and_above(fname): basic_header[x] = basic_header[x] * 1e-3 ver_major, ver_minor = basic_header.pop("ver_major"), basic_header.pop("ver_minor") - basic_header["spec"] = "{}.{}".format(ver_major, ver_minor) + basic_header["spec"] = f"{ver_major}.{ver_minor}" data_header = list() index = 0 @@ -355,9 +355,9 @@ def _get_hdr_info(fname, stim_channel=True, eog=None, misc=None): ch_names = list(nsx_info["extended"]["electrode_label"]) ch_types = list(nsx_info["extended"]["type"]) ch_units = list(nsx_info["extended"]["units"]) - ch_names, ch_types, ch_units = [ + ch_names, ch_types, ch_units = ( list(map(bytes.decode, xx)) for xx in (ch_names, ch_types, ch_units) - ] + ) max_analog_val = nsx_info["extended"]["max_analog_val"].astype("double") min_analog_val = nsx_info["extended"]["min_analog_val"].astype("double") max_digital_val = nsx_info["extended"]["max_digital_val"].astype("double") diff --git a/mne/io/persyst/persyst.py b/mne/io/persyst/persyst.py index 0ef6723ba11..11f8a3a35ea 100644 --- a/mne/io/persyst/persyst.py +++ b/mne/io/persyst/persyst.py @@ -226,7 +226,7 @@ def __init__(self, fname, preload=False, verbose=None): raw_extras = {"dtype": dtype, "n_chs": n_chs, "n_samples": n_samples} # create Raw object - super(RawPersyst, self).__init__( + super().__init__( info, preload, filenames=[dat_fpath], @@ -351,7 +351,7 @@ def _read_lay_contents(fname): # initialize all section to empty str section = "" - with open(fname, "r") as fin: + with open(fname) as fin: for line in fin: # break a line into a status, key and value status, key, val = _process_lay_line(line, section) diff --git a/mne/io/persyst/tests/test_persyst.py b/mne/io/persyst/tests/test_persyst.py index c81b53f2b79..76e117817fd 100644 --- a/mne/io/persyst/tests/test_persyst.py +++ b/mne/io/persyst/tests/test_persyst.py @@ -85,7 +85,7 @@ def test_persyst_dates(tmp_path): # reformat the lay file to have testdate with # "/" character - with open(fname_lay, "r") as fin: + with open(fname_lay) as fin: with open(new_fname_lay, "w") as fout: # for each line in the input file for idx, line in enumerate(fin): @@ -101,7 +101,7 @@ def test_persyst_dates(tmp_path): # reformat the lay file to have testdate with # "-" character os.remove(new_fname_lay) - with open(fname_lay, "r") as fin: + with open(fname_lay) as fin: with open(new_fname_lay, "w") as fout: # for each line in the input file for idx, line in enumerate(fin): @@ -163,7 +163,7 @@ def test_persyst_moved_file(tmp_path): # to the full path, but it should still not work # as reader requires lay and dat file to be in # same directory - with open(fname_lay, "r") as fin: + with open(fname_lay) as fin: with open(new_fname_lay, "w") as fout: # for each line in the input file for idx, line in enumerate(fin): @@ -216,7 +216,7 @@ def test_persyst_errors(tmp_path): shutil.copy(fname_dat, new_fname_dat) # reformat the lay file - with open(fname_lay, "r") as fin: + with open(fname_lay) as fin: with open(new_fname_lay, "w") as fout: # for each line in the input file for idx, line in enumerate(fin): @@ -229,7 +229,7 @@ def test_persyst_errors(tmp_path): # reformat the lay file os.remove(new_fname_lay) - with open(fname_lay, "r") as fin: + with open(fname_lay) as fin: with open(new_fname_lay, "w") as fout: # for each line in the input file for idx, line in enumerate(fin): @@ -243,7 +243,7 @@ def test_persyst_errors(tmp_path): # reformat the lay file to have testdate # improperly specified os.remove(new_fname_lay) - with open(fname_lay, "r") as fin: + with open(fname_lay) as fin: with open(new_fname_lay, "w") as fout: # for each line in the input file for idx, line in enumerate(fin): diff --git a/mne/io/snirf/_snirf.py b/mne/io/snirf/_snirf.py index 0fc9ee246e9..a7d081983af 100644 --- a/mne/io/snirf/_snirf.py +++ b/mne/io/snirf/_snirf.py @@ -59,7 +59,7 @@ def read_raw_snirf( def _open(fname): - return open(fname, "r", encoding="latin-1") + return open(fname, encoding="latin-1") @fill_doc @@ -415,10 +415,10 @@ def natural_keys(text): info["dig"] = dig str_date = _correct_shape( - np.array((dat.get("/nirs/metaDataTags/MeasurementDate"))) + np.array(dat.get("/nirs/metaDataTags/MeasurementDate")) )[0].decode("UTF-8") str_time = _correct_shape( - np.array((dat.get("/nirs/metaDataTags/MeasurementTime"))) + np.array(dat.get("/nirs/metaDataTags/MeasurementTime")) )[0].decode("UTF-8") str_datetime = str_date + str_time @@ -460,7 +460,7 @@ def natural_keys(text): with info._unlock(): info["subject_info"]["birthday"] = birthday - super(RawSNIRF, self).__init__( + super().__init__( info, preload, filenames=[fname], diff --git a/mne/io/snirf/tests/test_snirf.py b/mne/io/snirf/tests/test_snirf.py index 2d2ad2c6324..f6eb5765fab 100644 --- a/mne/io/snirf/tests/test_snirf.py +++ b/mne/io/snirf/tests/test_snirf.py @@ -243,21 +243,21 @@ def test_snirf_nonstandard(tmp_path): fname = str(tmp_path) + "/mod.snirf" # Manually mark up the file to match MNE-NIRS custom tags with h5py.File(fname, "r+") as f: - f.create_dataset("nirs/metaDataTags/middleName", data=["X".encode("UTF-8")]) - f.create_dataset("nirs/metaDataTags/lastName", data=["Y".encode("UTF-8")]) - f.create_dataset("nirs/metaDataTags/sex", data=["1".encode("UTF-8")]) + f.create_dataset("nirs/metaDataTags/middleName", data=[b"X"]) + f.create_dataset("nirs/metaDataTags/lastName", data=[b"Y"]) + f.create_dataset("nirs/metaDataTags/sex", data=[b"1"]) raw = read_raw_snirf(fname, preload=True) assert raw.info["subject_info"]["middle_name"] == "X" assert raw.info["subject_info"]["last_name"] == "Y" assert raw.info["subject_info"]["sex"] == 1 with h5py.File(fname, "r+") as f: del f["nirs/metaDataTags/sex"] - f.create_dataset("nirs/metaDataTags/sex", data=["2".encode("UTF-8")]) + f.create_dataset("nirs/metaDataTags/sex", data=[b"2"]) raw = read_raw_snirf(fname, preload=True) assert raw.info["subject_info"]["sex"] == 2 with h5py.File(fname, "r+") as f: del f["nirs/metaDataTags/sex"] - f.create_dataset("nirs/metaDataTags/sex", data=["0".encode("UTF-8")]) + f.create_dataset("nirs/metaDataTags/sex", data=[b"0"]) raw = read_raw_snirf(fname, preload=True) assert raw.info["subject_info"]["sex"] == 0 diff --git a/mne/io/tests/test_raw.py b/mne/io/tests/test_raw.py index ce5d111bcbf..33384c1e0e4 100644 --- a/mne/io/tests/test_raw.py +++ b/mne/io/tests/test_raw.py @@ -764,7 +764,7 @@ def raw_factory(meas_date): ) return raw - raw_A, raw_B = [raw_factory((x, 0)) for x in [0, 2]] + raw_A, raw_B = (raw_factory((x, 0)) for x in [0, 2]) raw_A.append(raw_B) assert_array_equal(raw_A.annotations.onset, EXPECTED_ONSET) diff --git a/mne/label.py b/mne/label.py index 77ddf5bcffd..24aed492b12 100644 --- a/mne/label.py +++ b/mne/label.py @@ -1133,8 +1133,8 @@ def read_label(filename, subject=None, color=None, *, verbose=None): hemi = "rh" else: raise ValueError( - "Cannot find which hemisphere it is. File should end" - " with lh.label or rh.label: %s" % (basename,) + "Cannot find which hemisphere it is. File should end with lh.label or " + f"rh.label: {basename}" ) # find name @@ -1147,7 +1147,7 @@ def read_label(filename, subject=None, color=None, *, verbose=None): name = "%s-%s" % (basename_, hemi) # read the file - with open(filename, "r") as fid: + with open(filename) as fid: comment = fid.readline().replace("\n", "")[1:] nv = int(fid.readline()) data = np.empty((5, nv)) diff --git a/mne/minimum_norm/inverse.py b/mne/minimum_norm/inverse.py index f41f660ac4e..87122fdb6e1 100644 --- a/mne/minimum_norm/inverse.py +++ b/mne/minimum_norm/inverse.py @@ -725,7 +725,7 @@ def prepare_inverse_operator( # # w = diag(diag(R)) ** 0.5 # - noise_weight = inv["reginv"] * np.sqrt((1.0 + inv["sing"] ** 2 / lambda2)) + noise_weight = inv["reginv"] * np.sqrt(1.0 + inv["sing"] ** 2 / lambda2) noise_norm = np.zeros(inv["eigen_leads"]["nrow"]) (nrm2,) = linalg.get_blas_funcs(("nrm2",), (noise_norm,)) diff --git a/mne/misc.py b/mne/misc.py index 937f0eb4c9e..9313f048cbc 100644 --- a/mne/misc.py +++ b/mne/misc.py @@ -24,7 +24,7 @@ def parse_config(fname): """ reject_params = read_reject_parameters(fname) - with open(fname, "r") as f: + with open(fname) as f: lines = f.readlines() cat_ind = [i for i, x in enumerate(lines) if "category {" in x] @@ -69,7 +69,7 @@ def read_reject_parameters(fname): params : dict The rejection parameters. """ - with open(fname, "r") as f: + with open(fname) as f: lines = f.readlines() reject_names = ["gradReject", "magReject", "eegReject", "eogReject", "ecgReject"] @@ -85,7 +85,7 @@ def read_reject_parameters(fname): def read_flat_parameters(fname): """Read flat channel rejection parameters from .cov or .ave config file.""" - with open(fname, "r") as f: + with open(fname) as f: lines = f.readlines() reject_names = ["gradFlat", "magFlat", "eegFlat", "eogFlat", "ecgFlat"] diff --git a/mne/morph.py b/mne/morph.py index eb201e34451..db1d65236c7 100644 --- a/mne/morph.py +++ b/mne/morph.py @@ -738,12 +738,12 @@ def __repr__(self): # noqa: D105 s = "%s" % self.kind s += ", %s -> %s" % (self.subject_from, self.subject_to) if self.kind == "volume": - s += ", zooms : {}".format(self.zooms) - s += ", niter_affine : {}".format(self.niter_affine) - s += ", niter_sdr : {}".format(self.niter_sdr) + s += f", zooms : {self.zooms}" + s += f", niter_affine : {self.niter_affine}" + s += f", niter_sdr : {self.niter_sdr}" elif self.kind in ("surface", "vector"): - s += ", spacing : {}".format(self.spacing) - s += ", smooth : %s" % self.smooth + s += f", spacing : {self.spacing}" + s += f", smooth : {self.smooth}" s += ", xhemi" if self.xhemi else "" return "" % s @@ -1295,7 +1295,7 @@ def grade_to_vertices(subject, grade, subjects_dir=None, n_jobs=None, verbose=No spheres_to = [ subjects_dir / subject / "surf" / (xh + ".sphere.reg") for xh in ["lh", "rh"] ] - lhs, rhs = [read_surface(s)[0] for s in spheres_to] + lhs, rhs = (read_surface(s)[0] for s in spheres_to) if grade is not None: # fill a subset of vertices if isinstance(grade, list): @@ -1314,7 +1314,7 @@ def grade_to_vertices(subject, grade, subjects_dir=None, n_jobs=None, verbose=No # Compute nearest vertices in high dim mesh parallel, my_compute_nearest, _ = parallel_func(_compute_nearest, n_jobs) - lhs, rhs, rr = [a.astype(np.float32) for a in [lhs, rhs, ico["rr"]]] + lhs, rhs, rr = (a.astype(np.float32) for a in [lhs, rhs, ico["rr"]]) vertices = parallel(my_compute_nearest(xhs, rr) for xhs in [lhs, rhs]) # Make sure the vertices are ordered vertices = [np.sort(verts) for verts in vertices] diff --git a/mne/preprocessing/_fine_cal.py b/mne/preprocessing/_fine_cal.py index ca14c4de7e8..7b0492cdb2b 100644 --- a/mne/preprocessing/_fine_cal.py +++ b/mne/preprocessing/_fine_cal.py @@ -512,7 +512,7 @@ def read_fine_calibration(fname): fname = _check_fname(fname, overwrite="read", must_exist=True) check_fname(fname, "cal", (".dat",)) ch_names, locs, imb_cals = list(), list(), list() - with open(fname, "r") as fid: + with open(fname) as fid: for line in fid: if line[0] in "#\n": continue diff --git a/mne/preprocessing/eyetracking/_pupillometry.py b/mne/preprocessing/eyetracking/_pupillometry.py index b1d544f24ab..956c37cb114 100644 --- a/mne/preprocessing/eyetracking/_pupillometry.py +++ b/mne/preprocessing/eyetracking/_pupillometry.py @@ -60,14 +60,14 @@ def interpolate_blinks(raw, buffer=0.05, match="BAD_blink", interpolate_gaze=Fal # get the blink annotations blink_annots = [annot for annot in raw.annotations if annot["description"] in match] if not blink_annots: - warn("No annotations matching {} found. Aborting.".format(match)) + warn(f"No annotations matching {match} found. Aborting.") return raw _interpolate_blinks(raw, buffer, blink_annots, interpolate_gaze=interpolate_gaze) # remove bad from the annotation description for desc in match: if desc.startswith("BAD_"): - logger.info("Removing 'BAD_' from {}.".format(desc)) + logger.info(f"Removing 'BAD_' from {desc}.") raw.annotations.rename({desc: desc.replace("BAD_", "")}) return raw diff --git a/mne/preprocessing/eyetracking/calibration.py b/mne/preprocessing/eyetracking/calibration.py index e405e72f9eb..84b53ee3006 100644 --- a/mne/preprocessing/eyetracking/calibration.py +++ b/mne/preprocessing/eyetracking/calibration.py @@ -219,6 +219,6 @@ def read_eyelink_calibration( each eye of every calibration that was performed during the recording session. """ fname = _check_fname(fname, overwrite="read", must_exist=True, name="fname") - logger.info("Reading calibration data from {}".format(fname)) + logger.info(f"Reading calibration data from {fname}") lines = fname.read_text(encoding="ASCII").splitlines() return _parse_calibration(lines, screen_size, screen_distance, screen_resolution) diff --git a/mne/preprocessing/ica.py b/mne/preprocessing/ica.py index 1290c3d1e5a..86dfbbf6793 100644 --- a/mne/preprocessing/ica.py +++ b/mne/preprocessing/ica.py @@ -16,7 +16,7 @@ from inspect import Parameter, isfunction, signature from numbers import Integral from time import time -from typing import Dict, List, Literal, Optional, Union +from typing import Literal, Optional, Union import numpy as np from scipy import linalg, stats @@ -508,13 +508,13 @@ def _get_infos_for_repr(self): class _InfosForRepr: fit_on: Optional[Literal["raw data", "epochs"]] fit_method: Literal["fastica", "infomax", "extended-infomax", "picard"] - fit_params: Dict[str, Union[str, float]] + fit_params: dict[str, Union[str, float]] fit_n_iter: Optional[int] fit_n_samples: Optional[int] fit_n_components: Optional[int] fit_n_pca_components: Optional[int] - ch_types: List[str] - excludes: List[str] + ch_types: list[str] + excludes: list[str] if self.current_fit == "unfitted": fit_on = None @@ -754,7 +754,7 @@ def fit( var_ord = var.argsort()[::-1] _sort_components(self, var_ord, copy=False) t_stop = time() - logger.info("Fitting ICA took {:.1f}s.".format(t_stop - t_start)) + logger.info(f"Fitting ICA took {t_stop - t_start:.1f}s.") return self def _reset(self): @@ -818,8 +818,8 @@ def _fit_epochs(self, epochs, picks, decim, verbose): """Aux method.""" if epochs.events.size == 0: raise RuntimeError( - "Tried to fit ICA with epochs, but none were " - 'found: epochs.events is "{}".'.format(epochs.events) + "Tried to fit ICA with epochs, but none were found: epochs.events is " + f'"{epochs.events}".' ) # this should be a copy (picks a list of int) @@ -1550,7 +1550,7 @@ def _find_bads_ch( elif measure == "correlation": this_idx = np.where(abs(scores[-1]) > threshold)[0] else: - raise ValueError("Unknown measure {}".format(measure)) + raise ValueError(f"Unknown measure {measure}") idx += [this_idx] self.labels_["%s/%i/" % (prefix, ii) + ch] = list(this_idx) @@ -3063,7 +3063,7 @@ def read_ica(fname, verbose=None): fid.close() - ica_init, ica_misc = [_deserialize(k) for k in (ica_init, ica_misc)] + ica_init, ica_misc = (_deserialize(k) for k in (ica_init, ica_misc)) n_pca_components = ica_init.pop("n_pca_components") current_fit = ica_init.pop("current_fit") max_pca_components = ica_init.pop("max_pca_components") @@ -3341,7 +3341,7 @@ def corrmap( template_fig, labelled_ics = None, None if plot is True: if is_subject: # plotting from an ICA object - ttl = "Template from subj. {}".format(str(template[0])) + ttl = f"Template from subj. {str(template[0])}" template_fig = icas[template[0]].plot_components( picks=template[1], ch_type=ch_type, @@ -3393,7 +3393,7 @@ def corrmap( # find iteration with highest avg correlation with target _, median_corr, _, max_corrs = paths[np.argmax([path[1] for path in paths])] - allmaps, indices, subjs, nones = [list() for _ in range(4)] + allmaps, indices, subjs, nones = (list() for _ in range(4)) logger.info("Median correlation with constructed map: %0.3f" % median_corr) del median_corr if plot is True: diff --git a/mne/preprocessing/nirs/tests/test_optical_density.py b/mne/preprocessing/nirs/tests/test_optical_density.py index 77d7a559bb9..4ac662e0c9a 100644 --- a/mne/preprocessing/nirs/tests/test_optical_density.py +++ b/mne/preprocessing/nirs/tests/test_optical_density.py @@ -52,7 +52,7 @@ def test_optical_density_manual(): test_tol = 0.01 raw = read_raw_nirx(fname_nirx, preload=True) # log(1) = 0 - raw._data[4] = np.ones((145)) + raw._data[4] = np.ones(145) # log(0.5)/-1 = 0.69 # log(1.5)/-1 = -0.40 test_data = np.tile([0.5, 1.5], 73)[:145] diff --git a/mne/preprocessing/tests/test_maxwell.py b/mne/preprocessing/tests/test_maxwell.py index 336a007dd16..bb7bea8ef84 100644 --- a/mne/preprocessing/tests/test_maxwell.py +++ b/mne/preprocessing/tests/test_maxwell.py @@ -1340,7 +1340,7 @@ def test_shielding_factor(tmp_path): assert counts[0] == 3 # Show it by rewriting the 3D as 1D and testing it temp_fname = tmp_path / "test_cal.dat" - with open(fine_cal_fname_3d, "r") as fid: + with open(fine_cal_fname_3d) as fid: with open(temp_fname, "w") as fid_out: for line in fid: fid_out.write(" ".join(line.strip().split(" ")[:14]) + "\n") diff --git a/mne/preprocessing/tests/test_xdawn.py b/mne/preprocessing/tests/test_xdawn.py index f56629db6db..03bc445f2a7 100644 --- a/mne/preprocessing/tests/test_xdawn.py +++ b/mne/preprocessing/tests/test_xdawn.py @@ -335,7 +335,7 @@ def _simulate_erplike_mixed_data(n_epochs=100, n_channels=10): events[:, 2] = y info = create_info( - ch_names=["C{:02d}".format(i) for i in range(n_channels)], + ch_names=[f"C{i:02d}" for i in range(n_channels)], ch_types=["eeg"] * n_channels, sfreq=sfreq, ) diff --git a/mne/preprocessing/xdawn.py b/mne/preprocessing/xdawn.py index ffb0cb0e5cd..d2e39a1f5ed 100644 --- a/mne/preprocessing/xdawn.py +++ b/mne/preprocessing/xdawn.py @@ -425,9 +425,7 @@ def __init__( self, n_components=2, signal_cov=None, correct_overlap="auto", reg=None ): """Init.""" - super(Xdawn, self).__init__( - n_components=n_components, signal_cov=signal_cov, reg=reg - ) + super().__init__(n_components=n_components, signal_cov=signal_cov, reg=reg) self.correct_overlap = _check_option( "correct_overlap", correct_overlap, ["auto", True, False] ) diff --git a/mne/report/report.py b/mne/report/report.py index f8243d8c820..6b83642ec8f 100644 --- a/mne/report/report.py +++ b/mne/report/report.py @@ -23,7 +23,7 @@ from io import BytesIO, StringIO from pathlib import Path from shutil import copyfile -from typing import Optional, Tuple +from typing import Optional import numpy as np @@ -302,11 +302,11 @@ class _ContentElement: name: str section: Optional[str] dom_id: str - tags: Tuple[str] + tags: tuple[str] html: str -def _check_tags(tags) -> Tuple[str]: +def _check_tags(tags) -> tuple[str]: # Must be iterable, but not a string if isinstance(tags, str): tags = (tags,) @@ -1005,7 +1005,7 @@ def _content_as_html(self): ] section_htmls = [el.html for el in section_elements] section_tags = tuple( - sorted((set([t for el in section_elements for t in el.tags]))) + sorted(set([t for el in section_elements for t in el.tags])) ) section_dom_id = self._get_dom_id( section=None, # root level of document diff --git a/mne/source_estimate.py b/mne/source_estimate.py index 19b23da7d60..35e14e2e769 100644 --- a/mne/source_estimate.py +++ b/mne/source_estimate.py @@ -1406,8 +1406,8 @@ def to_data_frame( kinds = ["VOL"] * len(self.vertices) if isinstance(self, (_BaseSurfaceSourceEstimate, _BaseMixedSourceEstimate)): kinds[:2] = ["LH", "RH"] - for ii, (kind, vertno) in enumerate(zip(kinds, self.vertices)): - col_names.extend(["{}_{}".format(kind, vert) for vert in vertno]) + for kind, vertno in zip(kinds, self.vertices): + col_names.extend([f"{kind}_{vert}" for vert in vertno]) # build DataFrame df = _build_data_frame( self, diff --git a/mne/source_space/_source_space.py b/mne/source_space/_source_space.py index 8ec15ad48b0..bc17b08e53b 100644 --- a/mne/source_space/_source_space.py +++ b/mne/source_space/_source_space.py @@ -289,7 +289,7 @@ class SourceSpaces(list): def __init__(self, source_spaces, info=None): # First check the types is actually a valid config _validate_type(source_spaces, list, "source_spaces") - super(SourceSpaces, self).__init__(source_spaces) # list + super().__init__(source_spaces) # list self.kind # will raise an error if there is a problem if info is None: self.info = dict() @@ -2333,7 +2333,7 @@ def _vol_vertex(width, height, jj, kk, pp): def _src_vol_dims(s): - w, h, d = [s[f"mri_{key}"] for key in ("width", "height", "depth")] + w, h, d = (s[f"mri_{key}"] for key in ("width", "height", "depth")) return w, h, d, np.prod([w, h, d]) @@ -2897,7 +2897,7 @@ def _get_vertex_map_nn( subjects_dir / s / "surf" / f"{hemi}.sphere.reg" for s in (subject_from, subject_to) ] - reg_fro, reg_to = [read_surface(r, return_dict=True)[-1] for r in regs] + reg_fro, reg_to = (read_surface(r, return_dict=True)[-1] for r in regs) if to_neighbor_tri is not None: reg_to["neighbor_tri"] = to_neighbor_tri if "neighbor_tri" not in reg_to: diff --git a/mne/stats/cluster_level.py b/mne/stats/cluster_level.py index 82bd0943c29..cca48ebdfee 100644 --- a/mne/stats/cluster_level.py +++ b/mne/stats/cluster_level.py @@ -1145,7 +1145,7 @@ def _check_fun(X, stat_fun, threshold, tail=0, kind="within"): threshold = -tstat.ppf(p_thresh, n_samples - 1) if np.sign(tail) < 0: threshold = -threshold - logger.info("Using a threshold of {:.6f}".format(threshold)) + logger.info(f"Using a threshold of {threshold:.6f}") stat_fun = ttest_1samp_no_p if stat_fun is None else stat_fun else: assert kind == "between" @@ -1161,7 +1161,7 @@ def _check_fun(X, stat_fun, threshold, tail=0, kind="within"): dfn = len(X) - 1 dfd = np.sum([len(x) for x in X]) - len(X) threshold = fstat.ppf(1.0 - p_thresh, dfn, dfd) - logger.info("Using a threshold of {:.6f}".format(threshold)) + logger.info(f"Using a threshold of {threshold:.6f}") stat_fun = f_oneway if stat_fun is None else stat_fun return stat_fun, threshold diff --git a/mne/stats/parametric.py b/mne/stats/parametric.py index e777bd7f53e..0da2d2d0732 100644 --- a/mne/stats/parametric.py +++ b/mne/stats/parametric.py @@ -197,14 +197,13 @@ def _map_effects(n_factors, effects): elif "*" in effects: pass # handle later else: - raise ValueError('"{}" is not a valid option for "effects"'.format(effects)) + raise ValueError(f'"{effects}" is not a valid option for "effects"') if isinstance(effects, list): bad_names = [e for e in effects if e not in factor_names] if len(bad_names) > 1: raise ValueError( - "Effect names: {} are not valid. They should " - "the first `n_factors` ({}) characters from the" - "alphabet".format(bad_names, n_factors) + f"Effect names: {bad_names} are not valid. They should consist of the " + f"first `n_factors` ({n_factors}) characters from the alphabet" ) indices = list(np.arange(2**n_factors - 1)) @@ -402,7 +401,7 @@ def f_mway_rm(data, factor_levels, effects="all", correction=False, return_pvals # numerical imprecision can cause eps=0.99999999999999989 # even with a single category, so never let our degrees of # freedom drop below 1. - df1, df2 = [np.maximum(d[None, :] * eps, 1.0) for d in (df1, df2)] + df1, df2 = (np.maximum(d[None, :] * eps, 1.0) for d in (df1, df2)) if return_pvals: pvals = stats.f(df1, df2).sf(fvals) diff --git a/mne/stats/regression.py b/mne/stats/regression.py index 39bd8e63d95..762a250bc3b 100644 --- a/mne/stats/regression.py +++ b/mne/stats/regression.py @@ -266,7 +266,7 @@ def linear_regression_raw( """ if isinstance(solver, str): if solver not in {"cholesky"}: - raise ValueError("No such solver: {}".format(solver)) + raise ValueError(f"No such solver: {solver}") if solver == "cholesky": def solver(X, y): @@ -361,7 +361,7 @@ def _prepare_rerp_preds( else: tmin_s = {cond: int(round(tmin.get(cond, -0.1) * sfreq)) for cond in conds} if isinstance(tmax, (float, int)): - tmax_s = {cond: int(round((tmax * sfreq)) + 1) for cond in conds} + tmax_s = {cond: int(round(tmax * sfreq) + 1) for cond in conds} else: tmax_s = {cond: int(round(tmax.get(cond, 1.0) * sfreq)) + 1 for cond in conds} @@ -388,9 +388,9 @@ def _prepare_rerp_preds( covs = covariates[cond] if len(covs) != len(events): error = ( - "Condition {0} from ``covariates`` is " - "not the same length as ``events``" - ).format(cond) + f"Condition {cond} from ``covariates`` is not the same length as " + "``events``" + ) raise ValueError(error) onsets = -(events[np.where(covs != 0), 0] + tmin_)[0] v = np.asarray(covs)[np.nonzero(covs)].astype(float) diff --git a/mne/surface.py b/mne/surface.py index d9c6af696d7..d203a9ce00b 100644 --- a/mne/surface.py +++ b/mne/surface.py @@ -1032,7 +1032,7 @@ def _read_patch(fname): # This is adapted from PySurfer PR #269, Bruce Fischl's read_patch.m, # and PyCortex (BSD) patch = dict() - with open(fname, "r") as fid: + with open(fname) as fid: ver = np.fromfile(fid, dtype=">i4", count=1).item() if ver != -1: raise RuntimeError(f"incorrect version # {ver} (not -1) found") @@ -1802,7 +1802,7 @@ def read_tri(fname_in, swap=False, verbose=None): ----- .. versionadded:: 0.13.0 """ - with open(fname_in, "r") as fid: + with open(fname_in) as fid: lines = fid.readlines() n_nodes = int(lines[0]) n_tris = int(lines[n_nodes + 1]) @@ -1843,7 +1843,7 @@ def read_tri(fname_in, swap=False, verbose=None): def _get_solids(tri_rrs, fros): """Compute _sum_solids_div total angle in chunks.""" # NOTE: This incorporates the division by 4PI that used to be separate - tot_angle = np.zeros((len(fros))) + tot_angle = np.zeros(len(fros)) for ti in range(len(tri_rrs)): tri_rr = tri_rrs[ti] v1 = fros - tri_rr[0] diff --git a/mne/tests/test_annotations.py b/mne/tests/test_annotations.py index eae1000cbdd..8be52b60a9d 100644 --- a/mne/tests/test_annotations.py +++ b/mne/tests/test_annotations.py @@ -1206,7 +1206,7 @@ def test_date_none(tmp_path): n_chans = 139 n_samps = 20 data = np.random.random_sample((n_chans, n_samps)) - ch_names = ["E{}".format(x) for x in range(n_chans)] + ch_names = [f"E{x}" for x in range(n_chans)] ch_types = ["eeg"] * n_chans info = create_info(ch_names=ch_names, ch_types=ch_types, sfreq=2048) assert info["meas_date"] is None @@ -1252,7 +1252,7 @@ def test_crop_when_negative_orig_time(windows_like_datetime): assert len(annot) == 10 # Crop with negative tmin, tmax - tmin, tmax = [orig_time_stamp + t for t in (0.25, 0.75)] + tmin, tmax = (orig_time_stamp + t for t in (0.25, 0.75)) assert tmin < 0 and tmax < 0 crop_annot = annot.crop(tmin=tmin, tmax=tmax) assert_allclose(crop_annot.onset, [0.3, 0.4, 0.5, 0.6, 0.7]) @@ -1355,7 +1355,7 @@ def test_annotations_from_events(): # 4. Try passing callable # ------------------------------------------------------------------------- - event_desc = lambda d: "event{}".format(d) # noqa:E731 + event_desc = lambda d: f"event{d}" # noqa:E731 annots = annotations_from_events( events, sfreq=raw.info["sfreq"], diff --git a/mne/tests/test_docstring_parameters.py b/mne/tests/test_docstring_parameters.py index f42147f378f..9e59c7302e7 100644 --- a/mne/tests/test_docstring_parameters.py +++ b/mne/tests/test_docstring_parameters.py @@ -357,7 +357,7 @@ def test_docdict_order(): # read the file as text, and get entries via regex docs_path = Path(__file__).parents[1] / "utils" / "docs.py" assert docs_path.is_file(), docs_path - with open(docs_path, "r", encoding="UTF-8") as fid: + with open(docs_path, encoding="UTF-8") as fid: docs = fid.read() entries = re.findall(r'docdict\[(?:\n )?["\'](.+)["\']\n?\] = ', docs) # test length & uniqueness diff --git a/mne/tests/test_epochs.py b/mne/tests/test_epochs.py index c68fc7ce6bd..76172982da7 100644 --- a/mne/tests/test_epochs.py +++ b/mne/tests/test_epochs.py @@ -1786,7 +1786,7 @@ def _assert_splits(fname, n, size): bad_fname = next_fnames.pop(-1) for ii, this_fname in enumerate(next_fnames[:-1]): assert this_fname.is_file(), f"Missing file: {this_fname}" - with open(this_fname, "r") as fid: + with open(this_fname) as fid: fid.seek(0, 2) file_size = fid.tell() min_ = 0.1 if ii < len(next_fnames) - 1 else 0.1 diff --git a/mne/tests/test_rank.py b/mne/tests/test_rank.py index f726aed9a75..3832fe18bff 100644 --- a/mne/tests/test_rank.py +++ b/mne/tests/test_rank.py @@ -177,7 +177,7 @@ def test_cov_rank_estimation(rank_method, proj, meg): # count channel types ch_types = this_info.get_channel_types() - n_eeg, n_mag, n_grad = [ch_types.count(k) for k in ["eeg", "mag", "grad"]] + n_eeg, n_mag, n_grad = (ch_types.count(k) for k in ["eeg", "mag", "grad"]) n_meg = n_mag + n_grad has_sss = n_meg > 0 and len(this_info["proc_history"]) > 0 if has_sss: diff --git a/mne/time_frequency/_stockwell.py b/mne/time_frequency/_stockwell.py index f92cc02a804..26b25444abb 100644 --- a/mne/time_frequency/_stockwell.py +++ b/mne/time_frequency/_stockwell.py @@ -34,8 +34,8 @@ def _is_power_of_two(n): ) if n_times < n_fft: logger.info( - 'The input signal is shorter ({}) than "n_fft" ({}). ' - "Applying zero padding.".format(x_in.shape[-1], n_fft) + f'The input signal is shorter ({x_in.shape[-1]}) than "n_fft" ({n_fft}). ' + "Applying zero padding." ) zero_pad = n_fft - n_times pad_array = np.zeros(x_in.shape[:-1] + (zero_pad,), x_in.dtype) diff --git a/mne/time_frequency/csd.py b/mne/time_frequency/csd.py index ed395137103..cea14c9944a 100644 --- a/mne/time_frequency/csd.py +++ b/mne/time_frequency/csd.py @@ -189,11 +189,11 @@ def __repr__(self): # noqa: D105 elif len(f) == 1: freq_strs.append(str(f[0])) else: - freq_strs.append("{}-{}".format(np.min(f), np.max(f))) + freq_strs.append(f"{np.min(f)}-{np.max(f)}") freq_str = ", ".join(freq_strs) + " Hz." if self.tmin is not None and self.tmax is not None: - time_str = "{} to {} s".format(self.tmin, self.tmax) + time_str = f"{self.tmin} to {self.tmax} s" else: time_str = "unknown" diff --git a/mne/time_frequency/tests/test_stockwell.py b/mne/time_frequency/tests/test_stockwell.py index ffdde8bf24e..54d71b907ed 100644 --- a/mne/time_frequency/tests/test_stockwell.py +++ b/mne/time_frequency/tests/test_stockwell.py @@ -87,7 +87,7 @@ def test_stockwell_core(): width = 0.5 freqs = fftpack.fftfreq(len(pulse), 1.0 / sfreq) fmin, fmax = 1.0, 100.0 - start_f, stop_f = [np.abs(freqs - f).argmin() for f in (fmin, fmax)] + start_f, stop_f = (np.abs(freqs - f).argmin() for f in (fmin, fmax)) W = _precompute_st_windows(n_samp, start_f, stop_f, sfreq, width) st_pulse = _st(pulse, start_f, W) diff --git a/mne/time_frequency/tfr.py b/mne/time_frequency/tfr.py index ec53cd848f6..2e9d3102054 100644 --- a/mne/time_frequency/tfr.py +++ b/mne/time_frequency/tfr.py @@ -2254,7 +2254,7 @@ def _onselect( fig = figure_nobar() fig.suptitle( - "{:.2f} s - {:.2f} s, {:.2f} Hz - {:.2f} Hz".format(tmin, tmax, fmin, fmax), + f"{tmin:.2f} s - {tmax:.2f} s, {fmin:.2f} Hz - {fmax:.2f} Hz", y=0.04, ) @@ -2748,7 +2748,7 @@ def __init__( # check consistency: assert len(selection) == len(events) assert len(drop_log) >= len(events) - assert len(selection) == sum((len(dl) == 0 for dl in drop_log)) + assert len(selection) == sum(len(dl) == 0 for dl in drop_log) event_id = _check_event_id(event_id, events) self.data = data self._set_times(np.array(times, dtype=float)) @@ -3164,19 +3164,19 @@ def _get_timefreqs(tfr, timefreqs): if isinstance(timefreqs, dict): for k, v in timefreqs.items(): for item in (k, v): - if len(item) != 2 or any((not _is_numeric(n) for n in item)): + if len(item) != 2 or any(not _is_numeric(n) for n in item): raise ValueError(timefreq_error_msg, item) elif timefreqs is not None: if not hasattr(timefreqs, "__len__"): raise ValueError(timefreq_error_msg, timefreqs) - if len(timefreqs) == 2 and all((_is_numeric(v) for v in timefreqs)): + if len(timefreqs) == 2 and all(_is_numeric(v) for v in timefreqs): timefreqs = [tuple(timefreqs)] # stick a pair of numbers in a list else: for item in timefreqs: if ( hasattr(item, "__len__") and len(item) == 2 - and all((_is_numeric(n) for n in item)) + and all(_is_numeric(n) for n in item) ): pass else: diff --git a/mne/transforms.py b/mne/transforms.py index b8dcb1728ff..cb387582ef8 100644 --- a/mne/transforms.py +++ b/mne/transforms.py @@ -112,7 +112,7 @@ class Transform(dict): """ def __init__(self, fro, to, trans=None): - super(Transform, self).__init__() + super().__init__() # we could add some better sanity checks here fro = _to_const(fro) to = _to_const(to) @@ -237,13 +237,9 @@ def _find_trans(subject, subjects_dir=None): trans_fnames = glob.glob(str(subjects_dir / subject / "*-trans.fif")) if len(trans_fnames) < 1: - raise RuntimeError( - "Could not find the transformation for " "{subject}".format(subject=subject) - ) + raise RuntimeError(f"Could not find the transformation for {subject}") elif len(trans_fnames) > 1: - raise RuntimeError( - "Found multiple transformations for " "{subject}".format(subject=subject) - ) + raise RuntimeError(f"Found multiple transformations for {subject}") return Path(trans_fnames[0]) @@ -1554,7 +1550,7 @@ def read_ras_mni_t(subject, subjects_dir=None): def _read_fs_xfm(fname): """Read a Freesurfer transform from a .xfm file.""" assert fname.endswith(".xfm") - with open(fname, "r") as fid: + with open(fname) as fid: logger.debug("Reading FreeSurfer talairach.xfm file:\n%s" % fname) # read lines until we get the string 'Linear_Transform', which precedes diff --git a/mne/utils/check.py b/mne/utils/check.py index 467bd14e952..461d7e526a7 100644 --- a/mne/utils/check.py +++ b/mne/utils/check.py @@ -8,7 +8,7 @@ import operator import os import re -from builtins import input # no-op here but facilitates testing +from builtins import input # noqa: UP029 from difflib import get_close_matches from importlib import import_module from pathlib import Path @@ -431,8 +431,8 @@ def _check_pandas_index_arguments(index, valid): index = [index] if not isinstance(index, list): raise TypeError( - "index must be `None` or a string or list of strings," - " got type {}.".format(type(index)) + "index must be `None` or a string or list of strings, got type " + f"{type(index)}." ) invalid = set(index) - set(valid) if invalid: @@ -452,8 +452,8 @@ def _check_time_format(time_format, valid, meas_date=None): if time_format not in valid and time_format is not None: valid_str = '", "'.join(valid) raise ValueError( - '"{}" is not a valid time format. Valid options are ' - '"{}" and None.'.format(time_format, valid_str) + f'"{time_format}" is not a valid time format. Valid options are ' + f'"{valid_str}" and None.' ) # allow datetime only if meas_date available if time_format == "datetime" and meas_date is None: @@ -649,7 +649,7 @@ def _path_like(item): def _check_if_nan(data, msg=" to be plotted"): """Raise if any of the values are NaN.""" if not np.isfinite(data).all(): - raise ValueError("Some of the values {} are NaN.".format(msg)) + raise ValueError(f"Some of the values {msg} are NaN.") @verbose diff --git a/mne/utils/config.py b/mne/utils/config.py index 62b4d053012..e74a61ccd9d 100644 --- a/mne/utils/config.py +++ b/mne/utils/config.py @@ -217,7 +217,7 @@ def set_memmap_min_size(memmap_min_size): def _load_config(config_path, raise_error=False): """Safely load a config file.""" - with open(config_path, "r") as fid: + with open(config_path) as fid: try: config = json.load(fid) except ValueError: diff --git a/mne/utils/docs.py b/mne/utils/docs.py index 1fa26fa16dd..ec9fe66bae0 100644 --- a/mne/utils/docs.py +++ b/mne/utils/docs.py @@ -1210,20 +1210,20 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): from the filename extension. See supported formats above for more information.""" -docdict["export_fmt_params_epochs"] = """ +docdict["export_fmt_params_epochs"] = f""" fmt : 'auto' | 'eeglab' - {} -""".format(_export_fmt_params_base) + {_export_fmt_params_base} +""" -docdict["export_fmt_params_evoked"] = """ +docdict["export_fmt_params_evoked"] = f""" fmt : 'auto' | 'mff' - {} -""".format(_export_fmt_params_base) + {_export_fmt_params_base} +""" -docdict["export_fmt_params_raw"] = """ +docdict["export_fmt_params_raw"] = f""" fmt : 'auto' | 'brainvision' | 'edf' | 'eeglab' - {} -""".format(_export_fmt_params_base) + {_export_fmt_params_base} +""" docdict["export_fmt_support_epochs"] = """\ Supported formats: diff --git a/mne/utils/misc.py b/mne/utils/misc.py index 3dbff7b2bc5..3f342c80570 100644 --- a/mne/utils/misc.py +++ b/mne/utils/misc.py @@ -156,20 +156,7 @@ def run_subprocess(command, return_code=False, verbose=None, *args, **kwargs): break else: out = out.decode("utf-8") - # Strip newline at end of the string, otherwise we'll end - # up with two subsequent newlines (as the logger adds one) - # - # XXX Once we drop support for Python <3.9, uncomment the - # following line and remove the if/else block below. - # - # log_out = out.removesuffix('\n') - if sys.version_info[:2] >= (3, 9): - log_out = out.removesuffix("\n") - elif out.endswith("\n"): - log_out = out[:-1] - else: - log_out = out - + log_out = out.removesuffix("\n") logger.info(log_out) all_out += out @@ -180,19 +167,7 @@ def run_subprocess(command, return_code=False, verbose=None, *args, **kwargs): break else: err = err.decode("utf-8") - # Strip newline at end of the string, otherwise we'll end - # up with two subsequent newlines (as the logger adds one) - # - # XXX Once we drop support for Python <3.9, uncomment the - # following line and remove the if/else block below. - # - # err_out = err.removesuffix('\n') - if sys.version_info[:2] >= (3, 9): - err_out = err.removesuffix("\n") - elif err.endswith("\n"): - err_out = err[:-1] - else: - err_out = err + err_out = err.removesuffix("\n") # Leave this as logger.warning rather than warn(...) to # mirror the logger.info above for stdout. This function diff --git a/mne/utils/progressbar.py b/mne/utils/progressbar.py index 14429cb9033..b1938c2fac3 100644 --- a/mne/utils/progressbar.py +++ b/mne/utils/progressbar.py @@ -137,8 +137,7 @@ def update_with_increment_value(self, increment_value): def __iter__(self): """Iterate to auto-increment the pbar with 1.""" - for x in self._tqdm: - yield x + yield from self._tqdm def subset(self, idx): """Make a joblib-friendly index subset updater. @@ -188,7 +187,7 @@ def __del__(self): class _UpdateThread(Thread): def __init__(self, pb): - super(_UpdateThread, self).__init__(daemon=True) + super().__init__(daemon=True) self._mne_run = True self._mne_pb = pb diff --git a/mne/utils/tests/test_logging.py b/mne/utils/tests/test_logging.py index 2128140dcf5..8a19d76a089 100644 --- a/mne/utils/tests/test_logging.py +++ b/mne/utils/tests/test_logging.py @@ -84,11 +84,11 @@ def test_logging_options(tmp_path): with pytest.raises(ValueError, match="Invalid value for the 'verbose"): set_log_level("foo") test_name = tmp_path / "test.log" - with open(fname_log, "r") as old_log_file: + with open(fname_log) as old_log_file: # [:-1] used to strip an extra "No baseline correction applied" old_lines = clean_lines(old_log_file.readlines()) old_lines.pop(-1) - with open(fname_log_2, "r") as old_log_file_2: + with open(fname_log_2) as old_log_file_2: old_lines_2 = clean_lines(old_log_file_2.readlines()) old_lines_2.pop(14) old_lines_2.pop(-1) @@ -112,7 +112,7 @@ def test_logging_options(tmp_path): assert fid.readlines() == [] # SHOULD print evoked = read_evokeds(fname_evoked, condition=1, verbose=True) - with open(test_name, "r") as new_log_file: + with open(test_name) as new_log_file: new_lines = clean_lines(new_log_file.readlines()) assert new_lines == old_lines set_log_file(None) # Need to do this to close the old file @@ -131,7 +131,7 @@ def test_logging_options(tmp_path): assert fid.readlines() == [] # SHOULD print evoked = read_evokeds(fname_evoked, condition=1) - with open(test_name, "r") as new_log_file: + with open(test_name) as new_log_file: new_lines = clean_lines(new_log_file.readlines()) assert new_lines == old_lines # check to make sure appending works (and as default, raises a warning) @@ -139,7 +139,7 @@ def test_logging_options(tmp_path): with pytest.warns(RuntimeWarning, match="appended to the file"): set_log_file(test_name) evoked = read_evokeds(fname_evoked, condition=1) - with open(test_name, "r") as new_log_file: + with open(test_name) as new_log_file: new_lines = clean_lines(new_log_file.readlines()) assert new_lines == old_lines_2 @@ -148,7 +148,7 @@ def test_logging_options(tmp_path): # this line needs to be called to actually do some logging evoked = read_evokeds(fname_evoked, condition=1) del evoked - with open(test_name, "r") as new_log_file: + with open(test_name) as new_log_file: new_lines = clean_lines(new_log_file.readlines()) assert new_lines == old_lines with catch_logging() as log: diff --git a/mne/viz/_3d.py b/mne/viz/_3d.py index b67b9f49096..a9d44ce9ad5 100644 --- a/mne/viz/_3d.py +++ b/mne/viz/_3d.py @@ -2197,9 +2197,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" " {} was given.".format(type(brain)) - ) + raise TypeError("Expected type is Brain but" f" {type(brain)} was given.") # enable time viewer if necessary brain.setup_time_viewer() subjects = [brain._subject for brain in brains] diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 69f70786645..09e27a96408 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -1573,7 +1573,7 @@ def plot_time_course(self, hemi, vertex_id, color, update=True): mni = " MNI: " + ", ".join("%5.1f" % m for m in mni) else: mni = "" - label = "{}:{}{}".format(hemi_str, str(vertex_id).ljust(6), mni) + label = f"{hemi_str}:{str(vertex_id).ljust(6)}{mni}" act_data, smooth = self.act_data_smooth[hemi] if smooth is not None: act_data = smooth[vertex_id].dot(act_data)[0] @@ -1907,8 +1907,8 @@ def add_data( if array.ndim == 3: if array.shape[1] != 3: raise ValueError( - "If array has 3 dimensions, array.shape[1] " - "must equal 3, got %s" % (array.shape[1],) + "If array has 3 dimensions, array.shape[1] must equal 3, got " + f"{array.shape[1]}" ) fmin, fmid, fmax = _update_limits(fmin, fmid, fmax, center, array) if colormap == "auto": @@ -1921,13 +1921,13 @@ def add_data( elif isinstance(smoothing_steps, int): if smoothing_steps < 0: raise ValueError( - "Expected value of `smoothing_steps` is" - " positive but {} was given.".format(smoothing_steps) + "Expected value of `smoothing_steps` is positive but " + f"{smoothing_steps} was given." ) else: raise TypeError( - "Expected type of `smoothing_steps` is int or" - " NoneType but {} was given.".format(type(smoothing_steps)) + "Expected type of `smoothing_steps` is int or NoneType but " + f"{type(smoothing_steps)} was given." ) self._data["stc"] = stc diff --git a/mne/viz/_brain/surface.py b/mne/viz/_brain/surface.py index f4625c5f019..b2b79f2bf7c 100644 --- a/mne/viz/_brain/surface.py +++ b/mne/viz/_brain/surface.py @@ -184,7 +184,7 @@ def load_curvature(self): else: self.curv = None self.bin_curv = None - color = np.ones((self.coords.shape[0])) + color = np.ones(self.coords.shape[0]) # morphometry (curvature) normalization in order to get gray cortex # TODO: delete self.grey_curv after cortex parameter # will be fully supported diff --git a/mne/viz/_brain/tests/test_brain.py b/mne/viz/_brain/tests/test_brain.py index f233f83389d..ea470937300 100644 --- a/mne/viz/_brain/tests/test_brain.py +++ b/mne/viz/_brain/tests/test_brain.py @@ -1362,7 +1362,7 @@ def _create_testing_brain( rng = np.random.RandomState(0) vertices = [s["vertno"] for s in sample_src] n_verts = sum(len(v) for v in vertices) - stc_data = np.zeros((n_verts * n_time)) + stc_data = np.zeros(n_verts * n_time) stc_size = stc_data.size stc_data[(rng.rand(stc_size // 20) * stc_size).astype(int)] = rng.rand( stc_data.size // 20 diff --git a/mne/viz/backends/_pyvista.py b/mne/viz/backends/_pyvista.py index b5d921f3968..3e5062b593b 100644 --- a/mne/viz/backends/_pyvista.py +++ b/mne/viz/backends/_pyvista.py @@ -756,9 +756,8 @@ def text2d( actor.GetTextProperty().SetJustificationToRight() else: raise ValueError( - "Expected values for `justification`" - "are `left`, `center` or `right` but " - "got {} instead.".format(justification) + "Expected values for `justification` are `left`, `center` or " + f"`right` but got {justification} instead." ) _hide_testing_actor(actor) return actor diff --git a/mne/viz/backends/_utils.py b/mne/viz/backends/_utils.py index e9e4ea439aa..e67656a25ed 100644 --- a/mne/viz/backends/_utils.py +++ b/mne/viz/backends/_utils.py @@ -68,8 +68,8 @@ def _check_color(color): raise ValueError("Values out of range [0.0, 1.0].") else: raise TypeError( - "Expected data type is `np.int64`, `np.int32`, or " - "`np.float64` but {} was given.".format(np_color.dtype) + "Expected data type is `np.int64`, `np.int32`, or `np.float64` but " + f"{np_color.dtype} was given." ) else: raise TypeError( @@ -353,7 +353,7 @@ def _qt_get_stylesheet(theme): stylesheet = qdarkstyle.load_stylesheet(klass) else: try: - file = open(theme, "r") + file = open(theme) except OSError: warn( "Requested theme file not found, will use light instead: " diff --git a/mne/viz/epochs.py b/mne/viz/epochs.py index e3ae7b28e6e..95989637523 100644 --- a/mne/viz/epochs.py +++ b/mne/viz/epochs.py @@ -286,8 +286,8 @@ def plot_epochs_image( if len(set(this_ch_type)) > 1: types = ", ".join(set(this_ch_type)) raise ValueError( - 'Cannot combine sensors of different types; "{}" ' - "contains types {}.".format(this_group, types) + f'Cannot combine sensors of different types; "{this_group}" contains ' + f"types {types}." ) # now we know they're all the same type... group_by[this_group] = dict( @@ -297,8 +297,8 @@ def plot_epochs_image( # are they trying to combine a single channel? if len(these_picks) < 2 and combine_given: warn( - 'Only one channel in group "{}"; cannot combine by method ' - '"{}".'.format(this_group, combine) + f'Only one channel in group "{this_group}"; cannot combine by method ' + f'"{combine}".' ) # check for compatible `fig` / `axes`; instantiate figs if needed; add @@ -437,13 +437,12 @@ def _validate_fig_and_axes(fig, axes, group_by, evoked, colorbar, clear=False): n_axes = 1 + int(evoked) + int(colorbar) ax_names = ("image", "evoked", "colorbar") ax_names = np.array(ax_names)[np.where([True, evoked, colorbar])] - prefix = "Since evoked={} and colorbar={}, ".format(evoked, colorbar) + prefix = f"Since evoked={evoked} and colorbar={colorbar}, " # got both fig and axes if fig is not None and axes is not None: raise ValueError( - 'At least one of "fig" or "axes" must be None; got ' - "fig={}, axes={}.".format(fig, axes) + f'At least one of "fig" or "axes" must be None; got fig={fig}, axes={axes}.' ) # got fig=None and axes=None: make fig(s) and axes @@ -516,14 +515,14 @@ def _validate_fig_and_axes(fig, axes, group_by, evoked, colorbar, clear=False): # group_by dict and the user won't have known what keys we chose. if set(axes) != set(group_by): raise ValueError( - 'If "axes" is a dict its keys ({}) must match ' - 'the keys in "group_by" ({}).'.format(list(axes), list(group_by)) + f'If "axes" is a dict its keys ({list(axes)}) must match the keys in ' + f'"group_by" ({list(group_by)}).' ) for this_group, this_axes_list in axes.items(): if len(this_axes_list) != n_axes: raise ValueError( - '{}each value in "axes" must be a list of {} ' - "axes, got {}.".format(prefix, n_axes, len(this_axes_list)) + f'{prefix}each value in "axes" must be a list of {n_axes} axes, got' + f" {len(this_axes_list)}." ) # NB: next line assumes all axes in each list are in same figure group_by[this_group]["fig"] = this_axes_list[0].get_figure() diff --git a/mne/viz/evoked.py b/mne/viz/evoked.py index b01092951e6..20bdd655a93 100644 --- a/mne/viz/evoked.py +++ b/mne/viz/evoked.py @@ -1481,7 +1481,7 @@ def plot_evoked_image( def _plot_update_evoked(params, bools): """Update the plot evoked lines.""" - picks, evoked = [params[k] for k in ("picks", "evoked")] + picks, evoked = (params[k] for k in ("picks", "evoked")) projs = [ proj for ii, proj in enumerate(params["projs"]) if ii in np.where(bools)[0] ] @@ -1695,10 +1695,10 @@ def whitened_gfp(x, rank=None): for ch, sub_picks in picks_list: this_rank = rank_[ch] - title = "{0} ({2}{1})".format( + title = "{} ({}{})".format( titles_[ch] if n_columns > 1 else ch, - this_rank, "rank " if n_columns > 1 else "", + this_rank, ) label = noise_cov.get("method", "empirical") @@ -1878,7 +1878,7 @@ def plot_evoked_joint( got_axes = False illegal_args = {"show", "times", "exclude"} for args in (ts_args, topomap_args): - if any((x in args for x in illegal_args)): + if any(x in args for x in illegal_args): raise ValueError( "Don't pass any of {} as *_args.".format(", ".join(list(illegal_args))) ) @@ -2106,8 +2106,8 @@ def _validate_style_keys_pce(styles, conditions, tags): styles = deepcopy(styles) if not set(styles).issubset(tags.union(conditions)): raise ValueError( - 'The keys in "styles" ({}) must match the keys in ' - '"evokeds" ({}).'.format(list(styles), conditions) + f'The keys in "styles" ({list(styles)}) must match the keys in ' + f'"evokeds" ({conditions}).' ) # make sure all the keys are in there for cond in conditions: @@ -2145,26 +2145,20 @@ def _validate_colors_pce(colors, cmap, conditions, tags): if isinstance(colors, (list, tuple, np.ndarray)): if len(conditions) > len(colors): raise ValueError( - "Trying to plot {} conditions, but there are only" - " {} colors{}. Please specify colors manually.".format( - len(conditions), len(colors), err_suffix - ) + f"Trying to plot {len(conditions)} conditions, but there are only " + f"{len(colors)} colors{err_suffix}. Please specify colors manually." ) colors = dict(zip(conditions, colors)) # should be a dict by now... if not isinstance(colors, dict): raise TypeError( - '"colors" must be a dict, list, or None; got {}.'.format( - type(colors).__name__ - ) + f'"colors" must be a dict, list, or None; got {type(colors).__name__}.' ) # validate color dict keys if not set(colors).issubset(tags.union(conditions)): raise ValueError( - 'If "colors" is a dict its keys ({}) must ' - 'match the keys/conditions in "evokeds" ({}).'.format( - list(colors), conditions - ) + f'If "colors" is a dict its keys ({list(colors)}) must match the ' + f'keys/conditions in "evokeds" ({conditions}).' ) # validate color dict values color_vals = list(colors.values()) @@ -2218,9 +2212,8 @@ def _validate_linestyles_pce(linestyles, conditions, tags): if isinstance(linestyles, (list, tuple, np.ndarray)): if len(conditions) > len(linestyles): raise ValueError( - "Trying to plot {} conditions, but there are " - "only {} linestyles. Please specify linestyles " - "manually.".format(len(conditions), len(linestyles)) + f"Trying to plot {len(conditions)} conditions, but there are only " + f"{len(linestyles)} linestyles. Please specify linestyles manually." ) linestyles = dict(zip(conditions, linestyles)) # should be a dict by now... @@ -2233,10 +2226,8 @@ def _validate_linestyles_pce(linestyles, conditions, tags): # validate linestyle dict keys if not set(linestyles).issubset(tags.union(conditions)): raise ValueError( - 'If "linestyles" is a dict its keys ({}) must ' - 'match the keys/conditions in "evokeds" ({}).'.format( - list(linestyles), conditions - ) + f'If "linestyles" is a dict its keys ({list(linestyles)}) must match the ' + f'keys/conditions in "evokeds" ({conditions}).' ) # normalize linestyle values (so we can accurately count unique linestyles # later). See https://github.com/matplotlib/matplotlib/blob/master/matplotlibrc.template#L131-L133 # noqa @@ -2500,7 +2491,7 @@ def _get_data_and_ci(evoked, combine, combine_func, picks, scaling=1, ci_fun=Non data = np.array([evk.data[picks] * scaling for evk in evoked]) # combine across sensors if combine is not None: - logger.info('combining channels using "{}"'.format(combine)) + logger.info(f'combining channels using "{combine}"') data = combine_func(data) # get confidence band if ci_fun is not None: @@ -2528,9 +2519,7 @@ def _get_ci_function_pce(ci, do_topo=False): return partial(_ci, ci=ci, method=method) else: raise TypeError( - '"ci" must be None, bool, float or callable, got {}'.format( - type(ci).__name__ - ) + f'"ci" must be None, bool, float or callable, got {type(ci).__name__}' ) @@ -2575,7 +2564,7 @@ def _title_helper_pce(title, picked_types, picks, ch_names, combine): if title is not None and len(title) and isinstance(combine, str) and do_combine: _comb = combine.upper() if combine == "gfp" else combine _comb = "std. dev." if _comb == "std" else _comb - title += " ({})".format(_comb) + title += f" ({_comb})" return title @@ -2861,7 +2850,7 @@ def plot_compare_evokeds( if not isinstance(evokeds, dict): raise TypeError( '"evokeds" must be a dict, list, or instance of ' - "mne.Evoked; got {}".format(type(evokeds).__name__) + f"mne.Evoked; got {type(evokeds).__name__}" ) evokeds = deepcopy(evokeds) # avoid modifying dict outside function scope for cond, evoked in evokeds.items(): diff --git a/mne/viz/montage.py b/mne/viz/montage.py index 18ff3e1c2d7..e51cbcfb762 100644 --- a/mne/viz/montage.py +++ b/mne/viz/montage.py @@ -72,9 +72,9 @@ def plot_montage( n_chans = pos.shape[0] n_dupes = dupes.shape[0] idx = np.setdiff1d(np.arange(len(pos)), dupes[:, 1]).tolist() - logger.info("{} duplicate electrode labels found:".format(n_dupes)) + logger.info(f"{n_dupes} duplicate electrode labels found:") logger.info(", ".join([ch_names[d[0]] + "/" + ch_names[d[1]] for d in dupes])) - logger.info("Plotting {} unique labels.".format(n_chans - n_dupes)) + logger.info(f"Plotting {n_chans - n_dupes} unique labels.") ch_names = [ch_names[i] for i in idx] ch_pos = dict(zip(ch_names, pos[idx, :])) # XXX: this might cause trouble if montage was originally in head diff --git a/mne/viz/tests/test_3d.py b/mne/viz/tests/test_3d.py index 8f6346549e2..2ff951200d0 100644 --- a/mne/viz/tests/test_3d.py +++ b/mne/viz/tests/test_3d.py @@ -125,7 +125,7 @@ def test_plot_sparse_source_estimates(renderer_interactive, brain_gc): vertices = [s["vertno"] for s in sample_src] n_time = 5 n_verts = sum(len(v) for v in vertices) - stc_data = np.zeros((n_verts * n_time)) + stc_data = np.zeros(n_verts * n_time) stc_size = stc_data.size stc_data[ (np.random.rand(stc_size // 20) * stc_size).astype(int) @@ -748,7 +748,7 @@ def test_process_clim_plot(renderer_interactive, brain_gc): vertices = [s["vertno"] for s in sample_src] n_time = 5 n_verts = sum(len(v) for v in vertices) - stc_data = np.random.RandomState(0).rand((n_verts * n_time)) + stc_data = np.random.RandomState(0).rand(n_verts * n_time) stc_data.shape = (n_verts, n_time) stc = SourceEstimate(stc_data, vertices, 1, 1, "sample") @@ -870,7 +870,7 @@ def test_stc_mpl(): vertices = [s["vertno"] for s in sample_src] n_time = 5 n_verts = sum(len(v) for v in vertices) - stc_data = np.ones((n_verts * n_time)) + stc_data = np.ones(n_verts * n_time) stc_data.shape = (n_verts, n_time) stc = SourceEstimate(stc_data, vertices, 1, 1, "sample") stc.plot( @@ -1198,7 +1198,7 @@ def test_link_brains(renderer_interactive): vertices = [s["vertno"] for s in sample_src] n_time = 5 n_verts = sum(len(v) for v in vertices) - stc_data = np.zeros((n_verts * n_time)) + stc_data = np.zeros(n_verts * n_time) stc_size = stc_data.size stc_data[ (np.random.rand(stc_size // 20) * stc_size).astype(int) diff --git a/mne/viz/tests/test_evoked.py b/mne/viz/tests/test_evoked.py index b6fe6d87511..999260465fd 100644 --- a/mne/viz/tests/test_evoked.py +++ b/mne/viz/tests/test_evoked.py @@ -423,7 +423,7 @@ def test_plot_compare_evokeds(): red.data *= 1.5 blue.data /= 1.5 evoked_dict = {"aud/l": blue, "aud/r": red, "vis": evoked} - huge_dict = {"cond{}".format(i): ev for i, ev in enumerate([evoked] * 11)} + huge_dict = {f"cond{i}": ev for i, ev in enumerate([evoked] * 11)} plot_compare_evokeds(evoked_dict) # dict plot_compare_evokeds([[red, evoked], [blue, evoked]]) # list of lists figs = plot_compare_evokeds({"cond": [blue, red, evoked]}) # dict of list diff --git a/mne/viz/topomap.py b/mne/viz/topomap.py index 4c66d58b80e..489f4cb62d8 100644 --- a/mne/viz/topomap.py +++ b/mne/viz/topomap.py @@ -1250,9 +1250,9 @@ def _plot_topomap( ) if pos.ndim != 2: error = ( - "{ndim}D array supplied as electrode positions, where a 2D " - "array was expected" - ).format(ndim=pos.ndim) + f"{pos.ndim}D array supplied as electrode positions, where a 2D array was " + "expected" + ) raise ValueError(error + " " + pos_help) elif pos.shape[1] == 3: error = ( @@ -3450,10 +3450,10 @@ def _plot_corrmap( for ii, data_, ax, subject, idx in zip(picks, data, axes, subjs, indices): if template: - ttl = "Subj. {}, {}".format(subject, ica._ica_names[idx]) + ttl = f"Subj. {subject}, {ica._ica_names[idx]}" ax.set_title(ttl, fontsize=12) else: - ax.set_title("Subj. {}".format(subject)) + ax.set_title(f"Subj. {subject}") if merge_channels: data_, _ = _merge_ch_data(data_, ch_type, []) _vlim = _setup_vmin_vmax(data_, None, None) diff --git a/mne/viz/ui_events.py b/mne/viz/ui_events.py index 231776c9165..b28754664d4 100644 --- a/mne/viz/ui_events.py +++ b/mne/viz/ui_events.py @@ -15,7 +15,7 @@ import re import weakref from dataclasses import dataclass -from typing import List, Optional, Union +from typing import Optional, Union from matplotlib.colors import Colormap @@ -205,7 +205,7 @@ class Contours(UIEvent): """ kind: str - contours: List[str] + contours: list[str] def _get_event_channel(fig): diff --git a/mne/viz/utils.py b/mne/viz/utils.py index 180d2b37595..6df8b210c00 100644 --- a/mne/viz/utils.py +++ b/mne/viz/utils.py @@ -1415,7 +1415,7 @@ def _compute_scalings(scalings, inst, remove_dc=False, duration=10): time_middle = np.mean(inst.times) tmin = np.clip(time_middle - n_secs / 2.0, inst.times.min(), None) tmax = np.clip(time_middle + n_secs / 2.0, None, inst.times.max()) - smin, smax = [int(round(x * inst.info["sfreq"])) for x in (tmin, tmax)] + smin, smax = (int(round(x * inst.info["sfreq"])) for x in (tmin, tmax)) data = inst._read_segment(smin, smax) elif isinstance(inst, BaseEpochs): # Load a random subset of epochs up to 100mb in size @@ -2350,9 +2350,8 @@ def _make_combine_callable(combine): combine = combine_dict[combine] except KeyError: raise ValueError( - '"combine" must be None, a callable, or one of ' - '"mean", "median", "std", or "gfp"; got {}' - "".format(combine) + '"combine" must be None, a callable, or one of "mean", "median", "std",' + f' or "gfp"; got {combine}' ) return combine diff --git a/pyproject.toml b/pyproject.toml index 0b90b4a4e69..abb2e1b3f1b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -204,12 +204,13 @@ builtin = "clear,rare,informal,names,usage" skip = "doc/references.bib" [tool.ruff] -select = ["E", "F", "W", "D", "I"] +select = ["E", "F", "W", "D", "I", "UP"] exclude = ["__init__.py", "constants.py", "resources.py"] ignore = [ "D100", # Missing docstring in public module "D104", # Missing docstring in public package "D413", # Missing blank line after last section + "UP031", # Use format specifiers instead of percent format ] [tool.ruff.pydocstyle] diff --git a/tutorials/clinical/60_sleep.py b/tutorials/clinical/60_sleep.py index 25273a0ff2f..17e7a69ddbf 100644 --- a/tutorials/clinical/60_sleep.py +++ b/tutorials/clinical/60_sleep.py @@ -307,7 +307,7 @@ def eeg_power_band(epochs): y_test = epochs_test.events[:, 2] acc = accuracy_score(y_test, y_pred) -print("Accuracy score: {}".format(acc)) +print(f"Accuracy score: {acc}") ############################################################################## # In short, yes. We can predict Bob's sleeping stages based on Alice's data. diff --git a/tutorials/epochs/10_epochs_overview.py b/tutorials/epochs/10_epochs_overview.py index 54a99f9f149..7726bc754a2 100644 --- a/tutorials/epochs/10_epochs_overview.py +++ b/tutorials/epochs/10_epochs_overview.py @@ -314,9 +314,7 @@ shorter_epochs = epochs.copy().crop(tmin=-0.1, tmax=0.1, include_tmax=True) for name, obj in dict(Original=epochs, Cropped=shorter_epochs).items(): - print( - "{} epochs has {} time samples".format(name, obj.get_data(copy=False).shape[-1]) - ) + print(f"{name} epochs has {obj.get_data(copy=False).shape[-1]} time samples") # %% # Cropping removed part of the baseline. When printing the @@ -370,7 +368,7 @@ channel_4_6_8 = epochs.get_data(picks=slice(4, 9, 2)) for name, arr in dict(EOG=eog_data, MEG=meg_data, Slice=channel_4_6_8).items(): - print("{} contains {} channels".format(name, arr.shape[1])) + print(f"{name} contains {arr.shape[1]} channels") # %% # Note that if your analysis requires repeatedly extracting single epochs from diff --git a/tutorials/epochs/30_epochs_metadata.py b/tutorials/epochs/30_epochs_metadata.py index 51d551090d4..42fa1219b52 100644 --- a/tutorials/epochs/30_epochs_metadata.py +++ b/tutorials/epochs/30_epochs_metadata.py @@ -123,7 +123,7 @@ # plotting: words = ["typhoon", "bungalow", "colossus", "drudgery", "linguist", "solenoid"] -epochs["WORD in {}".format(words)].plot(n_channels=29, events=True) +epochs[f"WORD in {words}"].plot(n_channels=29, events=True) # %% # Notice that in this dataset, each "condition" (A.K.A., each word) occurs only diff --git a/tutorials/evoked/10_evoked_overview.py b/tutorials/evoked/10_evoked_overview.py index a2513ea1e27..9116bb19ea6 100644 --- a/tutorials/evoked/10_evoked_overview.py +++ b/tutorials/evoked/10_evoked_overview.py @@ -336,11 +336,7 @@ channel, latency, value = trial.get_peak(ch_type="eeg", return_amplitude=True) latency = int(round(latency * 1e3)) # convert to milliseconds value = int(round(value * 1e6)) # convert to µV - print( - "Trial {}: peak of {} µV at {} ms in channel {}".format( - ix, value, latency, channel - ) - ) + print(f"Trial {ix}: peak of {value} µV at {latency} ms in channel {channel}") # %% # .. REFERENCES diff --git a/tutorials/forward/20_source_alignment.py b/tutorials/forward/20_source_alignment.py index c1ff697f9ce..f14b556f165 100644 --- a/tutorials/forward/20_source_alignment.py +++ b/tutorials/forward/20_source_alignment.py @@ -121,8 +121,8 @@ ) dists = mne.dig_mri_distances(raw.info, trans, "sample", subjects_dir=subjects_dir) print( - "Distance from %s digitized points to head surface: %0.1f mm" - % (len(dists), 1000 * np.mean(dists)) + f"Distance from {len(dists)} digitized points to head surface: " + f"{1000 * np.mean(dists):0.1f} mm" ) # %% diff --git a/tutorials/inverse/10_stc_class.py b/tutorials/inverse/10_stc_class.py index 4330daa41ed..8638b4eaf2a 100644 --- a/tutorials/inverse/10_stc_class.py +++ b/tutorials/inverse/10_stc_class.py @@ -118,7 +118,7 @@ shape = stc.data.shape -print("The data has %s vertex locations with %s sample points each." % shape) +print(f"The data has {shape} vertex locations with {shape} sample points each.") # %% # We see that stc carries 7498 time series of 25 samples length. Those time @@ -140,7 +140,8 @@ shape_lh = stc.lh_data.shape print( - "The left hemisphere has %s vertex locations with %s sample points each." % shape_lh + f"The left hemisphere has {shape_lh} vertex locations with {shape_lh} sample points" + " each." ) # %% diff --git a/tutorials/inverse/20_dipole_fit.py b/tutorials/inverse/20_dipole_fit.py index bf40d55e4ea..958ff809ede 100644 --- a/tutorials/inverse/20_dipole_fit.py +++ b/tutorials/inverse/20_dipole_fit.py @@ -92,8 +92,8 @@ best_idx = np.argmax(dip.gof) best_time = dip.times[best_idx] print( - "Highest GOF %0.1f%% at t=%0.1f ms with confidence volume %0.1f cm^3" - % (dip.gof[best_idx], best_time * 1000, dip.conf["vol"][best_idx] * 100**3) + f"Highest GOF {dip.gof[best_idx]:0.1f}% at t={best_time * 1000:0.1f} ms with " + f"confidence volume {dip.conf['vol'][best_idx] * 100**3:0.1f} cm^3" ) # remember to create a subplot for the colorbar fig, axes = plt.subplots( diff --git a/tutorials/inverse/80_brainstorm_phantom_elekta.py b/tutorials/inverse/80_brainstorm_phantom_elekta.py index 303be4260d1..ed9a14fc56f 100644 --- a/tutorials/inverse/80_brainstorm_phantom_elekta.py +++ b/tutorials/inverse/80_brainstorm_phantom_elekta.py @@ -151,19 +151,19 @@ ) diffs = 1000 * np.sqrt(np.sum((dip.pos - actual_pos) ** 2, axis=-1)) -print("mean(position error) = %0.1f mm" % (np.mean(diffs),)) +print(f"mean(position error) = {np.mean(diffs):0.1f} mm") ax1.bar(event_id, diffs) ax1.set_xlabel("Dipole index") ax1.set_ylabel("Loc. error (mm)") angles = np.rad2deg(np.arccos(np.abs(np.sum(dip.ori * actual_ori, axis=1)))) -print("mean(angle error) = %0.1f°" % (np.mean(angles),)) +print(f"mean(angle error) = {np.mean(angles):0.1f}°") ax2.bar(event_id, angles) ax2.set_xlabel("Dipole index") ax2.set_ylabel("Angle error (°)") amps = actual_amp - dip.amplitude / 1e-9 -print("mean(abs amplitude error) = %0.1f nAm" % (np.mean(np.abs(amps)),)) +print(f"mean(abs amplitude error) = {np.mean(np.abs(amps)):0.1f} nAm") ax3.bar(event_id, amps) ax3.set_xlabel("Dipole index") ax3.set_ylabel("Amplitude error (nAm)") diff --git a/tutorials/io/60_ctf_bst_auditory.py b/tutorials/io/60_ctf_bst_auditory.py index 450b8237db4..a9d86594669 100644 --- a/tutorials/io/60_ctf_bst_auditory.py +++ b/tutorials/io/60_ctf_bst_auditory.py @@ -105,7 +105,7 @@ for idx in [1, 2]: csv_fname = data_path / "MEG" / "bst_auditory" / f"events_bad_0{idx}.csv" df = pd.read_csv(csv_fname, header=None, names=["onset", "duration", "id", "label"]) - print("Events from run {0}:".format(idx)) + print(f"Events from run {idx}:") print(df) df["onset"] += offset * (idx - 1) @@ -208,9 +208,7 @@ onsets = onsets[diffs > min_diff] assert len(onsets) == len(events) diffs = 1000.0 * (events[:, 0] - onsets) / raw.info["sfreq"] -print( - "Trigger delay removed (μ ± σ): %0.1f ± %0.1f ms" % (np.mean(diffs), np.std(diffs)) -) +print(f"Trigger delay removed (μ ± σ): {np.mean(diffs):0.1f} ± {np.std(diffs):0.1f} ms") events[:, 0] = onsets del sound_data, diffs diff --git a/tutorials/machine-learning/30_strf.py b/tutorials/machine-learning/30_strf.py index a838ae0018c..4d8acad03c2 100644 --- a/tutorials/machine-learning/30_strf.py +++ b/tutorials/machine-learning/30_strf.py @@ -170,9 +170,9 @@ # Create training and testing data train, test = np.arange(n_epochs - 1), n_epochs - 1 X_train, X_test, y_train, y_test = X[train], X[test], y[train], y[test] -X_train, X_test, y_train, y_test = [ +X_train, X_test, y_train, y_test = ( np.rollaxis(ii, -1, 0) for ii in (X_train, X_test, y_train, y_test) -] +) # Model the simulated data as a function of the spectrogram input alphas = np.logspace(-3, 3, 7) scores = np.zeros_like(alphas) diff --git a/tutorials/machine-learning/50_decoding.py b/tutorials/machine-learning/50_decoding.py index 06d34bd49c8..10fa044281b 100644 --- a/tutorials/machine-learning/50_decoding.py +++ b/tutorials/machine-learning/50_decoding.py @@ -145,7 +145,7 @@ # Mean scores across cross-validation splits score = np.mean(scores, axis=0) -print("Spatio-temporal: %0.1f%%" % (100 * score,)) +print(f"Spatio-temporal: {100 * score:0.1f}%") # %% # PSDEstimator @@ -224,7 +224,7 @@ csp = CSP(n_components=3, norm_trace=False) clf_csp = make_pipeline(csp, LinearModel(LogisticRegression(solver="liblinear"))) scores = cross_val_multiscore(clf_csp, X, y, cv=5, n_jobs=None) -print("CSP: %0.1f%%" % (100 * scores.mean(),)) +print(f"CSP: {100 * scores.mean():0.1f}%") # %% # Source power comodulation (SPoC) diff --git a/tutorials/preprocessing/25_background_filtering.py b/tutorials/preprocessing/25_background_filtering.py index 948ab43d76f..cbd10ab213b 100644 --- a/tutorials/preprocessing/25_background_filtering.py +++ b/tutorials/preprocessing/25_background_filtering.py @@ -250,7 +250,7 @@ freq = [0.0, f_p, f_s, nyq] gain = [1.0, 1.0, 0.0, 0.0] ax = plt.subplots(1, figsize=third_height)[1] -title = "%s Hz lowpass with a %s Hz transition" % (f_p, trans_bandwidth) +title = f"{f_p} Hz lowpass with a {trans_bandwidth} Hz transition" plot_ideal_filter(freq, gain, ax, title=title, flim=flim) # %% diff --git a/tutorials/preprocessing/30_filtering_resampling.py b/tutorials/preprocessing/30_filtering_resampling.py index a3be45e1ec2..cf9b3335949 100644 --- a/tutorials/preprocessing/30_filtering_resampling.py +++ b/tutorials/preprocessing/30_filtering_resampling.py @@ -78,9 +78,7 @@ duration=60, proj=False, n_channels=len(raw.ch_names), remove_dc=False ) fig.subplots_adjust(top=0.9) - fig.suptitle( - "High-pass filtered at {} Hz".format(cutoff), size="xx-large", weight="bold" - ) + fig.suptitle(f"High-pass filtered at {cutoff} Hz", size="xx-large", weight="bold") # %% # Looks like 0.1 Hz was not quite high enough to fully remove the slow drifts. @@ -164,7 +162,7 @@ def add_arrows(axes): fig = data.compute_psd(fmax=250).plot( average=True, amplitude=False, picks="data", exclude="bads" ) - fig.suptitle("{}filtered".format(title), size="xx-large", weight="bold") + fig.suptitle(f"{title}filtered", size="xx-large", weight="bold") add_arrows(fig.axes[:2]) # %% @@ -185,7 +183,7 @@ def add_arrows(axes): fig = data.compute_psd(fmax=250).plot( average=True, amplitude=False, picks="data", exclude="bads" ) - fig.suptitle("{}filtered".format(title), size="xx-large", weight="bold") + fig.suptitle(f"{title}filtered", size="xx-large", weight="bold") add_arrows(fig.axes[:2]) # %% diff --git a/tutorials/preprocessing/40_artifact_correction_ica.py b/tutorials/preprocessing/40_artifact_correction_ica.py index 6f21840fa30..3e0698a0efe 100644 --- a/tutorials/preprocessing/40_artifact_correction_ica.py +++ b/tutorials/preprocessing/40_artifact_correction_ica.py @@ -567,7 +567,7 @@ with mne.viz.use_browser_backend("matplotlib"): fig = ica.plot_sources(raw, show_scrollbars=False) fig.subplots_adjust(top=0.9) # make space for title - fig.suptitle("Subject {}".format(index)) + fig.suptitle(f"Subject {index}") # %% # Notice that subjects 2 and 3 each seem to have *two* ICs that reflect ocular diff --git a/tutorials/preprocessing/45_projectors_background.py b/tutorials/preprocessing/45_projectors_background.py index 0b11d168db4..00de570229b 100644 --- a/tutorials/preprocessing/45_projectors_background.py +++ b/tutorials/preprocessing/45_projectors_background.py @@ -372,7 +372,7 @@ def setup_3d_axes(): with mne.viz.use_browser_backend("matplotlib"): fig = mags.plot(butterfly=True, proj=proj) fig.subplots_adjust(top=0.9) - fig.suptitle("proj={}".format(proj), size="xx-large", weight="bold") + fig.suptitle(f"proj={proj}", size="xx-large", weight="bold") # %% # Additional ways of visualizing projectors are covered in the tutorial @@ -443,7 +443,7 @@ def setup_3d_axes(): with mne.viz.use_browser_backend("matplotlib"): fig = data.plot(butterfly=True, proj=True) fig.subplots_adjust(top=0.9) - fig.suptitle("{} ECG projector".format(title), size="xx-large", weight="bold") + fig.suptitle(f"{title} ECG projector", size="xx-large", weight="bold") # %% # When are projectors "applied"? diff --git a/tutorials/preprocessing/50_artifact_correction_ssp.py b/tutorials/preprocessing/50_artifact_correction_ssp.py index b99d068430b..bc0b9081f64 100644 --- a/tutorials/preprocessing/50_artifact_correction_ssp.py +++ b/tutorials/preprocessing/50_artifact_correction_ssp.py @@ -180,7 +180,7 @@ with mne.viz.use_browser_backend("matplotlib"): fig = raw.plot(proj=True, order=mags, duration=1, n_channels=2) fig.subplots_adjust(top=0.9) # make room for title - fig.suptitle("{} projectors".format(title), size="xx-large", weight="bold") + fig.suptitle(f"{title} projectors", size="xx-large", weight="bold") # %% # The effect is sometimes easier to see on averaged data. Here we use an @@ -347,7 +347,7 @@ with mne.viz.use_browser_backend("matplotlib"): fig = raw.plot(order=artifact_picks, n_channels=len(artifact_picks)) fig.subplots_adjust(top=0.9) # make room for title - fig.suptitle("{} ECG projectors".format(title), size="xx-large", weight="bold") + fig.suptitle(f"{title} ECG projectors", size="xx-large", weight="bold") # %% # Finally, note that above we passed ``reject=None`` to the @@ -459,7 +459,7 @@ with mne.viz.use_browser_backend("matplotlib"): fig = raw.plot(order=artifact_picks, n_channels=len(artifact_picks)) fig.subplots_adjust(top=0.9) # make room for title - fig.suptitle("{} EOG projectors".format(title), size="xx-large", weight="bold") + fig.suptitle(f"{title} EOG projectors", size="xx-large", weight="bold") # %% # Notice that the small peaks in the first to magnetometer channels (``MEG diff --git a/tutorials/preprocessing/55_setting_eeg_reference.py b/tutorials/preprocessing/55_setting_eeg_reference.py index 049e8f31a8b..dbc817dc2d7 100644 --- a/tutorials/preprocessing/55_setting_eeg_reference.py +++ b/tutorials/preprocessing/55_setting_eeg_reference.py @@ -27,7 +27,7 @@ ) raw = mne.io.read_raw_fif(sample_data_raw_file, verbose=False) raw.crop(tmax=60).load_data() -raw.pick(["EEG 0{:02}".format(n) for n in range(41, 60)]) +raw.pick([f"EEG 0{n:02}" for n in range(41, 60)]) # %% # Background @@ -176,7 +176,7 @@ fig = raw.plot(proj=proj, n_channels=len(raw)) # make room for title fig.subplots_adjust(top=0.9) - fig.suptitle("{} reference".format(title), size="xx-large", weight="bold") + fig.suptitle(f"{title} reference", size="xx-large", weight="bold") # %% # Using an infinite reference (REST) @@ -199,7 +199,7 @@ fig = _raw.plot(n_channels=len(raw), scalings=dict(eeg=5e-5)) # make room for title fig.subplots_adjust(top=0.9) - fig.suptitle("{} reference".format(title), size="xx-large", weight="bold") + fig.suptitle(f"{title} reference", size="xx-large", weight="bold") # %% # Using a bipolar reference diff --git a/tutorials/preprocessing/70_fnirs_processing.py b/tutorials/preprocessing/70_fnirs_processing.py index cf0b63da311..4c211c9a770 100644 --- a/tutorials/preprocessing/70_fnirs_processing.py +++ b/tutorials/preprocessing/70_fnirs_processing.py @@ -246,7 +246,7 @@ epochs["Tapping"].average().plot_image(axes=axes[:, 1], clim=clims) for column, condition in enumerate(["Control", "Tapping"]): for ax in axes[:, column]: - ax.set_title("{}: {}".format(condition, ax.get_title())) + ax.set_title(f"{condition}: {ax.get_title()}") # %% @@ -346,7 +346,7 @@ for column, condition in enumerate(["Tapping Left", "Tapping Right", "Left-Right"]): for row, chroma in enumerate(["HbO", "HbR"]): - axes[row, column].set_title("{}: {}".format(chroma, condition)) + axes[row, column].set_title(f"{chroma}: {condition}") # %% # Lastly, we can also look at the individual waveforms to see what is diff --git a/tutorials/raw/10_raw_overview.py b/tutorials/raw/10_raw_overview.py index 7b777046afc..dbfb2b28467 100644 --- a/tutorials/raw/10_raw_overview.py +++ b/tutorials/raw/10_raw_overview.py @@ -145,7 +145,7 @@ f"the (cropped) sample data object has {n_time_samps} time samples and " f"{n_chan} channels." ) -print("The last time sample is at {} seconds.".format(time_secs[-1])) +print(f"The last time sample is at {time_secs[-1]} seconds.") print("The first few channel names are {}.".format(", ".join(ch_names[:3]))) print() # insert a blank line in the output diff --git a/tutorials/raw/30_annotate_raw.py b/tutorials/raw/30_annotate_raw.py index 8a2a43d4188..99c40506b66 100644 --- a/tutorials/raw/30_annotate_raw.py +++ b/tutorials/raw/30_annotate_raw.py @@ -230,7 +230,7 @@ descr = ann["description"] start = ann["onset"] end = ann["onset"] + ann["duration"] - print("'{}' goes from {} to {}".format(descr, start, end)) + print(f"'{descr}' goes from {start} to {end}") # %% # Note that iterating, indexing and slicing `~mne.Annotations` all diff --git a/tutorials/stats-sensor-space/75_cluster_ftest_spatiotemporal.py b/tutorials/stats-sensor-space/75_cluster_ftest_spatiotemporal.py index c7fdbdb1fc2..fedd88a568f 100644 --- a/tutorials/stats-sensor-space/75_cluster_ftest_spatiotemporal.py +++ b/tutorials/stats-sensor-space/75_cluster_ftest_spatiotemporal.py @@ -231,7 +231,7 @@ # add new axis for time courses and plot time courses ax_signals = divider.append_axes("right", size="300%", pad=1.2) - title = "Cluster #{0}, {1} sensor".format(i_clu + 1, len(ch_inds)) + title = f"Cluster #{i_clu + 1}, {len(ch_inds)} sensor" if len(ch_inds) > 1: title += "s (mean)" plot_compare_evokeds( @@ -385,7 +385,7 @@ # add new axis for spectrogram ax_spec = divider.append_axes("right", size="300%", pad=1.2) - title = "Cluster #{0}, {1} spectrogram".format(i_clu + 1, len(ch_inds)) + title = f"Cluster #{i_clu + 1}, {len(ch_inds)} spectrogram" if len(ch_inds) > 1: title += " (max over channels)" F_obs_plot = F_obs[..., ch_inds].max(axis=-1) diff --git a/tutorials/time-freq/50_ssvep.py b/tutorials/time-freq/50_ssvep.py index 39113f08132..706841fefac 100644 --- a/tutorials/time-freq/50_ssvep.py +++ b/tutorials/time-freq/50_ssvep.py @@ -422,13 +422,13 @@ def snr_spectrum(psd, noise_n_neighbor_freqs=1, noise_skip_neighbor_freqs=1): mne.viz.plot_topomap(snrs_12hz_chaverage, epochs.info, vlim=(1, None), axes=ax) print("sub 2, 12 Hz trials, SNR at 12 Hz") -print("average SNR (all channels): %f" % snrs_12hz_chaverage.mean()) -print("average SNR (occipital ROI): %f" % snrs_target.mean()) +print(f"average SNR (all channels): {snrs_12hz_chaverage.mean()}") +print(f"average SNR (occipital ROI): {snrs_target.mean()}") tstat_roi_vs_scalp = ttest_rel(snrs_target.mean(axis=1), snrs_12hz.mean(axis=1)) print( - "12 Hz SNR in occipital ROI is significantly larger than 12 Hz SNR over " - "all channels: t = %.3f, p = %f" % tstat_roi_vs_scalp + "12 Hz SNR in occipital ROI is significantly larger than 12 Hz SNR over all " + f"channels: t = {tstat_roi_vs_scalp[0]:.3f}, p = {tstat_roi_vs_scalp[1]}" ) ############################################################################## @@ -520,24 +520,24 @@ def snr_spectrum(psd, noise_n_neighbor_freqs=1, noise_skip_neighbor_freqs=1): res["stim_12hz_snrs_12hz"], res["stim_12hz_snrs_15hz"] ) print( - "12 Hz Trials: 12 Hz SNR is significantly higher than 15 Hz SNR" - ": t = %.3f, p = %f" % tstat_12hz_trial_stim + "12 Hz Trials: 12 Hz SNR is significantly higher than 15 Hz SNR: t = " + f"{tstat_12hz_trial_stim[0]:.3f}, p = {tstat_12hz_trial_stim[1]}" ) tstat_12hz_trial_1st_harmonic = ttest_rel( res["stim_12hz_snrs_24hz"], res["stim_12hz_snrs_30hz"] ) print( - "12 Hz Trials: 24 Hz SNR is significantly higher than 30 Hz SNR" - ": t = %.3f, p = %f" % tstat_12hz_trial_1st_harmonic + "12 Hz Trials: 24 Hz SNR is significantly higher than 30 Hz SNR: t = " + f"{tstat_12hz_trial_1st_harmonic[0]:.3f}, p = {tstat_12hz_trial_1st_harmonic[1]}" ) tstat_12hz_trial_2nd_harmonic = ttest_rel( res["stim_12hz_snrs_36hz"], res["stim_12hz_snrs_45hz"] ) print( - "12 Hz Trials: 36 Hz SNR is significantly higher than 45 Hz SNR" - ": t = %.3f, p = %f" % tstat_12hz_trial_2nd_harmonic + "12 Hz Trials: 36 Hz SNR is significantly higher than 45 Hz SNR: t = " + f"{tstat_12hz_trial_2nd_harmonic[0]:.3f}, p = {tstat_12hz_trial_2nd_harmonic[1]}" ) print() @@ -545,24 +545,24 @@ def snr_spectrum(psd, noise_n_neighbor_freqs=1, noise_skip_neighbor_freqs=1): res["stim_15hz_snrs_12hz"], res["stim_15hz_snrs_15hz"] ) print( - "15 Hz trials: 12 Hz SNR is significantly lower than 15 Hz SNR" - ": t = %.3f, p = %f" % tstat_15hz_trial_stim + "15 Hz trials: 12 Hz SNR is significantly lower than 15 Hz SNR: t = " + f"{tstat_15hz_trial_stim[0]:.3f}, p = {tstat_15hz_trial_stim[1]}" ) tstat_15hz_trial_1st_harmonic = ttest_rel( res["stim_15hz_snrs_24hz"], res["stim_15hz_snrs_30hz"] ) print( - "15 Hz trials: 24 Hz SNR is significantly lower than 30 Hz SNR" - ": t = %.3f, p = %f" % tstat_15hz_trial_1st_harmonic + "15 Hz trials: 24 Hz SNR is significantly lower than 30 Hz SNR: t = " + f"{tstat_15hz_trial_1st_harmonic[0]:.3f}, p = {tstat_15hz_trial_1st_harmonic[1]}" ) tstat_15hz_trial_2nd_harmonic = ttest_rel( res["stim_15hz_snrs_36hz"], res["stim_15hz_snrs_45hz"] ) print( - "15 Hz trials: 36 Hz SNR is significantly lower than 45 Hz SNR" - ": t = %.3f, p = %f" % tstat_15hz_trial_2nd_harmonic + "15 Hz trials: 36 Hz SNR is significantly lower than 45 Hz SNR: t = " + f"{tstat_15hz_trial_2nd_harmonic[0]:.3f}, p = {tstat_15hz_trial_2nd_harmonic[1]}" ) ############################################################################## diff --git a/tutorials/visualization/10_publication_figure.py b/tutorials/visualization/10_publication_figure.py index 69edf301eb5..138f9165db1 100644 --- a/tutorials/visualization/10_publication_figure.py +++ b/tutorials/visualization/10_publication_figure.py @@ -108,7 +108,7 @@ axes, [screenshot, cropped_screenshot], ["Before", "After"] ): ax.imshow(image) - ax.set_title("{} cropping".format(title)) + ax.set_title(f"{title} cropping") # %% # A lot of figure settings can be adjusted after the figure is created, but From 6af181afff86c679e1ac6c94b65a7843e07ae923 Mon Sep 17 00:00:00 2001 From: Marijn van Vliet Date: Tue, 16 Jan 2024 10:44:25 +0200 Subject: [PATCH 153/405] Fix vmax slider for EvokedField figures (#12354) --- doc/changes/devel/12354.bugfix.rst | 1 + mne/viz/evoked_field.py | 34 ++++++++++++++---------------- mne/viz/tests/test_3d.py | 11 +++++++++- 3 files changed, 27 insertions(+), 19 deletions(-) create mode 100644 doc/changes/devel/12354.bugfix.rst diff --git a/doc/changes/devel/12354.bugfix.rst b/doc/changes/devel/12354.bugfix.rst new file mode 100644 index 00000000000..f3c944c9373 --- /dev/null +++ b/doc/changes/devel/12354.bugfix.rst @@ -0,0 +1 @@ +Fix bug in :meth:`mne.viz.EvokedField.set_vmax` that prevented setting the color limits of the MEG magnetic field density, by `Marijn van Vliet`_ diff --git a/mne/viz/evoked_field.py b/mne/viz/evoked_field.py index 31e87772e91..8223f2ec624 100644 --- a/mne/viz/evoked_field.py +++ b/mne/viz/evoked_field.py @@ -380,6 +380,10 @@ def _configure_dock(self): if self._show_density: r._dock_add_label(value="max value", align=True, layout=layout) + @_auto_weakref + def _callback(vmax, kind, scaling): + self.set_vmax(vmax / scaling, kind=kind) + for surf_map in self._surf_maps: if surf_map["map_kind"] == "meg": scaling = DEFAULTS["scalings"]["grad"] @@ -388,10 +392,6 @@ def _configure_dock(self): rng = [0, np.max(np.abs(surf_map["data"])) * scaling] hlayout = r._dock_add_layout(vertical=False) - @_auto_weakref - def _callback(vmax, type, scaling): - self.set_vmax(vmax / scaling, type=type) - self._widgets[ f"vmax_slider_{surf_map['map_kind']}" ] = r._dock_add_slider( @@ -399,7 +399,7 @@ def _callback(vmax, type, scaling): value=surf_map["map_vmax"] * scaling, rng=rng, callback=partial( - _callback, type=surf_map["map_kind"], scaling=scaling + _callback, kind=surf_map["map_kind"], scaling=scaling ), double=True, layout=hlayout, @@ -411,7 +411,7 @@ def _callback(vmax, type, scaling): value=surf_map["map_vmax"] * scaling, rng=rng, callback=partial( - _callback, type=surf_map["map_kind"], scaling=scaling + _callback, kind=surf_map["map_kind"], scaling=scaling ), layout=hlayout, ) @@ -473,17 +473,15 @@ def _on_colormap_range(self, event): if self._show_density: surf_map["mesh"].update_overlay(name="field", rng=[vmin, vmax]) # Update the GUI widgets - # TODO: type is undefined here and only avoids a flake warning because it's - # a builtin! - if type == "meg": # noqa: E721 + if kind == "meg": scaling = DEFAULTS["scalings"]["grad"] else: scaling = DEFAULTS["scalings"]["eeg"] with disable_ui_events(self): - widget = self._widgets.get(f"vmax_slider_{type}", None) + widget = self._widgets.get(f"vmax_slider_{kind}", None) if widget is not None: widget.set_value(vmax * scaling) - widget = self._widgets.get(f"vmax_spin_{type}", None) + widget = self._widgets.get(f"vmax_spin_{kind}", None) if widget is not None: widget.set_value(vmax * scaling) @@ -543,28 +541,28 @@ def set_contours(self, n_contours): ), ) - def set_vmax(self, vmax, type="meg"): + def set_vmax(self, vmax, kind="meg"): """Change the color range of the density maps. Parameters ---------- vmax : float The new maximum value of the color range. - type : 'meg' | 'eeg' + kind : 'meg' | 'eeg' Which field map to apply the new color range to. """ - _check_option("type", type, ["eeg", "meg"]) + _check_option("type", kind, ["eeg", "meg"]) for surf_map in self._surf_maps: - if surf_map["map_kind"] == type: + if surf_map["map_kind"] == kind: publish( self, ColormapRange( - kind=f"field_strength_{type}", + kind=f"field_strength_{kind}", fmin=-vmax, fmax=vmax, ), ) - break + break else: raise ValueError(f"No {type.upper()} field map currently shown.") @@ -573,4 +571,4 @@ def _rescale(self): for surf_map in self._surf_maps: current_data = surf_map["data_interp"](self._current_time) vmax = float(np.max(current_data)) - self.set_vmax(vmax, type=surf_map["map_kind"]) + self.set_vmax(vmax, kind=surf_map["map_kind"]) diff --git a/mne/viz/tests/test_3d.py b/mne/viz/tests/test_3d.py index 2ff951200d0..14fdb544d83 100644 --- a/mne/viz/tests/test_3d.py +++ b/mne/viz/tests/test_3d.py @@ -36,6 +36,7 @@ from mne._fiff.constants import FIFF from mne.bem import read_bem_solution, read_bem_surfaces from mne.datasets import testing +from mne.defaults import DEFAULTS from mne.io import read_info, read_raw_bti, read_raw_ctf, read_raw_kit, read_raw_nirx from mne.minimum_norm import apply_inverse from mne.source_estimate import _BaseVolSourceEstimate @@ -196,8 +197,16 @@ def test_plot_evoked_field(renderer): assert isinstance(fig, EvokedField) fig._rescale() fig.set_time(0.05) + assert fig._current_time == 0.05 fig.set_contours(10) - fig.set_vmax(2) + assert fig._n_contours == 10 + assert fig._widgets["contours"].get_value() == 10 + fig.set_vmax(2e-12, kind="meg") + assert fig._surf_maps[1]["contours"][-1] == 2e-12 + assert ( + fig._widgets["vmax_slider_meg"].get_value() + == DEFAULTS["scalings"]["grad"] * 2e-12 + ) fig = evoked.plot_field(maps, time_viewer=False) assert isinstance(fig, Figure3D) From 85838854a333575ecb9d74b8f767f8bcbd515d56 Mon Sep 17 00:00:00 2001 From: Kristijan Armeni Date: Tue, 16 Jan 2024 12:02:40 -0500 Subject: [PATCH 154/405] [FIX] remove indexing along channel axis in `AnalogSignalGap.load()` (#12357) --- doc/changes/devel/12357.bugfix.rst | 1 + mne/io/neuralynx/neuralynx.py | 18 ++++++++++++------ mne/io/neuralynx/tests/test_neuralynx.py | 11 +++++++++++ 3 files changed, 24 insertions(+), 6 deletions(-) create mode 100644 doc/changes/devel/12357.bugfix.rst diff --git a/doc/changes/devel/12357.bugfix.rst b/doc/changes/devel/12357.bugfix.rst new file mode 100644 index 00000000000..d38ce54d5f5 --- /dev/null +++ b/doc/changes/devel/12357.bugfix.rst @@ -0,0 +1 @@ +Fix faulty indexing in :func:`mne.io.read_raw_neuralynx` when picking a single channel, by `Kristijan Armeni`_. \ No newline at end of file diff --git a/mne/io/neuralynx/neuralynx.py b/mne/io/neuralynx/neuralynx.py index 1d3a0a48ca8..55de7579d67 100644 --- a/mne/io/neuralynx/neuralynx.py +++ b/mne/io/neuralynx/neuralynx.py @@ -22,7 +22,7 @@ class AnalogSignalGap: Parameters ---------- signal : array-like - Array of shape (n_channels, n_samples) containing the data. + Array of shape (n_samples, n_chans) containing the data. units : str Units of the data. (e.g., 'uV') sampling_rate : quantity @@ -39,13 +39,20 @@ def __init__(self, signal, units, sampling_rate): self.units = units self.sampling_rate = sampling_rate - def load(self, channel_indexes): + def load(self, **kwargs): """Return AnalogSignal object.""" _soft_import("neo", "Reading NeuralynxIO files", strict=True) from neo import AnalogSignal + # `kwargs` is a dummy argument to mirror the + # AnalogSignalProxy.load() call signature which + # accepts `channel_indexes`` argument; but here we don't need + # any extra data selection arguments since + # self.signal array is already in the correct shape + # (channel dimension is based on `idx` variable) + sig = AnalogSignal( - signal=self.signal[:, channel_indexes], + signal=self.signal, units=self.units, sampling_rate=self.sampling_rate, ) @@ -141,8 +148,7 @@ def __init__( sfreq=nlx_reader.get_signal_sampling_rate(), ) - # find total number of samples per .ncs file (`channel`) by summing - # the sample sizes of all segments + # Neo reads only valid contiguous .ncs samples grouped as segments n_segments = nlx_reader.header["nb_segment"][0] block_id = 0 # assumes there's only one block of recording @@ -160,7 +166,7 @@ def __init__( seg_diffs = next_start_times - previous_stop_times # mark as discontinuous any two segments that have - # start/stop delta larger than sampling period (1/sampling_rate) + # start/stop delta larger than sampling period (1.5/sampling_rate) logger.info("Checking for temporal discontinuities in Neo data segments.") delta = 1.5 / info["sfreq"] gaps = seg_diffs > delta diff --git a/mne/io/neuralynx/tests/test_neuralynx.py b/mne/io/neuralynx/tests/test_neuralynx.py index 614e5021e69..14e030df23c 100644 --- a/mne/io/neuralynx/tests/test_neuralynx.py +++ b/mne/io/neuralynx/tests/test_neuralynx.py @@ -219,3 +219,14 @@ def test_neuralynx_gaps(): assert_allclose( mne_y, mat_y, rtol=1e-6, err_msg="MNE and Nlx2MatCSC.m not all close" ) + + # test that channel selection works + raw = read_raw_neuralynx( + fname=testing_path, + preload=False, + exclude_fname_patterns=ignored_ncs_files, + ) + + raw.pick("LAHC2") + assert raw.ch_names == ["LAHC2"] + raw.load_data() # before gh-12357 this would fail From 52a905928d43703f96517b7050f524e5a653a079 Mon Sep 17 00:00:00 2001 From: Clemens Brunner Date: Wed, 17 Jan 2024 04:51:35 +0100 Subject: [PATCH 155/405] Add Python 3.12 (#12361) Co-authored-by: Eric Larson --- .github/workflows/tests.yml | 14 ++++++++++---- doc/conf.py | 9 +++++++++ mne/conftest.py | 9 +++++++++ mne/export/_egimff.py | 3 +-- tools/github_actions_dependencies.sh | 2 +- 5 files changed, 30 insertions(+), 7 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 85f537930a5..913aceea194 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -55,14 +55,14 @@ jobs: strategy: matrix: include: - - os: ubuntu-latest - python: '3.10' - kind: conda - os: ubuntu-latest python: '3.11' kind: pip-pre + - os: ubuntu-latest + python: '3.12' + kind: conda - os: macos-latest - python: '3.9' + python: '3.11' kind: mamba - os: windows-latest python: '3.10' @@ -89,6 +89,12 @@ jobs: python-version: ${{ matrix.python }} if: startswith(matrix.kind, 'pip') # Python (if conda) + - name: Remove numba and dipy + run: | # TODO: Remove when numba 0.59 and dipy 1.8 land on conda-forge + sed -i '/numba/d' environment.yml + sed -i '/dipy/d' environment.yml + sed -i 's/- mne$/- mne-base/' environment.yml + if: matrix.os == 'ubuntu-latest' && startswith(matrix.kind, 'conda') && matrix.python == '3.12' - uses: mamba-org/setup-micromamba@v1 with: environment-file: ${{ env.CONDA_ENV }} diff --git a/doc/conf.py b/doc/conf.py index d114237bd5a..5805f377983 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -1335,6 +1335,15 @@ def reset_warnings(gallery_conf, fname): r"The .* was deprecated in Matplotlib 3\.7", # scipy r"scipy.signal.morlet2 is deprecated in SciPy 1\.12", + # 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.*", ): warnings.filterwarnings( # deal with other modules having bad imports "ignore", message=".*%s.*" % key, category=DeprecationWarning diff --git a/mne/conftest.py b/mne/conftest.py index a693b702935..f41bfa1374c 100644 --- a/mne/conftest.py +++ b/mne/conftest.py @@ -187,6 +187,15 @@ def pytest_configure(config): 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 + ignore:ast\.Num is deprecated.*:DeprecationWarning + ignore:Attribute n is deprecated and will be removed in Python 3\.14.*:DeprecationWarning + # numpydoc + ignore:ast\.NameConstant is deprecated and will be removed in Python 3\.14.*:DeprecationWarning + # pooch + ignore:Python 3\.14 will, by default, filter extracted tar archives.*:DeprecationWarning """ # noqa: E501 for warning_line in warning_lines.split("\n"): warning_line = warning_line.strip() diff --git a/mne/export/_egimff.py b/mne/export/_egimff.py index ef10c71acfc..70462a96841 100644 --- a/mne/export/_egimff.py +++ b/mne/export/_egimff.py @@ -50,7 +50,6 @@ def export_evokeds_mff(fname, evoked, history=None, *, overwrite=False, verbose= using MFF read functions. """ mffpy = _import_mffpy("Export evokeds to MFF.") - import pytz info = evoked[0].info if np.round(info["sfreq"]) != info["sfreq"]: @@ -73,7 +72,7 @@ def export_evokeds_mff(fname, evoked, history=None, *, overwrite=False, verbose= if op.exists(fname): os.remove(fname) if op.isfile(fname) else shutil.rmtree(fname) writer = mffpy.Writer(fname) - current_time = pytz.utc.localize(datetime.datetime.utcnow()) + current_time = datetime.datetime.now(datetime.timezone.utc) writer.addxml("fileInfo", recordTime=current_time) try: device = info["device_info"]["type"] diff --git a/tools/github_actions_dependencies.sh b/tools/github_actions_dependencies.sh index c56dd3d9ad0..4c4fb9eb3c2 100755 --- a/tools/github_actions_dependencies.sh +++ b/tools/github_actions_dependencies.sh @@ -7,7 +7,7 @@ INSTALL_ARGS="-e" INSTALL_KIND="test_extra,hdf5" if [ ! -z "$CONDA_ENV" ]; then echo "Uninstalling MNE for CONDA_ENV=${CONDA_ENV}" - conda remove -c conda-forge --force -yq mne + conda remove -c conda-forge --force -yq mne-base python -m pip uninstall -y mne if [[ "${RUNNER_OS}" != "Windows" ]]; then INSTALL_ARGS="" From 566c6ea5a6c5c66657e9edcffb1e596d21ba6952 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Wed, 17 Jan 2024 10:38:17 -0500 Subject: [PATCH 156/405] MAINT: Bump installer version (#12368) --- 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 39583ac9135..26199483d60 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.0/MNE-Python-1.6.0_0-Linux.sh + .. button-link:: https://github.com/mne-tools/mne-installers/releases/download/v1.6.1/MNE-Python-1.6.1_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.0_0-Linux.sh + $ sh ./MNE-Python-1.6.1_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.0/MNE-Python-1.6.0_0-macOS_Intel.pkg + .. button-link:: https://github.com/mne-tools/mne-installers/releases/download/v1.6.1/MNE-Python-1.6.1_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.0/MNE-Python-1.6.0_0-macOS_M1.pkg + .. button-link:: https://github.com/mne-tools/mne-installers/releases/download/v1.6.1/MNE-Python-1.6.1_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.0/MNE-Python-1.6.0_0-Windows.exe + .. button-link:: https://github.com/mne-tools/mne-installers/releases/download/v1.6.1/MNE-Python-1.6.1_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.0_0/bin/python + Using Python: /some/directory/mne-python_1.6.1_0/bin/python This path is what you need to enter in VS Code when selecting the Python interpreter. From 5339e08d7188f94e480d3b98bb81716a2e84e7f4 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 22 Jan 2024 15:01:08 -0500 Subject: [PATCH 157/405] [pre-commit.ci] pre-commit autoupdate (#12378) 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 427c5a09468..cfc33cc5ceb 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.1.13 + rev: v0.1.14 hooks: - id: ruff name: ruff lint mne From e0af51dec97219655d59aae0eab09675363f3792 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Mon, 22 Jan 2024 22:55:18 -0500 Subject: [PATCH 158/405] MAINT: Work around pytest issue (#12377) --- environment.yml | 1 + mne/annotations.py | 4 +--- mne/conftest.py | 4 ++++ mne/io/fiff/tests/test_raw_fiff.py | 6 ++++++ pyproject.toml | 3 ++- tools/azure_dependencies.sh | 7 +------ tools/github_actions_dependencies.sh | 2 +- 7 files changed, 16 insertions(+), 11 deletions(-) diff --git a/environment.yml b/environment.yml index 96c89fe472b..56a5cf37523 100644 --- a/environment.yml +++ b/environment.yml @@ -14,6 +14,7 @@ dependencies: - packaging - numba - pandas + - pyarrow - xlrd - scikit-learn - h5py diff --git a/mne/annotations.py b/mne/annotations.py index cc4209bf898..f0f88783b68 100644 --- a/mne/annotations.py +++ b/mne/annotations.py @@ -1616,9 +1616,7 @@ def events_from_annotations( inds = values = np.array([]).astype(int) for annot in annotations[event_sel]: annot_offset = annot["onset"] + annot["duration"] - _onsets = np.arange( - start=annot["onset"], stop=annot_offset, step=chunk_duration - ) + _onsets = np.arange(annot["onset"], annot_offset, chunk_duration) good_events = annot_offset - _onsets >= chunk_duration if good_events.any(): _onsets = _onsets[good_events] diff --git a/mne/conftest.py b/mne/conftest.py index f41bfa1374c..42f1e26c1e7 100644 --- a/mne/conftest.py +++ b/mne/conftest.py @@ -196,6 +196,10 @@ def pytest_configure(config): ignore:ast\.NameConstant is deprecated and will be removed in Python 3\.14.*:DeprecationWarning # pooch ignore:Python 3\.14 will, by default, filter extracted tar archives.*:DeprecationWarning + # pandas + ignore:\n*Pyarrow will become a required dependency of pandas.*:DeprecationWarning + # pyvista <-> NumPy 2.0 + ignore:__array_wrap__ must accept context and return_scalar arguments.*:DeprecationWarning """ # noqa: E501 for warning_line in warning_lines.split("\n"): warning_line = warning_line.strip() diff --git a/mne/io/fiff/tests/test_raw_fiff.py b/mne/io/fiff/tests/test_raw_fiff.py index fa3f04be0c7..f2269b0cb51 100644 --- a/mne/io/fiff/tests/test_raw_fiff.py +++ b/mne/io/fiff/tests/test_raw_fiff.py @@ -7,6 +7,7 @@ import os import pathlib import pickle +import platform import shutil import sys from copy import deepcopy @@ -42,6 +43,7 @@ assert_and_remove_boundary_annot, assert_object_equal, catch_logging, + check_version, requires_mne, run_subprocess, ) @@ -1023,6 +1025,8 @@ def test_proj(tmp_path): @pytest.mark.parametrize("preload", [False, True, "memmap.dat"]) def test_preload_modify(preload, tmp_path): """Test preloading and modifying data.""" + if platform.system() == "Windows" and check_version("numpy", "2.0.0dev"): + pytest.skip("Problem on Windows, see numpy/issues/25665") rng = np.random.RandomState(0) raw = read_raw_fif(fif_fname, preload=preload) @@ -1926,6 +1930,8 @@ def test_equalize_channels(): def test_memmap(tmp_path): """Test some interesting memmapping cases.""" # concatenate_raw + if platform.system() == "Windows" and check_version("numpy", "2.0.0dev"): + pytest.skip("Problem on Windows, see numpy/issues/25665") memmaps = [str(tmp_path / str(ii)) for ii in range(3)] raw_0 = read_raw_fif(test_fif_fname, preload=memmaps[0]) assert raw_0._data.filename == memmaps[0] diff --git a/pyproject.toml b/pyproject.toml index abb2e1b3f1b..824f3155148 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -72,6 +72,7 @@ full = [ "numba", "h5py", "pandas", + "pyarrow", # only needed to avoid a deprecation warning in pandas "numexpr", "jupyter", "python-picard", @@ -109,7 +110,7 @@ full = [ # Dependencies for running the test infrastructure test = [ - "pytest!=8.0.0rc1", + "pytest!=8.0.0rc1,!=8.0.0rc2", "pytest-cov", "pytest-timeout", "pytest-harvest", diff --git a/tools/azure_dependencies.sh b/tools/azure_dependencies.sh index cce220a8188..9ee566f3c30 100755 --- a/tools/azure_dependencies.sh +++ b/tools/azure_dependencies.sh @@ -8,12 +8,7 @@ elif [ "${TEST_MODE}" == "pip-pre" ]; then STD_ARGS="$STD_ARGS --pre" python -m pip install $STD_ARGS --only-binary ":all:" --extra-index-url "https://www.riverbankcomputing.com/pypi/simple" "PyQt6!=6.6.1" PyQt6-sip PyQt6-Qt6 "PyQt6-Qt6!=6.6.1" echo "Numpy etc." - # See github_actions_dependencies.sh for comments - # Until https://github.com/scipy/scipy/issues/19605 and - # https://github.com/scipy/scipy/issues/19713 are resolved, we can't use the NumPy - # 2.0 wheels :( - python -m pip install $STD_ARGS --only-binary numpy scipy h5py - python -m pip install $STD_ARGS --only-binary ":all:" --extra-index-url "https://pypi.anaconda.org/scientific-python-nightly-wheels/simple" scikit-learn matplotlib statsmodels + python -m pip install $STD_ARGS --only-binary ":all:" --default-timeout=60 --extra-index-url "https://pypi.anaconda.org/scientific-python-nightly-wheels/simple" "numpy>=2.0.0.dev0" "scipy>=1.12.0.dev0" "scikit-learn>=1.5.dev0" matplotlib pillow statsmodels pyarrow # echo "dipy" # python -m pip install $STD_ARGS --only-binary ":all:" --extra-index-url "https://pypi.anaconda.org/scipy-wheels-nightly/simple" dipy # echo "OpenMEEG" diff --git a/tools/github_actions_dependencies.sh b/tools/github_actions_dependencies.sh index 4c4fb9eb3c2..b801b458dc8 100755 --- a/tools/github_actions_dependencies.sh +++ b/tools/github_actions_dependencies.sh @@ -28,7 +28,7 @@ else echo "PyQt6" pip install $STD_ARGS --only-binary ":all:" --default-timeout=60 --extra-index-url https://www.riverbankcomputing.com/pypi/simple "PyQt6!=6.6.1" "PyQt6-Qt6!=6.6.1" echo "NumPy/SciPy/pandas etc." - pip install $STD_ARGS --only-binary ":all:" --default-timeout=60 --extra-index-url "https://pypi.anaconda.org/scientific-python-nightly-wheels/simple" "numpy>=2.0.0.dev0" "scipy>=1.12.0.dev0" "scikit-learn==1.4.dev0" matplotlib pillow statsmodels + pip install $STD_ARGS --only-binary ":all:" --default-timeout=60 --extra-index-url "https://pypi.anaconda.org/scientific-python-nightly-wheels/simple" "numpy>=2.0.0.dev0" "scipy>=1.12.0.dev0" "scikit-learn>=1.5.dev0" matplotlib pillow statsmodels pyarrow # No pandas, dipy, h5py, openmeeg, python-picard (needs numexpr) until they update to NumPy 2.0 compat INSTALL_KIND="test_extra" # echo "dipy" From 2f0d2f3ee2422dfb721d1bcc7138ff1a6fccd662 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 23 Jan 2024 11:07:32 -0500 Subject: [PATCH 159/405] Bump actions/cache from 3 to 4 (#12374) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Eric Larson --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 913aceea194..396f03803f3 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -116,7 +116,7 @@ jobs: run: MNE_SKIP_TESTING_DATASET_TESTS=true pytest -m "not (ultraslowtest or pgtest)" --tb=short --cov=mne --cov-report xml -vv -rfE mne/ if: matrix.kind == 'minimal' - run: ./tools/get_testing_version.sh - - uses: actions/cache@v3 + - uses: actions/cache@v4 with: key: ${{ env.TESTING_VERSION }} path: ~/mne_data From 71faac906f00baa14b546da50d0f97aceedc004a Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Tue, 23 Jan 2024 12:56:36 -0500 Subject: [PATCH 160/405] MAINT: Check for shadowing and mutable defaults (#12380) --- doc/changes/devel/12380.bugfix.rst | 1 + doc/conf.py | 5 +++++ environment.yml | 1 + .../time_frequency/time_frequency_erds.py | 2 ++ mne/_fiff/open.py | 2 +- mne/_fiff/pick.py | 10 +++++----- mne/channels/tests/test_channels.py | 4 ++-- mne/commands/mne_setup_source_space.py | 12 ++++++----- mne/cov.py | 2 +- .../sleep_physionet/tests/test_physionet.py | 4 ++-- mne/decoding/tests/test_ssd.py | 2 +- mne/epochs.py | 4 ++-- mne/event.py | 8 +++++++- mne/evoked.py | 2 +- mne/export/_export.py | 4 ++-- mne/gui/_coreg.py | 4 ++-- mne/inverse_sparse/mxne_optim.py | 2 +- mne/io/bti/bti.py | 2 +- mne/io/curry/curry.py | 6 +++--- mne/io/edf/edf.py | 4 +++- mne/io/fieldtrip/tests/helpers.py | 2 +- mne/io/fieldtrip/tests/test_fieldtrip.py | 12 +++++------ mne/preprocessing/nirs/_tddr.py | 4 +--- mne/preprocessing/ssp.py | 14 ++++++++----- mne/preprocessing/tests/test_ssp.py | 3 +++ mne/report/report.py | 2 +- mne/source_estimate.py | 10 ++++++++-- mne/source_space/_source_space.py | 3 ++- mne/time_frequency/csd.py | 2 +- mne/time_frequency/tfr.py | 8 +++++--- mne/utils/tests/test_docs.py | 12 +++++------ mne/utils/tests/test_logging.py | 2 +- mne/viz/backends/_abstract.py | 4 ++-- mne/viz/backends/_notebook.py | 4 ++-- mne/viz/backends/_qt.py | 8 ++++---- mne/viz/backends/renderer.py | 2 +- mne/viz/circle.py | 8 +++++++- mne/viz/evoked.py | 6 +++--- mne/viz/topomap.py | 20 +++++++++---------- mne/viz/utils.py | 4 ++-- pyproject.toml | 2 +- 41 files changed, 126 insertions(+), 87 deletions(-) create mode 100644 doc/changes/devel/12380.bugfix.rst diff --git a/doc/changes/devel/12380.bugfix.rst b/doc/changes/devel/12380.bugfix.rst new file mode 100644 index 00000000000..8c5ee5a6fca --- /dev/null +++ b/doc/changes/devel/12380.bugfix.rst @@ -0,0 +1 @@ +Fix bug where :func:`mne.preprocessing.compute_proj_ecg` and :func:`mne.preprocessing.compute_proj_eog` could modify the default ``reject`` and ``flat`` arguments on multiple calls based on channel types present, by `Eric Larson`_. diff --git a/doc/conf.py b/doc/conf.py index 5805f377983..40222a265fe 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -1344,6 +1344,10 @@ def reset_warnings(gallery_conf, fname): 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.*", ): warnings.filterwarnings( # deal with other modules having bad imports "ignore", message=".*%s.*" % key, category=DeprecationWarning @@ -1382,6 +1386,7 @@ def reset_warnings(gallery_conf, fname): 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", diff --git a/environment.yml b/environment.yml index 56a5cf37523..0a77f57e5fe 100644 --- a/environment.yml +++ b/environment.yml @@ -6,6 +6,7 @@ dependencies: - pip - numpy - scipy + - openblas!=0.3.26 # until https://github.com/conda-forge/scipy-feedstock/pull/268 lands - matplotlib - tqdm - pooch>=1.5 diff --git a/examples/time_frequency/time_frequency_erds.py b/examples/time_frequency/time_frequency_erds.py index 593861674ed..556730b6cab 100644 --- a/examples/time_frequency/time_frequency_erds.py +++ b/examples/time_frequency/time_frequency_erds.py @@ -50,6 +50,7 @@ # %% # First, we load and preprocess the data. We use runs 6, 10, and 14 from # subject 1 (these runs contains hand and feet motor imagery). + fnames = eegbci.load_data(subject=1, runs=(6, 10, 14)) raw = concatenate_raws([read_raw_edf(f, preload=True) for f in fnames]) @@ -59,6 +60,7 @@ # %% # Now we can create 5-second epochs around events of interest. + tmin, tmax = -1, 4 event_ids = dict(hands=2, feet=3) # map event IDs to tasks diff --git a/mne/_fiff/open.py b/mne/_fiff/open.py index 65a7bec33a8..5bfcb83a951 100644 --- a/mne/_fiff/open.py +++ b/mne/_fiff/open.py @@ -268,7 +268,7 @@ def show_fiff( return out -def _find_type(value, fmts=["FIFF_"], exclude=["FIFF_UNIT"]): +def _find_type(value, fmts=("FIFF_",), exclude=("FIFF_UNIT",)): """Find matching values.""" value = int(value) vals = [ diff --git a/mne/_fiff/pick.py b/mne/_fiff/pick.py index 86790e6e3e8..2722e91f517 100644 --- a/mne/_fiff/pick.py +++ b/mne/_fiff/pick.py @@ -259,7 +259,7 @@ def channel_type(info, idx): @verbose -def pick_channels(ch_names, include, exclude=[], ordered=None, *, verbose=None): +def pick_channels(ch_names, include, exclude=(), ordered=None, *, verbose=None): """Pick channels by names. Returns the indices of ``ch_names`` in ``include`` but not in ``exclude``. @@ -706,7 +706,7 @@ def _has_kit_refs(info, picks): @verbose def pick_channels_forward( - orig, include=[], exclude=[], ordered=None, copy=True, *, verbose=None + orig, include=(), exclude=(), ordered=None, copy=True, *, verbose=None ): """Pick channels from forward operator. @@ -797,8 +797,8 @@ def pick_types_forward( seeg=False, ecog=False, dbs=False, - include=[], - exclude=[], + include=(), + exclude=(), ): """Pick by channel type and names from a forward operator. @@ -893,7 +893,7 @@ def channel_indices_by_type(info, picks=None): @verbose def pick_channels_cov( - orig, include=[], exclude="bads", ordered=None, copy=True, *, verbose=None + orig, include=(), exclude="bads", ordered=None, copy=True, *, verbose=None ): """Pick channels from covariance matrix. diff --git a/mne/channels/tests/test_channels.py b/mne/channels/tests/test_channels.py index b403a0e6713..adfe63f93d9 100644 --- a/mne/channels/tests/test_channels.py +++ b/mne/channels/tests/test_channels.py @@ -438,8 +438,8 @@ def test_1020_selection(): raw = raw.rename_channels(dict(zip(raw.ch_names, montage.ch_names))) raw.set_montage(montage) - for input in ("a_string", 100, raw, [1, 2]): - pytest.raises(TypeError, make_1020_channel_selections, input) + for input_ in ("a_string", 100, raw, [1, 2]): + pytest.raises(TypeError, make_1020_channel_selections, input_) sels = make_1020_channel_selections(raw.info) # are all frontal channels placed before all occipital channels? diff --git a/mne/commands/mne_setup_source_space.py b/mne/commands/mne_setup_source_space.py index b5654ecab7f..f5f5dc8b343 100644 --- a/mne/commands/mne_setup_source_space.py +++ b/mne/commands/mne_setup_source_space.py @@ -120,7 +120,7 @@ def run(): subjects_dir = options.subjects_dir spacing = options.spacing ico = options.ico - oct = options.oct + oct_ = options.oct surface = options.surface n_jobs = options.n_jobs add_dist = options.add_dist @@ -130,20 +130,22 @@ def run(): overwrite = True if options.overwrite is not None else False # Parse source spacing option - spacing_options = [ico, oct, spacing] + spacing_options = [ico, oct_, spacing] n_options = len([x for x in spacing_options if x is not None]) + use_spacing = "oct6" if n_options > 1: raise ValueError("Only one spacing option can be set at the same time") elif n_options == 0: # Default to oct6 - use_spacing = "oct6" + pass elif n_options == 1: if ico is not None: use_spacing = "ico" + str(ico) - elif oct is not None: - use_spacing = "oct" + str(oct) + elif oct_ is not None: + use_spacing = "oct" + str(oct_) elif spacing is not None: use_spacing = spacing + del ico, oct_, spacing # Generate filename if fname is None: if subject_to is None: diff --git a/mne/cov.py b/mne/cov.py index 311121c8f87..7b9a4b24252 100644 --- a/mne/cov.py +++ b/mne/cov.py @@ -310,7 +310,7 @@ def __iadd__(self, cov): def plot( self, info, - exclude=[], + exclude=(), colorbar=True, proj=False, show_svd=True, diff --git a/mne/datasets/sleep_physionet/tests/test_physionet.py b/mne/datasets/sleep_physionet/tests/test_physionet.py index 08b13c832c7..7cf57632057 100644 --- a/mne/datasets/sleep_physionet/tests/test_physionet.py +++ b/mne/datasets/sleep_physionet/tests/test_physionet.py @@ -46,12 +46,12 @@ def _check_mocked_function_calls(mocked_func, call_fname_hash_pairs, base_path): # order. for idx, current in enumerate(call_fname_hash_pairs): _, call_kwargs = mocked_func.call_args_list[idx] - hash_type, hash = call_kwargs["known_hash"].split(":") + hash_type, hash_ = call_kwargs["known_hash"].split(":") assert call_kwargs["url"] == _get_expected_url(current["name"]), idx assert Path(call_kwargs["path"], call_kwargs["fname"]) == _get_expected_path( base_path, current["name"] ) - assert hash == current["hash"] + assert hash_ == current["hash"] assert hash_type == "sha1" diff --git a/mne/decoding/tests/test_ssd.py b/mne/decoding/tests/test_ssd.py index bdb3f74c545..e72e0ff81ad 100644 --- a/mne/decoding/tests/test_ssd.py +++ b/mne/decoding/tests/test_ssd.py @@ -19,7 +19,7 @@ def simulate_data( - freqs_sig=[9, 12], + freqs_sig=(9, 12), n_trials=100, n_channels=20, n_samples=500, diff --git a/mne/epochs.py b/mne/epochs.py index d1f76609356..2b437dca6b3 100644 --- a/mne/epochs.py +++ b/mne/epochs.py @@ -3662,7 +3662,7 @@ def _is_good( reject, flat, full_report=False, - ignore_chs=[], + ignore_chs=(), verbose=None, ): """Test if data segment e is good according to reject and flat. @@ -4631,7 +4631,7 @@ def make_fixed_length_epochs( reject_by_annotation=True, proj=True, overlap=0.0, - id=1, + id=1, # noqa: A002 verbose=None, ): """Divide continuous raw data into equal-sized consecutive epochs. diff --git a/mne/event.py b/mne/event.py index 6bb8a1f604a..cfc112a2e8c 100644 --- a/mne/event.py +++ b/mne/event.py @@ -925,7 +925,13 @@ def shift_time_events(events, ids, tshift, sfreq): @fill_doc def make_fixed_length_events( - raw, id=1, start=0, stop=None, duration=1.0, first_samp=True, overlap=0.0 + raw, + id=1, # noqa: A002 + start=0, + stop=None, + duration=1.0, + first_samp=True, + overlap=0.0, ): """Make a set of :term:`events` separated by a fixed duration. diff --git a/mne/evoked.py b/mne/evoked.py index a3f530076ac..c988368c314 100644 --- a/mne/evoked.py +++ b/mne/evoked.py @@ -549,7 +549,7 @@ def plot_topo( scalings=None, title=None, proj=False, - vline=[0.0], + vline=(0.0,), fig_background=None, merge_grads=False, legend=True, diff --git a/mne/export/_export.py b/mne/export/_export.py index 80a18d090d2..aed7e44e0c8 100644 --- a/mne/export/_export.py +++ b/mne/export/_export.py @@ -211,9 +211,9 @@ def _infer_check_export_fmt(fmt, fname, supported_formats): if fmt not in supported_formats: supported = [] - for format, extensions in supported_formats.items(): + for supp_format, extensions in supported_formats.items(): ext_str = ", ".join(f"*.{ext}" for ext in extensions) - supported.append(f"{format} ({ext_str})") + supported.append(f"{supp_format} ({ext_str})") supported_str = ", ".join(supported) raise ValueError( diff --git a/mne/gui/_coreg.py b/mne/gui/_coreg.py index 6063f6f628f..983b4b5b067 100644 --- a/mne/gui/_coreg.py +++ b/mne/gui/_coreg.py @@ -1907,7 +1907,7 @@ def _configure_dock(self): func=self._save_trans, tooltip="Save the transform file to disk", layout=save_trans_layout, - filter="Head->MRI transformation (*-trans.fif *_trans.fif)", + filter_="Head->MRI transformation (*-trans.fif *_trans.fif)", initial_directory=str(Path(self._info_file).parent), ) self._widgets["load_trans"] = self._renderer._dock_add_file_button( @@ -1916,7 +1916,7 @@ def _configure_dock(self): func=self._load_trans, tooltip="Load the transform file from disk", layout=save_trans_layout, - filter="Head->MRI transformation (*-trans.fif *_trans.fif)", + filter_="Head->MRI transformation (*-trans.fif *_trans.fif)", initial_directory=str(Path(self._info_file).parent), ) self._renderer._layout_add_widget(trans_layout, save_trans_layout) diff --git a/mne/inverse_sparse/mxne_optim.py b/mne/inverse_sparse/mxne_optim.py index 5d785f5eec5..a4c63a557b5 100644 --- a/mne/inverse_sparse/mxne_optim.py +++ b/mne/inverse_sparse/mxne_optim.py @@ -799,7 +799,7 @@ def __call__(self, x): # noqa: D105 else: return np.hstack([x @ op for op in self.ops]) / np.sqrt(self.n_dicts) - def norm(self, z, ord=2): + 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( diff --git a/mne/io/bti/bti.py b/mne/io/bti/bti.py index 71c85880069..616602892dd 100644 --- a/mne/io/bti/bti.py +++ b/mne/io/bti/bti.py @@ -78,7 +78,7 @@ def __init__(self, target): def __enter__(self): # noqa: D105 return self.target - def __exit__(self, type, value, tb): # noqa: D105 + def __exit__(self, exception_type, value, tb): # noqa: D105 pass diff --git a/mne/io/curry/curry.py b/mne/io/curry/curry.py index 021bc729ebf..3b3d5e711d3 100644 --- a/mne/io/curry/curry.py +++ b/mne/io/curry/curry.py @@ -197,10 +197,10 @@ def _read_curry_parameters(fname): if any(var_name in line for var_name in var_names): key, val = line.replace(" ", "").replace("\n", "").split("=") param_dict[key.lower().replace("_", "")] = val - for type in CHANTYPES: - if "DEVICE_PARAMETERS" + CHANTYPES[type] + " START" in line: + for key, type_ in CHANTYPES.items(): + if f"DEVICE_PARAMETERS{type_} START" in line: data_unit = next(fid) - unit_dict[type] = ( + unit_dict[key] = ( data_unit.replace(" ", "").replace("\n", "").split("=")[-1] ) diff --git a/mne/io/edf/edf.py b/mne/io/edf/edf.py index 62987ac19fc..888db95c9a6 100644 --- a/mne/io/edf/edf.py +++ b/mne/io/edf/edf.py @@ -1454,7 +1454,9 @@ def _read_gdf_header(fname, exclude, include=None): def _check_stim_channel( - stim_channel, ch_names, tal_ch_names=["EDF Annotations", "BDF Annotations"] + stim_channel, + ch_names, + tal_ch_names=("EDF Annotations", "BDF Annotations"), ): """Check that the stimulus channel exists in the current datafile.""" DEFAULT_STIM_CH_NAMES = ["status", "trigger"] diff --git a/mne/io/fieldtrip/tests/helpers.py b/mne/io/fieldtrip/tests/helpers.py index 5ab02286b66..66cb582dde9 100644 --- a/mne/io/fieldtrip/tests/helpers.py +++ b/mne/io/fieldtrip/tests/helpers.py @@ -185,7 +185,7 @@ def get_epochs(system): else: event_id = [int(cfg_local["eventvalue"])] - event_id = [id for id in event_id if id in events[:, 2]] + event_id = [id_ for id_ in event_id if id_ in events[:, 2]] epochs = mne.Epochs( raw_data, diff --git a/mne/io/fieldtrip/tests/test_fieldtrip.py b/mne/io/fieldtrip/tests/test_fieldtrip.py index 0f66d1b1fae..f7faac045ee 100644 --- a/mne/io/fieldtrip/tests/test_fieldtrip.py +++ b/mne/io/fieldtrip/tests/test_fieldtrip.py @@ -253,19 +253,19 @@ def test_one_channel_elec_bug(version): @pytest.mark.filterwarnings("ignore:.*parse meas date.*:RuntimeWarning") @pytest.mark.filterwarnings("ignore:.*number of bytes.*:RuntimeWarning") @pytest.mark.parametrize("version", all_versions) -@pytest.mark.parametrize("type", ["averaged", "epoched", "raw"]) -def test_throw_exception_on_cellarray(version, type): +@pytest.mark.parametrize("type_", ["averaged", "epoched", "raw"]) +def test_throw_exception_on_cellarray(version, type_): """Test for a meaningful exception when the data is a cell array.""" - fname = get_data_paths("cellarray") / f"{type}_{version}.mat" + 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" ): - if type == "averaged": + if type_ == "averaged": mne.read_evoked_fieldtrip(fname, info) - elif type == "epoched": + elif type_ == "epoched": mne.read_epochs_fieldtrip(fname, info) - elif type == "raw": + elif type_ == "raw": mne.io.read_raw_fieldtrip(fname, info) diff --git a/mne/preprocessing/nirs/_tddr.py b/mne/preprocessing/nirs/_tddr.py index 59c2ec926d9..a7d0af9a305 100644 --- a/mne/preprocessing/nirs/_tddr.py +++ b/mne/preprocessing/nirs/_tddr.py @@ -111,7 +111,6 @@ def _TDDR(signal, sample_rate): tune = 4.685 D = np.sqrt(np.finfo(signal.dtype).eps) mu = np.inf - iter = 0 # Step 1. Compute temporal derivative of the signal deriv = np.diff(signal_low) @@ -120,8 +119,7 @@ def _TDDR(signal, sample_rate): w = np.ones(deriv.shape) # Step 3. Iterative estimation of robust weights - while iter < 50: - iter = iter + 1 + for _ in range(50): mu0 = mu # Step 3a. Estimate weighted mean diff --git a/mne/preprocessing/ssp.py b/mne/preprocessing/ssp.py index 985a30a6e9d..271f9195416 100644 --- a/mne/preprocessing/ssp.py +++ b/mne/preprocessing/ssp.py @@ -13,7 +13,7 @@ from .._fiff.reference import make_eeg_average_ref_proj from ..epochs import Epochs from ..proj import compute_proj_epochs, compute_proj_evoked -from ..utils import logger, verbose, warn +from ..utils import _validate_type, logger, verbose, warn from .ecg import find_ecg_events from .eog import find_eog_events @@ -112,7 +112,10 @@ def _compute_exg_proj( my_info["bads"] += bads # Handler rejection parameters + _validate_type(reject, (None, dict), "reject") + _validate_type(flat, (None, dict), "flat") if reject is not None: # make sure they didn't pass None + reject = reject.copy() # must make a copy or we modify default! if ( len( pick_types( @@ -170,6 +173,7 @@ def _compute_exg_proj( ): _safe_del_key(reject, "eog") if flat is not None: # make sure they didn't pass None + flat = flat.copy() if ( len( pick_types( @@ -300,9 +304,9 @@ def compute_proj_ecg( filter_length="10s", n_jobs=None, ch_name=None, - reject=dict(grad=2000e-13, mag=3000e-15, eeg=50e-6, eog=250e-6), + reject=dict(grad=2000e-13, mag=3000e-15, eeg=50e-6, eog=250e-6), # noqa: B006 flat=None, - bads=[], + bads=(), avg_ref=False, no_proj=False, event_id=999, @@ -461,9 +465,9 @@ def compute_proj_eog( average=True, filter_length="10s", n_jobs=None, - reject=dict(grad=2000e-13, mag=3000e-15, eeg=500e-6, eog=np.inf), + reject=dict(grad=2000e-13, mag=3000e-15, eeg=500e-6, eog=np.inf), # noqa: B006 flat=None, - bads=[], + bads=(), avg_ref=False, no_proj=False, event_id=998, diff --git a/mne/preprocessing/tests/test_ssp.py b/mne/preprocessing/tests/test_ssp.py index 359a844686c..b7565a6c5ce 100644 --- a/mne/preprocessing/tests/test_ssp.py +++ b/mne/preprocessing/tests/test_ssp.py @@ -69,6 +69,9 @@ def test_compute_proj_ecg(short_raw, average): # XXX: better tests # without setting a bad channel, this should throw a warning + # (first with a call that makes sure we copy the mutable default "reject") + with pytest.warns(RuntimeWarning, match="longer than the signal"): + compute_proj_ecg(raw.copy().pick("mag"), l_freq=None, h_freq=None) with pytest.warns(RuntimeWarning, match="No good epochs found"): projs, events, drop_log = compute_proj_ecg( raw, diff --git a/mne/report/report.py b/mne/report/report.py index 6b83642ec8f..34acb8860e6 100644 --- a/mne/report/report.py +++ b/mne/report/report.py @@ -2998,7 +2998,7 @@ def __enter__(self): """Do nothing when entering the context block.""" return self - def __exit__(self, type, value, traceback): + def __exit__(self, exception_type, value, traceback): """Save the report when leaving the context block.""" if self.fname is not None: self.save(self.fname, open_browser=False, overwrite=True) diff --git a/mne/source_estimate.py b/mne/source_estimate.py index 35e14e2e769..66897bcaedc 100644 --- a/mne/source_estimate.py +++ b/mne/source_estimate.py @@ -2476,7 +2476,7 @@ def save_as_volume( src, dest="mri", mri_resolution=False, - format="nifti1", + format="nifti1", # noqa: A002 *, overwrite=False, verbose=None, @@ -2525,7 +2525,13 @@ def save_as_volume( ) nib.save(img, fname) - def as_volume(self, src, dest="mri", mri_resolution=False, format="nifti1"): + def as_volume( + self, + src, + dest="mri", + mri_resolution=False, + format="nifti1", # noqa: A002 + ): """Export volume source estimate as a nifti object. Parameters diff --git a/mne/source_space/_source_space.py b/mne/source_space/_source_space.py index bc17b08e53b..11834cc7631 100644 --- a/mne/source_space/_source_space.py +++ b/mne/source_space/_source_space.py @@ -2047,11 +2047,12 @@ def _make_volume_source_space( volume_labels=None, do_neighbors=True, n_jobs=None, - vol_info={}, + vol_info=None, single_volume=False, ): """Make a source space which covers the volume bounded by surf.""" # Figure out the grid size in the MRI coordinate frame + vol_info = {} if vol_info is None else vol_info if "rr" in surf: mins = np.min(surf["rr"], axis=0) maxs = np.max(surf["rr"], axis=0) diff --git a/mne/time_frequency/csd.py b/mne/time_frequency/csd.py index cea14c9944a..5bca1d03508 100644 --- a/mne/time_frequency/csd.py +++ b/mne/time_frequency/csd.py @@ -35,7 +35,7 @@ @verbose def pick_channels_csd( - csd, include=[], exclude=[], ordered=None, copy=True, *, verbose=None + csd, include=(), exclude=(), ordered=None, copy=True, *, verbose=None ): """Pick channels from cross-spectral density matrix. diff --git a/mne/time_frequency/tfr.py b/mne/time_frequency/tfr.py index 2e9d3102054..3be20ce28fc 100644 --- a/mne/time_frequency/tfr.py +++ b/mne/time_frequency/tfr.py @@ -1468,7 +1468,7 @@ def plot( mask_cmap="Greys", mask_alpha=0.1, combine=None, - exclude=[], + exclude=(), cnorm=None, verbose=None, ): @@ -1667,7 +1667,7 @@ def _plot( exclude=None, copy=True, source_plot_joint=False, - topomap_args=dict(), + topomap_args=None, ch_type=None, cnorm=None, verbose=None, @@ -1676,6 +1676,8 @@ def _plot( See self.plot() for parameters description. """ + _validate_type(topomap_args, (dict, None), "topomap_args") + topomap_args = {} if topomap_args is None else topomap_args import matplotlib.pyplot as plt # channel selection @@ -1821,7 +1823,7 @@ def plot_joint( title=None, yscale="auto", combine="mean", - exclude=[], + exclude=(), topomap_args=None, image_args=None, verbose=None, diff --git a/mne/utils/tests/test_docs.py b/mne/utils/tests/test_docs.py index 0fd13aa25a5..c5ab49d3167 100644 --- a/mne/utils/tests/test_docs.py +++ b/mne/utils/tests/test_docs.py @@ -122,12 +122,12 @@ def m1(): def test_copy_function_doc_to_method_doc(): """Test decorator for re-using function docstring as method docstrings.""" - def f1(object, a, b, c): + def f1(obj, a, b, c): """Docstring for f1. Parameters ---------- - object : object + obj : object Some object. This description also has blank lines in it. @@ -138,7 +138,7 @@ def f1(object, a, b, c): """ pass - def f2(object): + def f2(obj): """Docstring for f2. Parameters @@ -152,7 +152,7 @@ def f2(object): """ pass - def f3(object): + def f3(obj): """Docstring for f3. Parameters @@ -162,11 +162,11 @@ def f3(object): """ pass - def f4(object): + def f4(obj): """Docstring for f4.""" pass - def f5(object): # noqa: D410, D411, D414 + def f5(obj): # noqa: D410, D411, D414 """Docstring for f5. Parameters diff --git a/mne/utils/tests/test_logging.py b/mne/utils/tests/test_logging.py index 8a19d76a089..02a5a7363d0 100644 --- a/mne/utils/tests/test_logging.py +++ b/mne/utils/tests/test_logging.py @@ -73,7 +73,7 @@ def test_how_to_deal_with_warnings(): assert len(w) == 1 -def clean_lines(lines=[]): +def clean_lines(lines=()): """Scrub filenames for checking logging output (in test_logging).""" return [line if "Reading " not in line else "Reading test file" for line in lines] diff --git a/mne/viz/backends/_abstract.py b/mne/viz/backends/_abstract.py index b28468ebc77..c31023401ed 100644 --- a/mne/viz/backends/_abstract.py +++ b/mne/viz/backends/_abstract.py @@ -1136,7 +1136,7 @@ def _dock_add_file_button( desc, func, *, - filter=None, + filter_=None, initial_directory=None, save=False, is_directory=False, @@ -1209,7 +1209,7 @@ def _dialog_create( callback, *, icon="Warning", - buttons=[], + buttons=(), modal=True, window=None, ): diff --git a/mne/viz/backends/_notebook.py b/mne/viz/backends/_notebook.py index 6a9e5a6cf8f..4601ef1fc6a 100644 --- a/mne/viz/backends/_notebook.py +++ b/mne/viz/backends/_notebook.py @@ -976,7 +976,7 @@ def _dialog_create( callback, *, icon="Warning", - buttons=[], + buttons=(), modal=True, window=None, ): @@ -1202,7 +1202,7 @@ def _dock_add_file_button( desc, func, *, - filter=None, + filter_=None, initial_directory=None, save=False, is_directory=False, diff --git a/mne/viz/backends/_qt.py b/mne/viz/backends/_qt.py index 3f7f28abc1b..35cdc4de502 100644 --- a/mne/viz/backends/_qt.py +++ b/mne/viz/backends/_qt.py @@ -930,7 +930,7 @@ def _dialog_create( callback, *, icon="Warning", - buttons=[], + buttons=(), modal=True, window=None, ): @@ -1205,7 +1205,7 @@ def _dock_add_file_button( desc, func, *, - filter=None, + filter_=None, initial_directory=None, save=False, is_directory=False, @@ -1226,11 +1226,11 @@ def callback(): ) elif save: name = QFileDialog.getSaveFileName( - parent=self._window, directory=initial_directory, filter=filter + parent=self._window, directory=initial_directory, filter=filter_ ) else: name = QFileDialog.getOpenFileName( - parent=self._window, directory=initial_directory, filter=filter + parent=self._window, directory=initial_directory, filter=filter_ ) name = name[0] if isinstance(name, tuple) else name # handle the cancel button diff --git a/mne/viz/backends/renderer.py b/mne/viz/backends/renderer.py index 2e2ab1e7333..faa209454e1 100644 --- a/mne/viz/backends/renderer.py +++ b/mne/viz/backends/renderer.py @@ -391,7 +391,7 @@ def _enable_time_interaction( current_time_func, times, init_playback_speed=0.01, - playback_speed_range=[0.01, 0.1], + playback_speed_range=(0.01, 0.1), ): from ..ui_events import ( PlaybackSpeed, diff --git a/mne/viz/circle.py b/mne/viz/circle.py index 7f9d16ecc54..5a3406a7c1b 100644 --- a/mne/viz/circle.py +++ b/mne/viz/circle.py @@ -97,7 +97,13 @@ def circular_layout( def _plot_connectivity_circle_onpick( - event, fig=None, ax=None, indices=None, n_nodes=0, node_angles=None, ylim=[9, 10] + event, + fig=None, + ax=None, + indices=None, + n_nodes=0, + node_angles=None, + ylim=(9, 10), ): """Isolate connections around a single node when user left clicks a node. diff --git a/mne/viz/evoked.py b/mne/viz/evoked.py index 20bdd655a93..96976532767 100644 --- a/mne/viz/evoked.py +++ b/mne/viz/evoked.py @@ -390,9 +390,9 @@ def _plot_evoked( ax.set_xlabel("") ims = [ax.images[0] for ax in axes.values()] clims = np.array([im.get_clim() for im in ims]) - min, max = clims.min(), clims.max() + min_, max_ = clims.min(), clims.max() for im in ims: - im.set_clim(min, max) + im.set_clim(min_, max_) figs = [ax.get_figure() for ax in axes.values()] if len(set(figs)) == 1: return figs[0] @@ -1171,7 +1171,7 @@ def plot_evoked_topo( scalings=None, title=None, proj=False, - vline=[0.0], + vline=(0.0,), fig_background=None, merge_grads=False, legend=True, diff --git a/mne/viz/topomap.py b/mne/viz/topomap.py index 489f4cb62d8..a531bb7e866 100644 --- a/mne/viz/topomap.py +++ b/mne/viz/topomap.py @@ -183,9 +183,9 @@ def _prepare_topomap_plot(inst, ch_type, sphere=None): # Modify the nirs channel names to indicate they are to be merged # New names will have the form S1_D1xS2_D2 # More than two channels can overlap and be merged - for set in overlapping_channels: - idx = ch_names.index(set[0][:-4]) - new_name = "x".join(s[:-4] for s in set) + for set_ in overlapping_channels: + idx = ch_names.index(set_[0][:-4]) + new_name = "x".join(s[:-4] for s in set_) ch_names[idx] = new_name pos = np.array(pos)[:, :2] # 2D plot, otherwise interpolation bugs @@ -306,12 +306,12 @@ def _add_colorbar( cmap, *, title=None, - format=None, + format_=None, kind=None, ch_type=None, ): """Add a colorbar to an axis.""" - cbar = ax.figure.colorbar(im, format=format, shrink=0.6) + cbar = ax.figure.colorbar(im, format=format_, shrink=0.6) if cmap is not None and cmap[1]: ax.CB = DraggableColorbar(cbar, im, kind, ch_type) cax = cbar.ax @@ -597,7 +597,7 @@ def _plot_projs_topomap( im, cmap, title=units, - format=cbar_fmt, + format_=cbar_fmt, kind="projs_topomap", ch_type=_ch_type, ) @@ -1470,7 +1470,7 @@ def _plot_ica_topomap( im, cmap, title="AU", - format="%3.2f", + format_="%3.2f", kind="ica_topomap", ch_type=ch_type, ) @@ -1709,7 +1709,7 @@ def plot_ica_components( im, cmap, title="AU", - format=cbar_fmt, + format_=cbar_fmt, kind="ica_comp_topomap", ch_type=ch_type, ) @@ -1989,7 +1989,7 @@ def plot_tfr_topomap( im, cmap, title=units, - format=cbar_fmt, + format_=cbar_fmt, kind="tfr_topomap", ch_type=ch_type, ) @@ -2561,7 +2561,7 @@ def _plot_topomap_multi_cbar( ) if colorbar: - cbar, cax = _add_colorbar(ax, im, cmap, title=None, format=cbar_fmt) + cbar, cax = _add_colorbar(ax, im, cmap, title=None, format_=cbar_fmt) cbar.set_ticks(_vlim) if unit is not None: cbar.ax.set_ylabel(unit, fontsize=8) diff --git a/mne/viz/utils.py b/mne/viz/utils.py index 6df8b210c00..eeaf3d1098e 100644 --- a/mne/viz/utils.py +++ b/mne/viz/utils.py @@ -243,12 +243,12 @@ def _validate_if_list_of_axes(axes, obligatory_len=None, name="axes"): ) -def mne_analyze_colormap(limits=[5, 10, 15], format="vtk"): +def mne_analyze_colormap(limits=(5, 10, 15), format="vtk"): # noqa: A002 """Return a colormap similar to that used by mne_analyze. Parameters ---------- - limits : list (or array) of length 3 or 6 + limits : array-like of length 3 or 6 Bounds for the colormap, which will be mirrored across zero if length 3, or completely specified (and potentially asymmetric) if length 6. format : str diff --git a/pyproject.toml b/pyproject.toml index 824f3155148..2090e5b2667 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -205,7 +205,7 @@ builtin = "clear,rare,informal,names,usage" skip = "doc/references.bib" [tool.ruff] -select = ["E", "F", "W", "D", "I", "UP"] +select = ["A", "B006", "D", "E", "F", "I", "W", "UP"] exclude = ["__init__.py", "constants.py", "resources.py"] ignore = [ "D100", # Missing docstring in public module From 03d78f43bf46e98cc4e2905c6e98e6190cf01924 Mon Sep 17 00:00:00 2001 From: Jacob Woessner Date: Wed, 24 Jan 2024 12:43:52 -0600 Subject: [PATCH 161/405] minimum/maximum value of the all-positive/negative data (#12383) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Eric Larson --- doc/changes/devel/12383.newfeature.rst | 1 + mne/evoked.py | 27 ++++++++++++++++++++++---- mne/tests/test_evoked.py | 18 +++++++++++++++++ 3 files changed, 42 insertions(+), 4 deletions(-) create mode 100644 doc/changes/devel/12383.newfeature.rst diff --git a/doc/changes/devel/12383.newfeature.rst b/doc/changes/devel/12383.newfeature.rst new file mode 100644 index 00000000000..f896572eb93 --- /dev/null +++ b/doc/changes/devel/12383.newfeature.rst @@ -0,0 +1 @@ +Add ability to detect minima peaks found in :class:`mne.Evoked` if data is all positive and maxima if data is all negative. \ No newline at end of file diff --git a/mne/evoked.py b/mne/evoked.py index c988368c314..1f694f7c11b 100644 --- a/mne/evoked.py +++ b/mne/evoked.py @@ -914,6 +914,8 @@ def get_peak( time_as_index=False, merge_grads=False, return_amplitude=False, + *, + strict=True, ): """Get location and latency of peak amplitude. @@ -941,6 +943,12 @@ def get_peak( If True, return also the amplitude at the maximum response. .. versionadded:: 0.16 + strict : bool + If True, raise an error if values are all positive when detecting + a minimum (mode='neg'), or all negative when detecting a maximum + (mode='pos'). Defaults to True. + + .. versionadded:: 1.7 Returns ------- @@ -1032,7 +1040,14 @@ def get_peak( data, _ = _merge_ch_data(data, ch_type, []) ch_names = [ch_name[:-1] + "X" for ch_name in ch_names[::2]] - ch_idx, time_idx, max_amp = _get_peak(data, self.times, tmin, tmax, mode) + ch_idx, time_idx, max_amp = _get_peak( + data, + self.times, + tmin, + tmax, + mode, + strict=strict, + ) out = (ch_names[ch_idx], time_idx if time_as_index else self.times[time_idx]) @@ -1949,7 +1964,7 @@ def _write_evokeds(fname, evoked, check=True, *, on_mismatch="raise", overwrite= end_block(fid, FIFF.FIFFB_MEAS) -def _get_peak(data, times, tmin=None, tmax=None, mode="abs"): +def _get_peak(data, times, tmin=None, tmax=None, mode="abs", *, strict=True): """Get feature-index and time of maximum signal from 2D array. Note. This is a 'getter', not a 'finder'. For non-evoked type @@ -1970,6 +1985,10 @@ def _get_peak(data, times, tmin=None, tmax=None, mode="abs"): values will be considered. If 'neg' only negative values will be considered. If 'abs' absolute values will be considered. Defaults to 'abs'. + strict : bool + If True, raise an error if values are all positive when detecting + a minimum (mode='neg'), or all negative when detecting a maximum + (mode='pos'). Defaults to True. Returns ------- @@ -2008,12 +2027,12 @@ def _get_peak(data, times, tmin=None, tmax=None, mode="abs"): maxfun = np.argmax if mode == "pos": - if not np.any(data[~mask] > 0): + if strict and not np.any(data[~mask] > 0): raise ValueError( "No positive values encountered. Cannot " "operate in pos mode." ) elif mode == "neg": - if not np.any(data[~mask] < 0): + if strict and not np.any(data[~mask] < 0): raise ValueError( "No negative values encountered. Cannot " "operate in neg mode." ) diff --git a/mne/tests/test_evoked.py b/mne/tests/test_evoked.py index 2c5f064606d..31110596be6 100644 --- a/mne/tests/test_evoked.py +++ b/mne/tests/test_evoked.py @@ -589,6 +589,24 @@ def test_get_peak(): with pytest.raises(ValueError, match="No positive values"): evoked_all_neg.get_peak(mode="pos") + # Test finding minimum and maximum values + evoked_all_neg_outlier = evoked_all_neg.copy() + evoked_all_pos_outlier = evoked_all_pos.copy() + + # Add an outlier to the data + evoked_all_neg_outlier.data[0, 15] = -1e-20 + evoked_all_pos_outlier.data[0, 15] = 1e-20 + + ch_name, time_idx, max_amp = evoked_all_neg_outlier.get_peak( + mode="pos", return_amplitude=True, strict=False + ) + assert max_amp == -1e-20 + + ch_name, time_idx, min_amp = evoked_all_pos_outlier.get_peak( + mode="neg", return_amplitude=True, strict=False + ) + assert min_amp == 1e-20 + # Test interaction between `mode` and `tmin` / `tmax` # For the test, create an Evoked where half of the values are negative # and the rest is positive From f060e6b470704823dad6120cd685ad7390c1eae6 Mon Sep 17 00:00:00 2001 From: btkcodedev Date: Thu, 25 Jan 2024 13:27:26 +0530 Subject: [PATCH 162/405] chore(docs): update preprocessing tutorial for using inst.pick() instead of pick_types() (#12326) Co-authored-by: Marijn van Vliet Co-authored-by: Mathieu Scheltienne Co-authored-by: Mathieu Scheltienne --- doc/changes/devel/12326.other.rst | 1 + doc/changes/names.inc | 2 ++ tutorials/preprocessing/15_handling_bad_channels.py | 5 +++-- 3 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 doc/changes/devel/12326.other.rst diff --git a/doc/changes/devel/12326.other.rst b/doc/changes/devel/12326.other.rst new file mode 100644 index 00000000000..b8f2966bbf9 --- /dev/null +++ b/doc/changes/devel/12326.other.rst @@ -0,0 +1 @@ +Updated the text in the preprocessing tutorial to use :class:`mne.io.Raw.pick()` instead of the legacy :class:`mne.io.Raw.pick_types()`, by :newcontrib:`btkcodedev`. diff --git a/doc/changes/names.inc b/doc/changes/names.inc index 811029ddaa7..0389f75e83e 100644 --- a/doc/changes/names.inc +++ b/doc/changes/names.inc @@ -68,6 +68,8 @@ .. _Bruno Nicenboim: https://bnicenboim.github.io +.. _btkcodedev: https://github.com/btkcodedev + .. _buildqa: https://github.com/buildqa .. _Carlos de la Torre-Ortiz: https://ctorre.me diff --git a/tutorials/preprocessing/15_handling_bad_channels.py b/tutorials/preprocessing/15_handling_bad_channels.py index daac97976a5..06e9ffd6e53 100644 --- a/tutorials/preprocessing/15_handling_bad_channels.py +++ b/tutorials/preprocessing/15_handling_bad_channels.py @@ -238,8 +238,9 @@ fig.suptitle(title, size="xx-large", weight="bold") # %% -# Note that we used the ``exclude=[]`` trick in the call to -# :meth:`~mne.io.Raw.pick_types` to make sure the bad channels were not +# Note that the method :meth:`~mne.io.Raw.pick()` default +# arguments includes ``exclude=()`` which ensures that bad +# channels are not # automatically dropped from the selection. Here is the corresponding example # with the interpolated gradiometer channel; since there are more channels # we'll use a more transparent gray color this time: From 1b0d20850fa2ff60fb61b8a7e7a68a38323dcf07 Mon Sep 17 00:00:00 2001 From: Kristijan Armeni Date: Thu, 25 Jan 2024 13:05:37 -0500 Subject: [PATCH 163/405] [ENH] Speed up `read_raw_neuralynx()` on large datasets with many gaps (#12371) Co-authored-by: Eric Larson --- doc/changes/devel/12371.newfeature.rst | 1 + mne/io/neuralynx/neuralynx.py | 7 ++----- 2 files changed, 3 insertions(+), 5 deletions(-) create mode 100644 doc/changes/devel/12371.newfeature.rst diff --git a/doc/changes/devel/12371.newfeature.rst b/doc/changes/devel/12371.newfeature.rst new file mode 100644 index 00000000000..4d28ff1f5ce --- /dev/null +++ b/doc/changes/devel/12371.newfeature.rst @@ -0,0 +1 @@ +Speed up :func:`mne.io.read_raw_neuralynx` on large datasets with many gaps, by `Kristijan Armeni`_. \ No newline at end of file diff --git a/mne/io/neuralynx/neuralynx.py b/mne/io/neuralynx/neuralynx.py index 55de7579d67..ab768d57b13 100644 --- a/mne/io/neuralynx/neuralynx.py +++ b/mne/io/neuralynx/neuralynx.py @@ -223,11 +223,8 @@ def __init__( [np.full(shape=(n,), fill_value=i) for i, n in enumerate(sizes_sorted)] ) - # construct Annotations() - gap_seg_ids = np.unique(sample2segment)[gap_indicator] - gap_start_ids = np.array( - [np.where(sample2segment == seg_id)[0][0] for seg_id in gap_seg_ids] - ) + # get the start sample index for each gap segment () + gap_start_ids = np.cumsum(np.hstack([[0], sizes_sorted[:-1]]))[gap_indicator] # recreate time axis for gap annotations mne_times = np.arange(0, len(sample2segment)) / info["sfreq"] From 87cfea5323c61ded31da970c07238c447f4fe37f Mon Sep 17 00:00:00 2001 From: Daniel McCloy Date: Fri, 26 Jan 2024 14:16:51 -0600 Subject: [PATCH 164/405] fix misleading docstring return description for covariance SVD plot (#12359) --- mne/viz/misc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mne/viz/misc.py b/mne/viz/misc.py index 3d8a9469620..c9be85f8f9e 100644 --- a/mne/viz/misc.py +++ b/mne/viz/misc.py @@ -130,7 +130,7 @@ def plot_cov( fig_cov : instance of matplotlib.figure.Figure The covariance plot. fig_svd : instance of matplotlib.figure.Figure | None - The SVD spectra plot of the covariance. + The SVD plot of the covariance (i.e., the eigenvalues or "matrix spectrum"). See Also -------- From 4ccd30fed3d83953506b4ff0532429973a0f797a Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Fri, 26 Jan 2024 15:39:44 -0500 Subject: [PATCH 165/405] BUG: Fix bug with regress_artifact picking (#12389) --- doc/changes/devel/12389.bugfix.rst | 1 + mne/_fiff/pick.py | 12 ++- mne/_fiff/tests/test_pick.py | 6 ++ mne/datasets/config.py | 14 ++- mne/datasets/phantom_kit/phantom_kit.py | 2 +- mne/preprocessing/_regress.py | 24 ++--- mne/preprocessing/tests/test_regress.py | 13 +++ tutorials/inverse/95_phantom_KIT.py | 125 ++++++------------------ 8 files changed, 85 insertions(+), 112 deletions(-) create mode 100644 doc/changes/devel/12389.bugfix.rst diff --git a/doc/changes/devel/12389.bugfix.rst b/doc/changes/devel/12389.bugfix.rst new file mode 100644 index 00000000000..85892df97a8 --- /dev/null +++ b/doc/changes/devel/12389.bugfix.rst @@ -0,0 +1 @@ +Fix bug where :func:`mne.preprocessing.regress_artifact` projection check was not specific to the channels being processed, by `Eric Larson`_. diff --git a/mne/_fiff/pick.py b/mne/_fiff/pick.py index 2722e91f517..9e2e369ab71 100644 --- a/mne/_fiff/pick.py +++ b/mne/_fiff/pick.py @@ -649,7 +649,8 @@ def pick_info(info, sel=(), copy=True, verbose=None): return info elif len(sel) == 0: raise ValueError("No channels match the selection.") - n_unique = len(np.unique(np.arange(len(info["ch_names"]))[sel])) + ch_set = set(info["ch_names"][k] for k in sel) + 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)) @@ -687,6 +688,15 @@ def pick_info(info, sel=(), copy=True, verbose=None): if info.get("custom_ref_applied", False) and not _electrode_types(info): with info._unlock(): info["custom_ref_applied"] = FIFF.FIFFV_MNE_CUSTOM_REF_OFF + # remove unused projectors + if info.get("projs", False): + projs = list() + for p in info["projs"]: + if any(ch_name in ch_set for ch_name in p["data"]["col_names"]): + projs.append(p) + if len(projs) != len(info["projs"]): + with info._unlock(): + info["projs"] = projs info._check_consistency() return info diff --git a/mne/_fiff/tests/test_pick.py b/mne/_fiff/tests/test_pick.py index 5494093cd23..fa503a04ab3 100644 --- a/mne/_fiff/tests/test_pick.py +++ b/mne/_fiff/tests/test_pick.py @@ -558,11 +558,17 @@ def test_clean_info_bads(): # simulate the bad channels raw.info["bads"] = eeg_bad_ch + meg_bad_ch + assert len(raw.info["projs"]) == 3 + raw.set_eeg_reference(projection=True) + assert len(raw.info["projs"]) == 4 + # simulate the call to pick_info excluding the bad eeg channels info_eeg = pick_info(raw.info, picks_eeg) + assert len(info_eeg["projs"]) == 1 # simulate the call to pick_info excluding the bad meg channels info_meg = pick_info(raw.info, picks_meg) + assert len(info_meg["projs"]) == 3 assert info_eeg["bads"] == eeg_bad_ch assert info_meg["bads"] == meg_bad_ch diff --git a/mne/datasets/config.py b/mne/datasets/config.py index b7780778f24..238b61998d6 100644 --- a/mne/datasets/config.py +++ b/mne/datasets/config.py @@ -87,8 +87,12 @@ # To update the `testing` or `misc` datasets, push or merge commits to their # respective repos, and make a new release of the dataset on GitHub. Then # update the checksum in the MNE_DATASETS dict below, and change version -# here: ↓↓↓↓↓ ↓↓↓ -RELEASES = dict(testing="0.151", misc="0.27") +# here: ↓↓↓↓↓↓↓↓ +RELEASES = dict( + testing="0.151", + misc="0.27", + phantom_kit="0.2", +) TESTING_VERSIONED = f'mne-testing-data-{RELEASES["testing"]}' MISC_VERSIONED = f'mne-misc-data-{RELEASES["misc"]}' @@ -176,9 +180,9 @@ ) MNE_DATASETS["phantom_kit"] = dict( - archive_name="MNE-phantom-KIT-24bit.zip", - hash="md5:CAF82EE978DD473C7DE6C1034D9CCD45", - url="https://osf.io/download/svnt3/", + archive_name="MNE-phantom-KIT-data.tar.gz", + hash="md5:7bfdf40bbeaf17a66c99c695640e0740", + url="https://osf.io/fb6ya/download?version=1", folder_name="MNE-phantom-KIT-data", config_key="MNE_DATASETS_PHANTOM_KIT_PATH", ) diff --git a/mne/datasets/phantom_kit/phantom_kit.py b/mne/datasets/phantom_kit/phantom_kit.py index a4eac6c4a50..d57ca875f2c 100644 --- a/mne/datasets/phantom_kit/phantom_kit.py +++ b/mne/datasets/phantom_kit/phantom_kit.py @@ -10,7 +10,7 @@ def data_path( ): # noqa: D103 return _download_mne_dataset( name="phantom_kit", - processor="unzip", + processor="untar", path=path, force_update=force_update, update_path=update_path, diff --git a/mne/preprocessing/_regress.py b/mne/preprocessing/_regress.py index 31a842f7d4f..260796a221d 100644 --- a/mne/preprocessing/_regress.py +++ b/mne/preprocessing/_regress.py @@ -6,7 +6,7 @@ import numpy as np -from .._fiff.pick import _picks_to_idx +from .._fiff.pick import _picks_to_idx, pick_info from ..defaults import _BORDER_DEFAULT, _EXTRAPOLATE_DEFAULT, _INTERPOLATION_DEFAULT from ..epochs import BaseEpochs from ..evoked import Evoked @@ -178,9 +178,7 @@ def fit(self, inst): reference (see :func:`mne.set_eeg_reference`) before performing EOG regression. """ - self._check_inst(inst) - picks = _picks_to_idx(inst.info, self.picks, none="data", exclude=self.exclude) - picks_artifact = _picks_to_idx(inst.info, self.picks_artifact) + picks, picks_artifact = self._check_inst(inst) # Calculate regression coefficients. Add a row of ones to also fit the # intercept. @@ -232,9 +230,7 @@ def apply(self, inst, copy=True): """ if copy: inst = inst.copy() - self._check_inst(inst) - picks = _picks_to_idx(inst.info, self.picks, none="data", exclude=self.exclude) - picks_artifact = _picks_to_idx(inst.info, self.picks_artifact) + picks, picks_artifact = self._check_inst(inst) # Check that the channels are compatible with the regression weights. ref_picks = _picks_to_idx( @@ -324,19 +320,25 @@ def _check_inst(self, inst): _validate_type( inst, (BaseRaw, BaseEpochs, Evoked), "inst", "Raw, Epochs, Evoked" ) - if _needs_eeg_average_ref_proj(inst.info): + picks = _picks_to_idx(inst.info, self.picks, none="data", exclude=self.exclude) + picks_artifact = _picks_to_idx(inst.info, self.picks_artifact) + all_picks = np.unique(np.concatenate([picks, picks_artifact])) + use_info = pick_info(inst.info, all_picks) + del all_picks + if _needs_eeg_average_ref_proj(use_info): raise RuntimeError( - "No reference for the EEG channels has been " - "set. Use inst.set_eeg_reference() to do so." + "No average reference for the EEG channels has been " + "set. Use inst.set_eeg_reference(projection=True) to do so." ) if self.proj and not inst.proj: inst.apply_proj() - if not inst.proj and len(inst.info.get("projs", [])) > 0: + if not inst.proj and len(use_info.get("projs", [])) > 0: raise RuntimeError( "Projections need to be applied before " "regression can be performed. Use the " ".apply_proj() method to do so." ) + return picks, picks_artifact def __repr__(self): """Produce a string representation of this object.""" diff --git a/mne/preprocessing/tests/test_regress.py b/mne/preprocessing/tests/test_regress.py index 8050b6bebf7..48d960e0464 100644 --- a/mne/preprocessing/tests/test_regress.py +++ b/mne/preprocessing/tests/test_regress.py @@ -42,6 +42,19 @@ def test_regress_artifact(): epochs, betas = regress_artifact(epochs, picks="eog", picks_artifact="eog") assert np.ptp(epochs.get_data("eog")) < 1e-15 # constant value assert_allclose(betas, 1) + # proj should only be required of channels being processed + raw = read_raw_fif(raw_fname).crop(0, 1).load_data() + raw.del_proj() + raw.set_eeg_reference(projection=True) + model = EOGRegression(proj=False, picks="meg", picks_artifact="eog") + model.fit(raw) + model.apply(raw) + model = EOGRegression(proj=False, picks="eeg", picks_artifact="eog") + with pytest.raises(RuntimeError, match="Projections need to be applied"): + model.fit(raw) + raw.del_proj() + with pytest.raises(RuntimeError, match="No average reference for the EEG"): + model.fit(raw) @testing.requires_testing_data diff --git a/tutorials/inverse/95_phantom_KIT.py b/tutorials/inverse/95_phantom_KIT.py index 6a07658e13a..75e0025a9c2 100644 --- a/tutorials/inverse/95_phantom_KIT.py +++ b/tutorials/inverse/95_phantom_KIT.py @@ -15,9 +15,8 @@ # Copyright the MNE-Python contributors. # %% -import matplotlib.pyplot as plt +import mne_bids import numpy as np -from scipy.signal import find_peaks import mne @@ -25,14 +24,33 @@ actual_pos, actual_ori = mne.dipole.get_phantom_dipoles("oyama") actual_pos, actual_ori = actual_pos[:49], actual_ori[:49] # only 49 of 50 dipoles -raw = mne.io.read_raw_kit(data_path / "002_phantom_11Hz_100uA.con") -# cut from ~800 to ~300s for speed, and also at convenient dip stim boundaries -# chosen by examining MISC 017 by eye. -raw.crop(11.5, 302.9).load_data() -raw.filter(None, 40) # 11 Hz stimulation, no need to keep higher freqs +bids_path = mne_bids.BIDSPath( + root=data_path, + subject="01", + task="phantom", + run="01", + datatype="meg", +) +# ignore warning about misc units +raw = mne_bids.read_raw_bids(bids_path).load_data() + +# Let's apply a little bit of preprocessing (temporal filtering and reference +# regression) +picks_artifact = ["MISC 001", "MISC 002", "MISC 003"] +picks = np.r_[ + mne.pick_types(raw.info, meg=True), + mne.pick_channels(raw.info["ch_names"], picks_artifact), +] +raw.filter(None, 40, picks=picks) +mne.preprocessing.regress_artifact( + raw, picks="meg", picks_artifact=picks_artifact, copy=False, proj=False +) plot_scalings = dict(mag=5e-12) # large-amplitude sinusoids raw_plot_kwargs = dict(duration=15, n_channels=50, scalings=plot_scalings) -raw.plot(**raw_plot_kwargs) +events, event_id = mne.events_from_annotations(raw) +raw.plot(events=events, **raw_plot_kwargs) +n_dip = len(event_id) +assert n_dip == 49 # sanity check # %% # We can also look at the power spectral density to see the phantom oscillations at @@ -46,81 +64,10 @@ fig.axes[0].axvline(dip_freq, color="r", ls="--", lw=2, zorder=4) # %% -# To find the events, we can look at the MISC channel that recorded the activations. -# Here we use a very simple thresholding approach to find the events. -# The MISC 017 channel holds the dipole activations, which are 2-cycle 11 Hz sinusoidal -# bursts with the initial sinusoidal deflection downward, so we do a little bit of -# signal manipulation to help :func:`~scipy.signal.find_peaks`. - -# Figure out events -dip_act, dip_t = raw["MISC 017"] -dip_act = dip_act[0] # 2D to 1D array -dip_act -= dip_act.mean() # remove DC offset -dip_act *= -1 # invert so first deflection is positive -thresh = np.percentile(dip_act, 90) -min_dist = raw.info["sfreq"] / dip_freq * 0.9 # 90% of period, to be safe -peaks = find_peaks(dip_act, height=thresh, distance=min_dist)[0] -assert len(peaks) % 2 == 0 # 2-cycle modulations -peaks = peaks[::2] # take only first peaks of each 2-cycle burst - -fig, ax = plt.subplots(layout="constrained", figsize=(12, 4)) -stop = int(15 * raw.info["sfreq"]) # 15 sec -ax.plot(dip_t[:stop], dip_act[:stop], color="k", lw=1) -ax.axhline(thresh, color="r", ls="--", lw=1) -peak_idx = peaks[peaks < stop] -ax.plot(dip_t[peak_idx], dip_act[peak_idx], "ro", zorder=5, ms=5) -ax.set(xlabel="Time (s)", ylabel="Dipole activation (AU)\n(MISC 017 adjusted)") -ax.set(xlim=dip_t[[0, stop - 1]]) - -# We know that there are 32 dipoles, so mark the first ones as well -n_dip = 49 -assert len(peaks) % n_dip == 0 # we found them all (hopefully) -ax.plot(dip_t[peak_idx[::n_dip]], dip_act[peak_idx[::n_dip]], "bo", zorder=4, ms=10) - -# Knowing we've caught the top of the first cycle of a 11 Hz sinusoid, plot onsets -# with red X's. -onsets = peaks - np.round(raw.info["sfreq"] / dip_freq / 4.0).astype( - int -) # shift to start -onset_idx = onsets[onsets < stop] -ax.plot(dip_t[onset_idx], dip_act[onset_idx], "rx", zorder=5, ms=5) - -# %% -# Given the onsets are now stored in ``peaks``, we can create our events array and plot -# on our raw data. +# Now we can figure out our epoching parameters and epoch the data and plot it. -n_rep = len(peaks) // n_dip -events = np.zeros((len(peaks), 3), int) -events[:, 0] = onsets + raw.first_samp -events[:, 2] = np.tile(np.arange(1, n_dip + 1), n_rep) -raw.plot(events=events, **raw_plot_kwargs) - -# %% -# Now we can figure out our epoching parameters and epoch the data, sanity checking -# some values along the way knowing how the stimulation was done. - -# Sanity check and determine epoching params -deltas = np.diff(events[:, 0], axis=0) -group_deltas = deltas[n_dip - 1 :: n_dip] / raw.info["sfreq"] # gap between 49 and 1 -assert (group_deltas > 0.8).all() -assert (group_deltas < 0.9).all() -others = np.delete(deltas, np.arange(n_dip - 1, len(deltas), n_dip)) # remove 49->1 -others = others / raw.info["sfreq"] -assert (others > 0.25).all() -assert (others < 0.3).all() -tmax = 1 / dip_freq * 2.0 # 2 cycles -tmin = tmax - others.min() -assert tmin < 0 -epochs = mne.Epochs( - raw, - events, - tmin=tmin, - tmax=tmax, - baseline=(None, 0), - decim=10, - picks="data", - preload=True, -) +tmin, tmax = -0.08, 0.18 +epochs = mne.Epochs(raw, tmin=tmin, tmax=tmax, decim=10, picks="data", preload=True) del raw epochs.plot(scalings=plot_scalings) @@ -131,7 +78,7 @@ t_peak = 1.0 / dip_freq / 4.0 data = np.zeros((len(epochs.ch_names), n_dip)) for di in range(n_dip): - data[:, [di]] = epochs[str(di + 1)].average().crop(t_peak, t_peak).data + data[:, [di]] = epochs[f"dip{di + 1:02d}"].average().crop(t_peak, t_peak).data evoked = mne.EvokedArray(data, epochs.info, tmin=0, comment="KIT phantom activations") evoked.plot_joint() @@ -141,22 +88,12 @@ trans = mne.transforms.Transform("head", "mri", np.eye(4)) sphere = mne.make_sphere_model(r0=(0.0, 0.0, 0.0), head_radius=0.08) cov = mne.compute_covariance(epochs, tmax=0, method="empirical") -# We need to correct the ``dev_head_t`` because it's incorrect for these data! -# relative to the helmet: hleft, forward, up -translation = mne.transforms.translation(x=0.01, y=-0.015, z=-0.088) -# pitch down (rot about x/R), roll left (rot about y/A), yaw left (rot about z/S) -rotation = mne.transforms.rotation( - x=np.deg2rad(5), - y=np.deg2rad(-1), - z=np.deg2rad(-3), -) -evoked.info["dev_head_t"]["trans"][:] = translation @ rotation dip, residual = mne.fit_dipole(evoked, cov, sphere, n_jobs=None) # %% # Finally let's look at the results. -# sphinx_gallery_thumbnail_number = 7 +# sphinx_gallery_thumbnail_number = 5 print(f"Average amplitude: {np.mean(dip.amplitude) * 1e9:0.1f} nAm") print(f"Average GOF: {np.mean(dip.gof):0.1f}%") From 990ce18d4eaf0af6d0f0aadbc41fd86aa826ea59 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Mon, 29 Jan 2024 11:09:43 -0500 Subject: [PATCH 166/405] MAINT: Deal with pytest 8.0 (#12376) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- doc/changes/devel/12376.dependency.rst | 1 + environment.yml | 2 +- mne/_fiff/tests/test_meas_info.py | 7 ++-- mne/beamformer/tests/test_lcmv.py | 27 ++++++++-------- mne/conftest.py | 5 ++- mne/export/tests/test_export.py | 8 +++-- mne/forward/tests/test_forward.py | 11 ++++--- mne/forward/tests/test_make_forward.py | 3 +- mne/io/artemis123/tests/test_artemis123.py | 4 ++- mne/io/brainvision/brainvision.py | 14 ++++---- mne/io/brainvision/tests/test_brainvision.py | 14 +++++--- mne/io/cnt/tests/test_cnt.py | 11 ++++--- mne/io/ctf/tests/test_ctf.py | 2 +- mne/io/edf/tests/test_edf.py | 3 +- mne/io/edf/tests/test_gdf.py | 2 +- mne/io/eeglab/tests/test_eeglab.py | 12 ++++--- mne/io/eyelink/tests/test_eyelink.py | 5 ++- mne/io/fieldtrip/tests/test_fieldtrip.py | 18 +++++------ mne/io/fiff/tests/test_raw_fiff.py | 20 +++++------- mne/preprocessing/tests/test_ica.py | 20 ++++++++---- mne/preprocessing/tests/test_ssp.py | 11 ++++--- mne/report/tests/test_report.py | 12 ++++--- mne/source_space/tests/test_source_space.py | 4 ++- mne/stats/tests/test_cluster_level.py | 8 +++-- mne/tests/test_bem.py | 34 ++++++++++++-------- mne/tests/test_chpi.py | 10 ++++-- mne/tests/test_cov.py | 8 ++--- mne/tests/test_epochs.py | 7 ++-- mne/tests/test_evoked.py | 4 +-- mne/tests/test_source_estimate.py | 2 +- mne/time_frequency/tests/test_spectrum.py | 3 +- mne/utils/tests/test_check.py | 3 +- mne/utils/tests/test_logging.py | 2 +- mne/viz/tests/test_epochs.py | 13 +++++--- mne/viz/tests/test_evoked.py | 6 ++-- mne/viz/tests/test_ica.py | 6 ++-- mne/viz/tests/test_misc.py | 13 +++++--- mne/viz/tests/test_raw.py | 4 ++- mne/viz/tests/test_topo.py | 6 ++-- pyproject.toml | 7 ++-- 40 files changed, 212 insertions(+), 140 deletions(-) create mode 100644 doc/changes/devel/12376.dependency.rst diff --git a/doc/changes/devel/12376.dependency.rst b/doc/changes/devel/12376.dependency.rst new file mode 100644 index 00000000000..148ce8ac9ec --- /dev/null +++ b/doc/changes/devel/12376.dependency.rst @@ -0,0 +1 @@ +For developers, ``pytest>=8.0`` is now required for running unit tests, by `Eric Larson`_. diff --git a/environment.yml b/environment.yml index 0a77f57e5fe..cc2f8e752d5 100644 --- a/environment.yml +++ b/environment.yml @@ -6,7 +6,7 @@ dependencies: - pip - numpy - scipy - - openblas!=0.3.26 # until https://github.com/conda-forge/scipy-feedstock/pull/268 lands + - openblas - matplotlib - tqdm - pooch>=1.5 diff --git a/mne/_fiff/tests/test_meas_info.py b/mne/_fiff/tests/test_meas_info.py index 9038c71a382..3cf1f79cceb 100644 --- a/mne/_fiff/tests/test_meas_info.py +++ b/mne/_fiff/tests/test_meas_info.py @@ -350,9 +350,10 @@ def test_read_write_info(tmp_path): @testing.requires_testing_data def test_dir_warning(): """Test that trying to read a bad filename emits a warning before an error.""" - with pytest.raises(OSError, match="directory"): - with pytest.warns(RuntimeWarning, match="foo"): - read_info(ctf_fname) + with pytest.raises(OSError, match="directory"), pytest.warns( + RuntimeWarning, match="does not conform" + ): + read_info(ctf_fname) def test_io_dig_points(tmp_path): diff --git a/mne/beamformer/tests/test_lcmv.py b/mne/beamformer/tests/test_lcmv.py index 15c1a2ba5eb..f6c7ef20492 100644 --- a/mne/beamformer/tests/test_lcmv.py +++ b/mne/beamformer/tests/test_lcmv.py @@ -589,19 +589,20 @@ def test_make_lcmv_sphere(pick_ori, weight_norm): fwd_sphere = mne.make_forward_solution(evoked.info, None, src, sphere) # Test that we get an error if not reducing rank - with pytest.raises(ValueError, match="Singular matrix detected"): - with pytest.warns(RuntimeWarning, match="positive semidefinite"): - make_lcmv( - evoked.info, - fwd_sphere, - data_cov, - reg=0.1, - noise_cov=noise_cov, - weight_norm=weight_norm, - pick_ori=pick_ori, - reduce_rank=False, - rank="full", - ) + with pytest.raises( + ValueError, match="Singular matrix detected" + ), _record_warnings(), pytest.warns(RuntimeWarning, match="positive semidefinite"): + make_lcmv( + evoked.info, + fwd_sphere, + data_cov, + reg=0.1, + noise_cov=noise_cov, + weight_norm=weight_norm, + pick_ori=pick_ori, + reduce_rank=False, + rank="full", + ) # Now let's reduce it filters = make_lcmv( diff --git a/mne/conftest.py b/mne/conftest.py index 42f1e26c1e7..4bab9dc1186 100644 --- a/mne/conftest.py +++ b/mne/conftest.py @@ -32,6 +32,7 @@ _assert_no_instances, _check_qt_version, _pl, + _record_warnings, _TempDir, numerics, ) @@ -801,7 +802,9 @@ def src_volume_labels(): """Create a 7mm source space with labels.""" pytest.importorskip("nibabel") volume_labels = mne.get_volume_labels_from_aseg(fname_aseg) - with pytest.warns(RuntimeWarning, match="Found no usable.*t-vessel.*"): + with _record_warnings(), pytest.warns( + RuntimeWarning, match="Found no usable.*t-vessel.*" + ): src = mne.setup_volume_source_space( "sample", 7.0, diff --git a/mne/export/tests/test_export.py b/mne/export/tests/test_export.py index e55647d54e6..fc5e68c9225 100644 --- a/mne/export/tests/test_export.py +++ b/mne/export/tests/test_export.py @@ -78,7 +78,9 @@ def test_export_raw_pybv(tmp_path, meas_date, orig_time, ext): raw.set_annotations(annots) temp_fname = tmp_path / ("test" + ext) - with pytest.warns(RuntimeWarning, match="'short' format. Converting"): + with _record_warnings(), pytest.warns( + RuntimeWarning, match="'short' format. Converting" + ): raw.export(temp_fname) raw_read = read_raw_brainvision(str(temp_fname).replace(".eeg", ".vhdr")) assert raw.ch_names == raw_read.ch_names @@ -301,7 +303,9 @@ def test_export_edf_signal_clipping(tmp_path, physical_range, exceeded_bound): raw = read_raw_fif(fname_raw) raw.pick(picks=["eeg", "ecog", "seeg"]).load_data() temp_fname = tmp_path / "test.edf" - with pytest.warns(RuntimeWarning, match=f"The {exceeded_bound}"): + with _record_warnings(), pytest.warns( + RuntimeWarning, match=f"The {exceeded_bound}" + ): raw.export(temp_fname, physical_range=physical_range) raw_read = read_raw_edf(temp_fname, preload=True) assert raw_read.get_data().min() >= physical_range[0] diff --git a/mne/forward/tests/test_forward.py b/mne/forward/tests/test_forward.py index 9020f7c9a26..dd73d1099f1 100644 --- a/mne/forward/tests/test_forward.py +++ b/mne/forward/tests/test_forward.py @@ -37,7 +37,7 @@ ) from mne.io import read_info from mne.label import read_label -from mne.utils import requires_mne, run_subprocess +from mne.utils import _record_warnings, requires_mne, run_subprocess data_path = testing.data_path(download=False) fname_meeg = data_path / "MEG" / "sample" / "sample_audvis_trunc-meg-eeg-oct-4-fwd.fif" @@ -230,7 +230,9 @@ def test_apply_forward(): # Evoked evoked = read_evokeds(fname_evoked, condition=0) evoked.pick(picks="meg") - with pytest.warns(RuntimeWarning, match="only .* positive values"): + with _record_warnings(), pytest.warns( + RuntimeWarning, match="only .* positive values" + ): evoked = apply_forward(fwd, stc, evoked.info, start=start, stop=stop) data = evoked.data times = evoked.times @@ -248,13 +250,14 @@ def test_apply_forward(): stc.tmin, stc.tstep, ) - with pytest.warns(RuntimeWarning, match="very large"): + large_ctx = pytest.warns(RuntimeWarning, match="very large") + with large_ctx: evoked_2 = apply_forward(fwd, stc_vec, evoked.info) assert np.abs(evoked_2.data).mean() > 1e-5 assert_allclose(evoked.data, evoked_2.data, atol=1e-10) # Raw - with pytest.warns(RuntimeWarning, match="only .* positive values"): + with large_ctx, pytest.warns(RuntimeWarning, match="only .* positive values"): raw_proj = apply_forward_raw(fwd, stc, evoked.info, start=start, stop=stop) data, times = raw_proj[:, :] diff --git a/mne/forward/tests/test_make_forward.py b/mne/forward/tests/test_make_forward.py index 7965ae2343c..4e52b9a50b0 100644 --- a/mne/forward/tests/test_make_forward.py +++ b/mne/forward/tests/test_make_forward.py @@ -44,6 +44,7 @@ from mne.surface import _get_ico_surface from mne.transforms import Transform from mne.utils import ( + _record_warnings, catch_logging, requires_mne, requires_mne_mark, @@ -198,7 +199,7 @@ def test_magnetic_dipole(): r0 = coils[0]["rmag"][[0]] with pytest.raises(RuntimeError, match="Coil too close"): _magnetic_dipole_field_vec(r0, coils[:1]) - with pytest.warns(RuntimeWarning, match="Coil too close"): + with _record_warnings(), pytest.warns(RuntimeWarning, match="Coil too close"): fwd = _magnetic_dipole_field_vec(r0, coils[:1], too_close="warning") assert not np.isfinite(fwd).any() with np.errstate(invalid="ignore"): diff --git a/mne/io/artemis123/tests/test_artemis123.py b/mne/io/artemis123/tests/test_artemis123.py index 9a1cdb36eec..2dac9645c33 100644 --- a/mne/io/artemis123/tests/test_artemis123.py +++ b/mne/io/artemis123/tests/test_artemis123.py @@ -97,7 +97,9 @@ def test_dev_head_t(): assert_equal(raw.info["sfreq"], 5000.0) # test with head loc and digitization - with pytest.warns(RuntimeWarning, match="Large difference"): + with pytest.warns(RuntimeWarning, match="consistency"), pytest.warns( + RuntimeWarning, match="Large difference" + ): raw = read_raw_artemis123( short_HPI_dip_fname, add_head_trans=True, pos_fname=dig_fname ) diff --git a/mne/io/brainvision/brainvision.py b/mne/io/brainvision/brainvision.py index 5dea11dd35d..5aabdbb626c 100644 --- a/mne/io/brainvision/brainvision.py +++ b/mne/io/brainvision/brainvision.py @@ -344,9 +344,10 @@ def _read_annotations_brainvision(fname, sfreq="auto"): def _check_bv_version(header, kind): """Check the header version.""" - _data_err = """\ - MNE-Python currently only supports %s versions 1.0 and 2.0, got unparsable\ - %r. Contact MNE-Python developers for support.""" + _data_err = ( + "MNE-Python currently only supports %s versions 1.0 and 2.0, got unparsable " + "%r. Contact MNE-Python developers for support." + ) # optional space, optional Core or V-Amp, optional Exchange, # Version/Header, optional comma, 1/2 _data_re = ( @@ -355,14 +356,15 @@ def _check_bv_version(header, kind): assert kind in ("header", "marker") - if header == "": - warn(f"Missing header in {kind} file.") for version in range(1, 3): this_re = _data_re % (kind.capitalize(), version) if re.search(this_re, header) is not None: return version else: - warn(_data_err % (kind, header)) + if header == "": + warn(f"Missing header in {kind} file.") + else: + warn(_data_err % (kind, header)) _orientation_dict = dict(MULTIPLEXED="F", VECTORIZED="C") diff --git a/mne/io/brainvision/tests/test_brainvision.py b/mne/io/brainvision/tests/test_brainvision.py index 9c48be78a23..166c3564fae 100644 --- a/mne/io/brainvision/tests/test_brainvision.py +++ b/mne/io/brainvision/tests/test_brainvision.py @@ -20,7 +20,7 @@ from mne.datasets import testing from mne.io import read_raw_brainvision, read_raw_fif from mne.io.tests.test_raw import _test_raw_reader -from mne.utils import _stamp_to_dt, object_diff +from mne.utils import _record_warnings, _stamp_to_dt, object_diff data_dir = Path(__file__).parent / "data" vhdr_path = data_dir / "test.vhdr" @@ -72,6 +72,8 @@ # This should be amend in its own PR. montage = data_dir / "test.hpts" +_no_dig = pytest.warns(RuntimeWarning, match="No info on DataPoints") + def test_orig_units(recwarn): """Test exposure of original channel units.""" @@ -473,7 +475,7 @@ def test_brainvision_data_partially_disabled_hw_filters(): def test_brainvision_data_software_filters_latin1_global_units(): """Test reading raw Brain Vision files.""" - with pytest.warns(RuntimeWarning, match="software filter"): + with _no_dig, pytest.warns(RuntimeWarning, match="software filter"): raw = _test_raw_reader( read_raw_brainvision, vhdr_fname=vhdr_old_path, @@ -485,7 +487,7 @@ def test_brainvision_data_software_filters_latin1_global_units(): assert raw.info["lowpass"] == 50.0 # test sensor name with spaces (#9299) - with pytest.warns(RuntimeWarning, match="software filter"): + with _no_dig, pytest.warns(RuntimeWarning, match="software filter"): raw = _test_raw_reader( read_raw_brainvision, vhdr_fname=vhdr_old_longname_path, @@ -566,7 +568,7 @@ def test_brainvision_data(): def test_brainvision_vectorized_data(): """Test reading BrainVision data files with vectorized data.""" - with pytest.warns(RuntimeWarning, match="software filter"): + with _no_dig, pytest.warns(RuntimeWarning, match="software filter"): raw = read_raw_brainvision(vhdr_old_path, preload=True) assert_array_equal(raw._data.shape, (29, 251)) @@ -611,7 +613,9 @@ def test_brainvision_vectorized_data(): def test_coodinates_extraction(): """Test reading of [Coordinates] section if present.""" # vhdr 2 has a Coordinates section - with pytest.warns(RuntimeWarning, match="coordinate information"): + with _record_warnings(), pytest.warns( + RuntimeWarning, match="coordinate information" + ): raw = read_raw_brainvision(vhdr_v2_path) # Basic check of extracted coordinates diff --git a/mne/io/cnt/tests/test_cnt.py b/mne/io/cnt/tests/test_cnt.py index 526cc893e69..50724d1818a 100644 --- a/mne/io/cnt/tests/test_cnt.py +++ b/mne/io/cnt/tests/test_cnt.py @@ -19,10 +19,13 @@ fname_bad_spans = data_path / "CNT" / "test_CNT_events_mne_JWoess_clipped.cnt" +_no_parse = pytest.warns(RuntimeWarning, match="Could not parse") + + @testing.requires_testing_data def test_old_data(): """Test reading raw cnt files.""" - with pytest.warns(RuntimeWarning, match="number of bytes"): + with _no_parse, pytest.warns(RuntimeWarning, match="number of bytes"): raw = _test_raw_reader( read_raw_cnt, input_fname=fname, eog="auto", misc=["NA1", "LEFT_EAR"] ) @@ -50,12 +53,12 @@ def test_new_data(): @testing.requires_testing_data def test_auto_data(): """Test reading raw cnt files with automatic header.""" - with pytest.warns(RuntimeWarning): + with pytest.warns(RuntimeWarning, match="Omitted 6 annot"): raw = read_raw_cnt(input_fname=fname_bad_spans) assert raw.info["bads"] == ["F8"] - with pytest.warns(RuntimeWarning, match="number of bytes"): + with _no_parse, pytest.warns(RuntimeWarning, match="number of bytes"): raw = _test_raw_reader( read_raw_cnt, input_fname=fname, eog="auto", misc=["NA1", "LEFT_EAR"] ) @@ -74,7 +77,7 @@ def test_auto_data(): @testing.requires_testing_data def test_compare_events_and_annotations(): """Test comparing annotations and events.""" - with pytest.warns(RuntimeWarning, match="Could not parse meas date"): + with _no_parse, pytest.warns(RuntimeWarning, match="Could not define the num"): raw = read_raw_cnt(fname) events = np.array( [[333, 0, 7], [1010, 0, 7], [1664, 0, 109], [2324, 0, 7], [2984, 0, 109]] diff --git a/mne/io/ctf/tests/test_ctf.py b/mne/io/ctf/tests/test_ctf.py index 20fdf2e0127..bf4415d90b8 100644 --- a/mne/io/ctf/tests/test_ctf.py +++ b/mne/io/ctf/tests/test_ctf.py @@ -113,7 +113,7 @@ def test_read_ctf(tmp_path): shutil.copytree(ctf_eeg_fname, ctf_no_hc_fname) remove_base = op.join(ctf_no_hc_fname, op.basename(ctf_fname_catch[:-3])) os.remove(remove_base + ".hc") - with pytest.warns(RuntimeWarning, match="MISC channel"): + with _record_warnings(), pytest.warns(RuntimeWarning, match="MISC channel"): pytest.raises(RuntimeError, read_raw_ctf, ctf_no_hc_fname) os.remove(remove_base + ".eeg") shutil.copy( diff --git a/mne/io/edf/tests/test_edf.py b/mne/io/edf/tests/test_edf.py index 38532e062c1..bc6250e28a6 100644 --- a/mne/io/edf/tests/test_edf.py +++ b/mne/io/edf/tests/test_edf.py @@ -38,6 +38,7 @@ ) from mne.io.tests.test_raw import _test_raw_reader from mne.tests.test_annotations import _assert_annotations_equal +from mne.utils import _record_warnings td_mark = testing._pytest_mark() @@ -408,7 +409,7 @@ def test_no_data_channels(): annot_2 = raw.annotations _assert_annotations_equal(annot, annot_2) # only annotations (should warn) - with pytest.warns(RuntimeWarning, match="read_annotations"): + with _record_warnings(), pytest.warns(RuntimeWarning, match="read_annotations"): read_raw_edf(edf_annot_only) diff --git a/mne/io/edf/tests/test_gdf.py b/mne/io/edf/tests/test_gdf.py index 70801a22cce..9ae33ee2feb 100644 --- a/mne/io/edf/tests/test_gdf.py +++ b/mne/io/edf/tests/test_gdf.py @@ -153,7 +153,7 @@ def test_gdf2_data(): @testing.requires_testing_data def test_one_channel_gdf(): """Test a one-channel GDF file.""" - with pytest.warns(RuntimeWarning, match="different highpass"): + with pytest.warns(RuntimeWarning, match="contain different"): ecg = read_raw_gdf(gdf_1ch_path, preload=True) assert ecg["ECG"][0].shape == (1, 4500) assert 150.0 == ecg.info["sfreq"] diff --git a/mne/io/eeglab/tests/test_eeglab.py b/mne/io/eeglab/tests/test_eeglab.py index 35f9fea741b..88c18d2aab0 100644 --- a/mne/io/eeglab/tests/test_eeglab.py +++ b/mne/io/eeglab/tests/test_eeglab.py @@ -28,7 +28,7 @@ from mne.io.eeglab._eeglab import _readmat from mne.io.eeglab.eeglab import _dol_to_lod, _get_montage_information from mne.io.tests.test_raw import _test_raw_reader -from mne.utils import Bunch, _check_pymatreader_installed +from mne.utils import Bunch, _check_pymatreader_installed, _record_warnings base_dir = testing.data_path(download=False) / "EEGLAB" raw_fname_mat = base_dir / "test_raw.set" @@ -140,7 +140,9 @@ def test_io_set_raw_more(tmp_path): shutil.copyfile( base_dir / "test_raw.fdt", negative_latency_fname.with_suffix(".fdt") ) - with pytest.warns(RuntimeWarning, match="has a sample index of -1."): + with _record_warnings(), pytest.warns( + RuntimeWarning, match="has a sample index of -1." + ): read_raw_eeglab(input_fname=negative_latency_fname, preload=True) # test negative event latencies @@ -163,7 +165,7 @@ def test_io_set_raw_more(tmp_path): oned_as="row", ) with pytest.raises(ValueError, match="event sample index is negative"): - with pytest.warns(RuntimeWarning, match="has a sample index of -1."): + with _record_warnings(): read_raw_eeglab(input_fname=negative_latency_fname, preload=True) # test overlapping events @@ -350,9 +352,9 @@ def test_io_set_raw_more(tmp_path): def test_io_set_epochs(fnames): """Test importing EEGLAB .set epochs files.""" epochs_fname, epochs_fname_onefile = fnames - with pytest.warns(RuntimeWarning, match="multiple events"): + with _record_warnings(), pytest.warns(RuntimeWarning, match="multiple events"): epochs = read_epochs_eeglab(epochs_fname) - with pytest.warns(RuntimeWarning, match="multiple events"): + with _record_warnings(), pytest.warns(RuntimeWarning, match="multiple events"): epochs2 = read_epochs_eeglab(epochs_fname_onefile) # one warning for each read_epochs_eeglab because both files have epochs # associated with multiple events diff --git a/mne/io/eyelink/tests/test_eyelink.py b/mne/io/eyelink/tests/test_eyelink.py index 653be12564b..953fde5b67d 100644 --- a/mne/io/eyelink/tests/test_eyelink.py +++ b/mne/io/eyelink/tests/test_eyelink.py @@ -12,6 +12,7 @@ from mne.io import read_raw_eyelink from mne.io.eyelink._utils import _adjust_times, _find_overlaps from mne.io.tests.test_raw import _test_raw_reader +from mne.utils import _record_warnings pd = pytest.importorskip("pandas") @@ -255,7 +256,9 @@ def test_multi_block_misc_channels(fname, tmp_path): out_file = tmp_path / "tmp_eyelink.asc" _simulate_eye_tracking_data(fname, out_file) - with pytest.warns(RuntimeWarning, match="Raw eyegaze coordinates"): + with _record_warnings(), pytest.warns( + RuntimeWarning, match="Raw eyegaze coordinates" + ): raw = read_raw_eyelink(out_file, apply_offsets=True) chs_in_file = [ diff --git a/mne/io/fieldtrip/tests/test_fieldtrip.py b/mne/io/fieldtrip/tests/test_fieldtrip.py index f7faac045ee..11546e82607 100644 --- a/mne/io/fieldtrip/tests/test_fieldtrip.py +++ b/mne/io/fieldtrip/tests/test_fieldtrip.py @@ -68,16 +68,14 @@ def test_read_evoked(cur_system, version, use_info): """Test comparing reading an Evoked object and the FieldTrip version.""" test_data_folder_ft = get_data_paths(cur_system) mne_avg = get_evoked(cur_system) + cur_fname = test_data_folder_ft / f"averaged_{version}.mat" if use_info: info = get_raw_info(cur_system) - ctx = nullcontext() + avg_ft = mne.io.read_evoked_fieldtrip(cur_fname, info) else: info = None - ctx = pytest.warns(**no_info_warning) - - cur_fname = test_data_folder_ft / f"averaged_{version}.mat" - with ctx: - avg_ft = mne.io.read_evoked_fieldtrip(cur_fname, info) + with _record_warnings(), pytest.warns(**no_info_warning): + avg_ft = mne.io.read_evoked_fieldtrip(cur_fname, info) mne_data = mne_avg.data[:, :-1] ft_data = avg_ft.data @@ -98,6 +96,7 @@ def test_read_epochs(cur_system, version, use_info, monkeypatch): has_pandas = pandas is not False test_data_folder_ft = get_data_paths(cur_system) mne_epoched = get_epochs(cur_system) + cur_fname = test_data_folder_ft / f"epoched_{version}.mat" if use_info: info = get_raw_info(cur_system) ctx = nullcontext() @@ -105,9 +104,8 @@ def test_read_epochs(cur_system, version, use_info, monkeypatch): info = None ctx = pytest.warns(**no_info_warning) - cur_fname = test_data_folder_ft / f"epoched_{version}.mat" if has_pandas: - with ctx: + with _record_warnings(), ctx: epoched_ft = mne.io.read_epochs_fieldtrip(cur_fname, info) assert isinstance(epoched_ft.metadata, pandas.DataFrame) else: @@ -133,7 +131,7 @@ def modify_mat(fname, variable_names=None, ignore_fields=None): return out monkeypatch.setattr(pymatreader, "read_mat", modify_mat) - with pytest.warns(RuntimeWarning, match="multiple"): + with _record_warnings(), pytest.warns(RuntimeWarning, match="multiple"): mne.io.read_epochs_fieldtrip(cur_fname, info) @@ -160,7 +158,7 @@ def test_read_raw_fieldtrip(cur_system, version, use_info): cur_fname = test_data_folder_ft / f"raw_{version}.mat" - with ctx: + with _record_warnings(), ctx: raw_fiff_ft = mne.io.read_raw_fieldtrip(cur_fname, info) if cur_system == "BTI" and not use_info: diff --git a/mne/io/fiff/tests/test_raw_fiff.py b/mne/io/fiff/tests/test_raw_fiff.py index f2269b0cb51..cb1626a4ef7 100644 --- a/mne/io/fiff/tests/test_raw_fiff.py +++ b/mne/io/fiff/tests/test_raw_fiff.py @@ -7,7 +7,6 @@ import os import pathlib import pickle -import platform import shutil import sys from copy import deepcopy @@ -43,7 +42,6 @@ assert_and_remove_boundary_annot, assert_object_equal, catch_logging, - check_version, requires_mne, run_subprocess, ) @@ -903,7 +901,7 @@ def test_io_complex(tmp_path, dtype): @testing.requires_testing_data def test_getitem(): """Test getitem/indexing of Raw.""" - for preload in [False, True, "memmap.dat"]: + for preload in [False, True, "memmap1.dat"]: raw = read_raw_fif(fif_fname, preload=preload) data, times = raw[0, :] data1, times1 = raw[0] @@ -1022,11 +1020,9 @@ def test_proj(tmp_path): @testing.requires_testing_data -@pytest.mark.parametrize("preload", [False, True, "memmap.dat"]) +@pytest.mark.parametrize("preload", [False, True, "memmap2.dat"]) def test_preload_modify(preload, tmp_path): """Test preloading and modifying data.""" - if platform.system() == "Windows" and check_version("numpy", "2.0.0dev"): - pytest.skip("Problem on Windows, see numpy/issues/25665") rng = np.random.RandomState(0) raw = read_raw_fif(fif_fname, preload=preload) @@ -1930,9 +1926,7 @@ def test_equalize_channels(): def test_memmap(tmp_path): """Test some interesting memmapping cases.""" # concatenate_raw - if platform.system() == "Windows" and check_version("numpy", "2.0.0dev"): - pytest.skip("Problem on Windows, see numpy/issues/25665") - memmaps = [str(tmp_path / str(ii)) for ii in range(3)] + memmaps = [str(tmp_path / str(ii)) for ii in range(4)] raw_0 = read_raw_fif(test_fif_fname, preload=memmaps[0]) assert raw_0._data.filename == memmaps[0] raw_1 = read_raw_fif(test_fif_fname, preload=memmaps[1]) @@ -1957,8 +1951,8 @@ def test_memmap(tmp_path): # now let's see if .copy() actually works; it does, but eventually # we should make it optionally memmap to a new filename rather than # create an in-memory version (filename=None) - raw_0 = read_raw_fif(test_fif_fname, preload=memmaps[0]) - assert raw_0._data.filename == memmaps[0] + raw_0 = read_raw_fif(test_fif_fname, preload=memmaps[3]) + assert raw_0._data.filename == memmaps[3] assert raw_0._data[:1, 3:5].all() raw_1 = raw_0.copy() assert isinstance(raw_1._data, np.memmap) @@ -2098,7 +2092,9 @@ def test_corrupted(tmp_path, offset): bad_fname = tmp_path / "test_raw.fif" with open(bad_fname, "wb") as fid: fid.write(data) - with pytest.warns(RuntimeWarning, match=".*tag directory.*corrupt.*"): + with _record_warnings(), pytest.warns( + RuntimeWarning, match=".*tag directory.*corrupt.*" + ): raw_bad = read_raw_fif(bad_fname) assert_allclose(raw.get_data(), raw_bad.get_data()) diff --git a/mne/preprocessing/tests/test_ica.py b/mne/preprocessing/tests/test_ica.py index 184f7aeefdf..67aabf14b12 100644 --- a/mne/preprocessing/tests/test_ica.py +++ b/mne/preprocessing/tests/test_ica.py @@ -78,6 +78,8 @@ ) pytest.importorskip("sklearn") +_baseline_corrected = pytest.warns(RuntimeWarning, match="were baseline-corrected") + def ICA(*args, **kwargs): """Fix the random state in tests.""" @@ -171,7 +173,9 @@ def test_ica_simple(method): info = create_info(data.shape[-2], 1000.0, "eeg") cov = make_ad_hoc_cov(info) ica = ICA(n_components=n_components, method=method, random_state=0, noise_cov=cov) - with pytest.warns(RuntimeWarning, match="No average EEG.*"): + with pytest.warns(RuntimeWarning, match="high-pass filtered"), pytest.warns( + RuntimeWarning, match="No average EEG.*" + ): ica.fit(RawArray(data, info)) transform = ica.unmixing_matrix_ @ ica.pca_components_ @ A amari_distance = np.mean( @@ -649,7 +653,7 @@ def test_ica_additional(method, tmp_path, short_raw_epochs): # test if n_components=None works ica = ICA(n_components=None, method=method, max_iter=1) - with pytest.warns(UserWarning, match="did not converge"): + with _baseline_corrected, pytest.warns(UserWarning, match="did not converge"): ica.fit(epochs) _assert_ica_attributes(ica, epochs.get_data("data"), limits=(0.05, 20)) @@ -1032,7 +1036,7 @@ def test_get_explained_variance_ratio(tmp_path, short_raw_epochs): with pytest.raises(ValueError, match="ICA must be fitted first"): ica.get_explained_variance_ratio(epochs) - with pytest.warns(RuntimeWarning, match="were baseline-corrected"): + with _record_warnings(), _baseline_corrected: ica.fit(epochs) # components = int, ch_type = None @@ -1255,7 +1259,9 @@ def test_fit_params_epochs_vs_raw(param_name, param_val, tmp_path): ica = ICA(n_components=n_components, max_iter=max_iter, method=method) fit_params = {param_name: param_val} - with pytest.warns(RuntimeWarning, match="parameters.*will be ignored"): + with _record_warnings(), pytest.warns( + RuntimeWarning, match="parameters.*will be ignored" + ): ica.fit(inst=epochs, **fit_params) assert ica.reject_ == reject _assert_ica_attributes(ica) @@ -1448,7 +1454,7 @@ def test_ica_labels(): assert key in raw.ch_names raw.set_channel_types(rename) ica = ICA(n_components=4, max_iter=2, method="fastica", allow_ref_meg=True) - with pytest.warns(UserWarning, match="did not converge"): + with _record_warnings(), pytest.warns(UserWarning, match="did not converge"): ica.fit(raw) _assert_ica_attributes(ica) @@ -1473,7 +1479,7 @@ def test_ica_labels(): # derive reference ICA components and append them to raw ica_rf = ICA(n_components=2, max_iter=2, allow_ref_meg=True) - with pytest.warns(UserWarning, match="did not converge"): + with _record_warnings(), pytest.warns(UserWarning, match="did not converge"): ica_rf.fit(raw.copy().pick("ref_meg")) icacomps = ica_rf.get_sources(raw) # rename components so they are auto-detected by find_bads_ref @@ -1509,7 +1515,7 @@ def test_ica_labels(): assert_allclose(scores, [0.81, 0.14, 0.37, 0.05], atol=0.03) ica = ICA(n_components=4, max_iter=2, method="fastica", allow_ref_meg=True) - with pytest.warns(UserWarning, match="did not converge"): + with _record_warnings(), pytest.warns(UserWarning, match="did not converge"): ica.fit(raw, picks="eeg") ica.find_bads_muscle(raw) assert "muscle" in ica.labels_ diff --git a/mne/preprocessing/tests/test_ssp.py b/mne/preprocessing/tests/test_ssp.py index b7565a6c5ce..36bfa3505c1 100644 --- a/mne/preprocessing/tests/test_ssp.py +++ b/mne/preprocessing/tests/test_ssp.py @@ -11,6 +11,7 @@ from mne.datasets import testing from mne.io import read_raw_ctf, read_raw_fif from mne.preprocessing.ssp import compute_proj_ecg, compute_proj_eog +from mne.utils import _record_warnings data_path = Path(__file__).parents[2] / "io" / "tests" / "data" raw_fname = data_path / "test_raw.fif" @@ -72,7 +73,7 @@ def test_compute_proj_ecg(short_raw, average): # (first with a call that makes sure we copy the mutable default "reject") with pytest.warns(RuntimeWarning, match="longer than the signal"): compute_proj_ecg(raw.copy().pick("mag"), l_freq=None, h_freq=None) - with pytest.warns(RuntimeWarning, match="No good epochs found"): + with _record_warnings(), pytest.warns(RuntimeWarning, match="No good epochs found"): projs, events, drop_log = compute_proj_ecg( raw, n_mag=2, @@ -133,7 +134,7 @@ def test_compute_proj_eog(average, short_raw): assert proj["explained_var"] > thresh_eeg # XXX: better tests - with pytest.warns(RuntimeWarning, match="longer"): + with _record_warnings(), pytest.warns(RuntimeWarning, match="longer"): projs, events = compute_proj_eog( raw, n_mag=2, @@ -150,7 +151,9 @@ def test_compute_proj_eog(average, short_raw): assert projs == [] raw._data[raw.ch_names.index("EOG 061"), :] = 1.0 - with pytest.warns(RuntimeWarning, match="filter.*longer than the signal"): + with _record_warnings(), pytest.warns( + RuntimeWarning, match="filter.*longer than the signal" + ): projs, events = compute_proj_eog(raw=raw, tmax=dur_use, ch_name="EOG 061") @@ -175,7 +178,7 @@ def test_compute_proj_parallel(short_raw): filter_length=100, ) raw_2 = short_raw.copy() - with pytest.warns(RuntimeWarning, match="Attenuation"): + with _record_warnings(), pytest.warns(RuntimeWarning, match="Attenuation"): projs_2, _ = compute_proj_eog( raw_2, n_eeg=2, diff --git a/mne/report/tests/test_report.py b/mne/report/tests/test_report.py index 0e74201c1cb..5f549093581 100644 --- a/mne/report/tests/test_report.py +++ b/mne/report/tests/test_report.py @@ -38,7 +38,7 @@ CONTENT_ORDER, _webp_supported, ) -from mne.utils import Bunch +from mne.utils import Bunch, _record_warnings from mne.utils._testing import assert_object_equal from mne.viz import plot_alignment @@ -1000,7 +1000,7 @@ def test_manual_report_2d(tmp_path, invisible_fig): for ch in evoked_no_ch_locs.info["chs"]: ch["loc"][:3] = np.nan - with pytest.warns( + with _record_warnings(), pytest.warns( RuntimeWarning, match="No EEG channel locations found, cannot create joint plot" ): r.add_evokeds( @@ -1029,7 +1029,9 @@ def test_manual_report_2d(tmp_path, invisible_fig): for ch in ica_no_ch_locs.info["chs"]: ch["loc"][:3] = np.nan - with pytest.warns(RuntimeWarning, match="No Magnetometers channel locations"): + with _record_warnings(), pytest.warns( + RuntimeWarning, match="No Magnetometers channel locations" + ): r.add_ica( ica=ica_no_ch_locs, picks=[0], inst=raw.copy().load_data(), title="ICA" ) @@ -1053,7 +1055,9 @@ def test_manual_report_3d(tmp_path, renderer): add_kwargs = dict( trans=trans_fname, info=info, subject="sample", subjects_dir=subjects_dir ) - with pytest.warns(RuntimeWarning, match="could not be calculated"): + with _record_warnings(), pytest.warns( + RuntimeWarning, match="could not be calculated" + ): r.add_trans(title="coreg no dig", **add_kwargs) with info._unlock(): info["dig"] = dig diff --git a/mne/source_space/tests/test_source_space.py b/mne/source_space/tests/test_source_space.py index 2389e59cb5b..4a1e20eef9b 100644 --- a/mne/source_space/tests/test_source_space.py +++ b/mne/source_space/tests/test_source_space.py @@ -699,7 +699,9 @@ def test_source_space_exclusive_complete(src_volume_labels): for si, s in enumerate(src): assert_allclose(src_full[0]["rr"], s["rr"], atol=1e-6) # also check single_volume=True -- should be the same result - with pytest.warns(RuntimeWarning, match="Found no usable.*Left-vessel.*"): + with _record_warnings(), pytest.warns( + RuntimeWarning, match="Found no usable.*Left-vessel.*" + ): src_single = setup_volume_source_space( src[0]["subject_his_id"], 7.0, diff --git a/mne/stats/tests/test_cluster_level.py b/mne/stats/tests/test_cluster_level.py index c1c4ba40851..59bb0611aeb 100644 --- a/mne/stats/tests/test_cluster_level.py +++ b/mne/stats/tests/test_cluster_level.py @@ -96,7 +96,9 @@ def test_thresholds(numba_conditional): # nan handling in TFCE X = np.repeat(X[0], 2, axis=1) X[:, 1] = 0 - with pytest.warns(RuntimeWarning, match="invalid value"): # NumPy + with _record_warnings(), pytest.warns( + RuntimeWarning, match="invalid value" + ): # NumPy out = permutation_cluster_1samp_test( X, seed=0, threshold=dict(start=0, step=0.1), out_type="mask" ) @@ -140,7 +142,7 @@ def test_cache_dir(tmp_path, numba_conditional): # ensure that non-independence yields warning stat_fun = partial(ttest_1samp_no_p, sigma=1e-3) random_state = np.random.default_rng(0) - with pytest.warns(RuntimeWarning, match="independently"): + with _record_warnings(), pytest.warns(RuntimeWarning, match="independently"): permutation_cluster_1samp_test( X, buffer_size=10, @@ -509,7 +511,7 @@ def test_cluster_permutation_with_adjacency(numba_conditional, monkeypatch): assert np.min(out_adjacency_6[2]) < 0.05 with pytest.raises(ValueError, match="not compatible"): - with pytest.warns(RuntimeWarning, match="No clusters"): + with _record_warnings(): spatio_temporal_func( X1d_3, n_permutations=50, diff --git a/mne/tests/test_bem.py b/mne/tests/test_bem.py index d2ee3fe9f93..261fd9efe55 100644 --- a/mne/tests/test_bem.py +++ b/mne/tests/test_bem.py @@ -44,7 +44,7 @@ from mne.io import read_info from mne.surface import _get_ico_surface, read_surface from mne.transforms import translation -from mne.utils import catch_logging, check_version +from mne.utils import _record_warnings, catch_logging, check_version fname_raw = Path(__file__).parents[1] / "io" / "tests" / "data" / "test_raw.fif" subjects_dir = testing.data_path(download=False) / "subjects" @@ -54,6 +54,8 @@ fname_bem_sol_1 = subjects_dir / "sample" / "bem" / "sample-320-bem-sol.fif" fname_dense_head = subjects_dir / "sample" / "bem" / "sample-head-dense.fif" +_few_points = pytest.warns(RuntimeWarning, match="Only .* head digitization") + def _compare_bem_surfaces(surfs_1, surfs_2): """Compare BEM surfaces.""" @@ -414,7 +416,7 @@ def test_fit_sphere_to_headshape(): # # Test with 4 points that match a perfect sphere dig_kinds = (FIFF.FIFFV_POINT_CARDINAL, FIFF.FIFFV_POINT_EXTRA) - with pytest.warns(RuntimeWarning, match="Only .* head digitization"): + with _few_points: r, oh, od = fit_sphere_to_headshape(info, dig_kinds=dig_kinds, units="m") kwargs = dict(rtol=1e-3, atol=1e-5) assert_allclose(r, rad, **kwargs) @@ -424,7 +426,7 @@ def test_fit_sphere_to_headshape(): # Test with all points dig_kinds = ("cardinal", FIFF.FIFFV_POINT_EXTRA, "eeg") kwargs = dict(rtol=1e-3, atol=1e-3) - with pytest.warns(RuntimeWarning, match="Only .* head digitization"): + with _few_points: r, oh, od = fit_sphere_to_headshape(info, dig_kinds=dig_kinds, units="m") assert_allclose(r, rad, **kwargs) assert_allclose(oh, center, **kwargs) @@ -432,7 +434,7 @@ def test_fit_sphere_to_headshape(): # Test with some noisy EEG points only. dig_kinds = "eeg" - with pytest.warns(RuntimeWarning, match="Only .* head digitization"): + with _few_points: r, oh, od = fit_sphere_to_headshape(info, dig_kinds=dig_kinds, units="m") kwargs = dict(rtol=1e-3, atol=1e-2) assert_allclose(r, rad, **kwargs) @@ -446,7 +448,7 @@ def test_fit_sphere_to_headshape(): d["r"] -= center d["r"] *= big_rad / rad d["r"] += center - with pytest.warns(RuntimeWarning, match="Estimated head radius"): + with _few_points, pytest.warns(RuntimeWarning, match="Estimated head radius"): r, oh, od = fit_sphere_to_headshape(info_big, dig_kinds=dig_kinds, units="mm") assert_allclose(oh, center * 1000, atol=1e-3) assert_allclose(r, big_rad * 1000, atol=1e-3) @@ -459,27 +461,31 @@ def test_fit_sphere_to_headshape(): for d in info_shift["dig"]: d["r"] -= center d["r"] += shift_center - with pytest.warns(RuntimeWarning, match="from head frame origin"): + with _record_warnings(), pytest.warns( + RuntimeWarning, match="from head frame origin" + ): r, oh, od = fit_sphere_to_headshape(info_shift, dig_kinds=dig_kinds, units="m") assert_allclose(oh, shift_center, atol=1e-6) assert_allclose(r, rad, atol=1e-6) # Test "auto" mode (default) # Should try "extra", fail, and go on to EEG - with pytest.warns(RuntimeWarning, match="Only .* head digitization"): + with _few_points: r, oh, od = fit_sphere_to_headshape(info, units="m") kwargs = dict(rtol=1e-3, atol=1e-3) assert_allclose(r, rad, **kwargs) assert_allclose(oh, center, **kwargs) assert_allclose(od, dev_center, **kwargs) - with pytest.warns(RuntimeWarning, match="Only .* head digitization"): + with _few_points: r2, oh2, od2 = fit_sphere_to_headshape(info, units="m") assert_allclose(r, r2, atol=1e-7) assert_allclose(oh, oh2, atol=1e-7) assert_allclose(od, od2, atol=1e-7) # this one should pass, 1 EXTRA point and 3 EEG (but the fit is terrible) info = Info(dig=dig[:7], dev_head_t=dev_head_t) - with pytest.warns(RuntimeWarning, match="Only .* head digitization"): + with _record_warnings(), pytest.warns( + RuntimeWarning, match="Estimated head radius" + ): r, oh, od = fit_sphere_to_headshape(info, units="m") # this one should fail, 1 EXTRA point and 3 EEG (but the fit is terrible) info = Info(dig=dig[:6], dev_head_t=dev_head_t) @@ -499,12 +505,12 @@ def test_io_head_bem(tmp_path): with pytest.raises(ValueError, match="topological defects:"): write_head_bem(fname_defect, head["rr"], head["tris"]) - with pytest.warns(RuntimeWarning, match="topological defects:"): + with _record_warnings(), pytest.warns(RuntimeWarning, match="topological defects:"): write_head_bem(fname_defect, head["rr"], head["tris"], on_defects="warn") # test on_defects in read_bem_surfaces with pytest.raises(ValueError, match="topological defects:"): read_bem_surfaces(fname_defect) - with pytest.warns(RuntimeWarning, match="topological defects:"): + with _record_warnings(), pytest.warns(RuntimeWarning, match="topological defects:"): head_defect = read_bem_surfaces(fname_defect, on_defects="warn")[0] assert head["id"] == head_defect["id"] == FIFF.FIFFV_BEM_SURF_ID_HEAD @@ -550,12 +556,14 @@ def _decimate_surface(points, triangles, n_triangles): # These are ignorable monkeypatch.setattr(mne.bem, "_tri_levels", dict(sparse=315)) - with pytest.warns(RuntimeWarning, match=".*have fewer than three.*"): + with _record_warnings(), pytest.warns( + RuntimeWarning, match=".*have fewer than three.*" + ): make_scalp_surfaces(subject, subjects_dir, force=True, overwrite=True) (surf,) = read_bem_surfaces(sparse_path, on_defects="ignore") assert len(surf["tris"]) == 315 monkeypatch.setattr(mne.bem, "_tri_levels", dict(sparse=319)) - with pytest.warns(RuntimeWarning, match=".*is not complete.*"): + with _record_warnings(), pytest.warns(RuntimeWarning, match=".*is not complete.*"): make_scalp_surfaces(subject, subjects_dir, force=True, overwrite=True) (surf,) = read_bem_surfaces(sparse_path, on_defects="ignore") assert len(surf["tris"]) == 319 diff --git a/mne/tests/test_chpi.py b/mne/tests/test_chpi.py index 35b4dd00794..5801e374b3b 100644 --- a/mne/tests/test_chpi.py +++ b/mne/tests/test_chpi.py @@ -43,7 +43,13 @@ ) from mne.simulation import add_chpi from mne.transforms import _angle_between_quats, rot_to_quat -from mne.utils import assert_meg_snr, catch_logging, object_diff, verbose +from mne.utils import ( + _record_warnings, + assert_meg_snr, + catch_logging, + object_diff, + verbose, +) from mne.viz import plot_head_positions base_dir = Path(__file__).parents[1] / "io" / "tests" / "data" @@ -366,7 +372,7 @@ def test_calculate_chpi_positions_vv(): ] ) raw_bad.pick([raw_bad.ch_names[pick] for pick in picks]) - with pytest.warns(RuntimeWarning, match="Discrepancy"): + with _record_warnings(), pytest.warns(RuntimeWarning, match="Discrepancy"): with catch_logging() as log_file: _calculate_chpi_positions(raw_bad, t_step_min=1.0, verbose=True) # ignore HPI info header and [done] footer diff --git a/mne/tests/test_cov.py b/mne/tests/test_cov.py index 2b7570d127c..cd817dcfceb 100644 --- a/mne/tests/test_cov.py +++ b/mne/tests/test_cov.py @@ -352,7 +352,7 @@ def test_cov_estimation_on_raw(method, tmp_path): assert_snr(cov.data, cov_mne.data[:5, :5], 90) # cutoff samps # make sure we get a warning with too short a segment raw_2 = read_raw_fif(raw_fname).crop(0, 1) - with pytest.warns(RuntimeWarning, match="Too few samples"): + with _record_warnings(), pytest.warns(RuntimeWarning, match="Too few samples"): cov = compute_raw_covariance(raw_2, method=method, method_params=method_params) # no epochs found due to rejection pytest.raises( @@ -384,7 +384,7 @@ def test_cov_estimation_on_raw_reg(): raw.info["sfreq"] /= 10.0 raw = RawArray(raw._data[:, ::10].copy(), raw.info) # decimate for speed cov_mne = read_cov(erm_cov_fname) - with pytest.warns(RuntimeWarning, match="Too few samples"): + with _record_warnings(), pytest.warns(RuntimeWarning, match="Too few samples"): # "diagonal_fixed" is much faster. Use long epochs for speed. cov = compute_raw_covariance(raw, tstep=5.0, method="diagonal_fixed") assert_snr(cov.data, cov_mne.data, 5) @@ -891,13 +891,13 @@ def test_cov_ctf(): for comp in [0, 1]: raw.apply_gradient_compensation(comp) epochs = Epochs(raw, events, None, -0.2, 0.2, preload=True) - with pytest.warns(RuntimeWarning, match="Too few samples"): + with _record_warnings(), pytest.warns(RuntimeWarning, match="Too few samples"): noise_cov = compute_covariance(epochs, tmax=0.0, method=["empirical"]) prepare_noise_cov(noise_cov, raw.info, ch_names) raw.apply_gradient_compensation(0) epochs = Epochs(raw, events, None, -0.2, 0.2, preload=True) - with pytest.warns(RuntimeWarning, match="Too few samples"): + with _record_warnings(), pytest.warns(RuntimeWarning, match="Too few samples"): noise_cov = compute_covariance(epochs, tmax=0.0, method=["empirical"]) raw.apply_gradient_compensation(1) diff --git a/mne/tests/test_epochs.py b/mne/tests/test_epochs.py index 76172982da7..2b67dd9dbd6 100644 --- a/mne/tests/test_epochs.py +++ b/mne/tests/test_epochs.py @@ -64,6 +64,7 @@ from mne.preprocessing import maxwell_filter from mne.utils import ( _dt_to_stamp, + _record_warnings, assert_meg_snr, catch_logging, object_diff, @@ -2291,7 +2292,7 @@ def test_crop(tmp_path): reject=reject, flat=flat, ) - with pytest.warns(RuntimeWarning, match="tmax is set to"): + with _record_warnings(), pytest.warns(RuntimeWarning, match="tmax is set to"): epochs2.crop(-20, 200) # indices for slicing @@ -3610,7 +3611,7 @@ def test_concatenate_epochs(): # check concatenating epochs where one of the objects is empty epochs2 = epochs.copy()[:0] - with pytest.warns(RuntimeWarning, match="was empty"): + with _record_warnings(), pytest.warns(RuntimeWarning, match="was empty"): concatenate_epochs([epochs, epochs2]) # check concatenating epochs results are chronologically ordered @@ -4221,7 +4222,7 @@ def test_no_epochs(tmp_path): # and with no epochs remaining raw.info["bads"] = [] epochs = mne.Epochs(raw, events, reject=reject) - with pytest.warns(RuntimeWarning, match="no data"): + with _record_warnings(), pytest.warns(RuntimeWarning, match="no data"): epochs.save(tmp_path / "sample-epo.fif", overwrite=True) assert len(epochs) == 0 # all dropped diff --git a/mne/tests/test_evoked.py b/mne/tests/test_evoked.py index 31110596be6..b5f686c43c3 100644 --- a/mne/tests/test_evoked.py +++ b/mne/tests/test_evoked.py @@ -34,7 +34,7 @@ from mne._fiff.constants import FIFF from mne.evoked import Evoked, EvokedArray, _get_peak from mne.io import read_raw_fif -from mne.utils import grand_average +from mne.utils import _record_warnings, grand_average base_dir = Path(__file__).parents[1] / "io" / "tests" / "data" fname = base_dir / "test-ave.fif" @@ -817,7 +817,7 @@ def test_time_as_index_and_crop(): ) evoked.crop(evoked.tmin, evoked.tmax, include_tmax=False) n_times = len(evoked.times) - with pytest.warns(RuntimeWarning, match="tmax is set to"): + with _record_warnings(), pytest.warns(RuntimeWarning, match="tmax is set to"): evoked.crop(tmin, tmax, include_tmax=False) assert len(evoked.times) == n_times assert_allclose(evoked.times[[0, -1]], [tmin, tmax - delta], atol=atol) diff --git a/mne/tests/test_source_estimate.py b/mne/tests/test_source_estimate.py index ebe1a369e4d..dff220d9752 100644 --- a/mne/tests/test_source_estimate.py +++ b/mne/tests/test_source_estimate.py @@ -399,7 +399,7 @@ def test_stc_snr(): assert (stc.data < 0).any() with pytest.warns(RuntimeWarning, match="nAm"): stc.estimate_snr(evoked.info, fwd, cov) # dSPM - with pytest.warns(RuntimeWarning, match="free ori"): + with _record_warnings(), pytest.warns(RuntimeWarning, match="free ori"): abs(stc).estimate_snr(evoked.info, fwd, cov) stc = apply_inverse(evoked, inv, method="MNE") snr = stc.estimate_snr(evoked.info, fwd, cov) diff --git a/mne/time_frequency/tests/test_spectrum.py b/mne/time_frequency/tests/test_spectrum.py index 26c18529143..18fbf4da483 100644 --- a/mne/time_frequency/tests/test_spectrum.py +++ b/mne/time_frequency/tests/test_spectrum.py @@ -11,6 +11,7 @@ from mne import Annotations from mne.time_frequency import read_spectrum from mne.time_frequency.spectrum import EpochsSpectrumArray, SpectrumArray +from mne.utils import _record_warnings def test_compute_psd_errors(raw): @@ -273,7 +274,7 @@ def test_spectrum_kwarg_triaging(raw): regex = r"legacy plot_psd\(\) method.*unexpected keyword.*'axes'.*Try rewriting" _, axes = plt.subplots(1, 2) # `axes` is the new param name: technically only valid for Spectrum.plot() - with pytest.warns(RuntimeWarning, match=regex): + 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'"): diff --git a/mne/utils/tests/test_check.py b/mne/utils/tests/test_check.py index 48017b79ae2..4ec7450df99 100644 --- a/mne/utils/tests/test_check.py +++ b/mne/utils/tests/test_check.py @@ -27,6 +27,7 @@ _check_subject, _on_missing, _path_like, + _record_warnings, _safe_input, _suggest, _validate_type, @@ -368,7 +369,7 @@ def test_check_sphere_verbose(): info = mne.io.read_info(fname_raw) with info._unlock(): info["dig"] = info["dig"][:20] - with pytest.warns(RuntimeWarning, match="may be inaccurate"): + with _record_warnings(), pytest.warns(RuntimeWarning, match="may be inaccurate"): _check_sphere("auto", info) with mne.use_log_level("error"): _check_sphere("auto", info) diff --git a/mne/utils/tests/test_logging.py b/mne/utils/tests/test_logging.py index 02a5a7363d0..25668a1de37 100644 --- a/mne/utils/tests/test_logging.py +++ b/mne/utils/tests/test_logging.py @@ -63,7 +63,7 @@ def test_frame_info(capsys, monkeypatch): def test_how_to_deal_with_warnings(): """Test filter some messages out of warning records.""" - with pytest.warns(UserWarning, match="bb") as w: + with pytest.warns(Warning, match="(bb|aa) warning") as w: warnings.warn("aa warning", UserWarning) warnings.warn("bb warning", UserWarning) warnings.warn("bb warning", RuntimeWarning) diff --git a/mne/viz/tests/test_epochs.py b/mne/viz/tests/test_epochs.py index 6dcfdb57bdf..1eccf64bbc2 100644 --- a/mne/viz/tests/test_epochs.py +++ b/mne/viz/tests/test_epochs.py @@ -17,6 +17,7 @@ from mne import Epochs, EpochsArray, create_info from mne.datasets import testing from mne.event import make_fixed_length_events +from mne.utils import _record_warnings from mne.viz import plot_drop_log @@ -52,13 +53,13 @@ def test_plot_epochs_basic(epochs, epochs_full, noise_cov_io, capsys, browser_ba browser_backend._close_all() # add a channel to cov['bads'] noise_cov_io["bads"] = [epochs.ch_names[1]] - with pytest.warns(RuntimeWarning, match="projection"): + with _record_warnings(), pytest.warns(RuntimeWarning, match="projection"): epochs.plot(noise_cov=noise_cov_io) browser_backend._close_all() # have a data channel missing from the covariance noise_cov_io["names"] = noise_cov_io["names"][:306] noise_cov_io["data"] = noise_cov_io["data"][:306][:306] - with pytest.warns(RuntimeWarning, match="projection"): + with _record_warnings(), pytest.warns(RuntimeWarning, match="projection"): epochs.plot(noise_cov=noise_cov_io) browser_backend._close_all() # other options @@ -300,7 +301,9 @@ def test_plot_epochs_image(epochs): picks=[0, 1], order=lambda times, data: np.arange(len(data))[::-1] ) # test warning - with pytest.warns(RuntimeWarning, match="Only one channel in group"): + with _record_warnings(), pytest.warns( + RuntimeWarning, match="Only one channel in group" + ): epochs.plot_image(picks=[1], combine="mean") # group_by should be a dict with pytest.raises(TypeError, match="dict or None"): @@ -418,7 +421,7 @@ def test_plot_psd_epochs(epochs): err_str = "for channel %s" % epochs.ch_names[2] epochs.get_data(copy=False)[0, 2, :] = 0 for dB in [True, False]: - with pytest.warns(UserWarning, match=err_str): + with _record_warnings(), pytest.warns(UserWarning, match=err_str): epochs.compute_psd().plot(dB=dB) @@ -492,7 +495,7 @@ def test_plot_psd_epochs_ctf(raw_ctf): epochs = Epochs(raw_ctf, evts, preload=True) old_defaults = dict(picks="data", exclude="bads") # EEG060 is flat in this dataset - with pytest.warns(UserWarning, match="for channel EEG060"): + with _record_warnings(), pytest.warns(UserWarning, match="for channel EEG060"): spectrum = epochs.compute_psd() for dB in [True, False]: spectrum.plot(dB=dB) diff --git a/mne/viz/tests/test_evoked.py b/mne/viz/tests/test_evoked.py index 999260465fd..66609839df8 100644 --- a/mne/viz/tests/test_evoked.py +++ b/mne/viz/tests/test_evoked.py @@ -34,7 +34,7 @@ from mne.datasets import testing from mne.io import read_raw_fif from mne.stats.parametric import _parametric_ci -from mne.utils import catch_logging +from mne.utils import _record_warnings, catch_logging from mne.viz import plot_compare_evokeds, plot_evoked_white from mne.viz.utils import _fake_click, _get_cmap @@ -119,7 +119,7 @@ def test_plot_evoked_cov(): epochs = Epochs(raw, events, picks=default_picks) cov = compute_covariance(epochs) evoked_sss = epochs.average() - with pytest.warns(RuntimeWarning, match="relative scaling"): + with _record_warnings(), pytest.warns(RuntimeWarning, match="relative scaling"): evoked_sss.plot(noise_cov=cov, time_unit="s") plt.close("all") @@ -333,7 +333,7 @@ def test_plot_evoked_image(): mask=np.ones(evoked.data.shape).astype(bool), time_unit="s", ) - with pytest.warns(RuntimeWarning, match="not adding contour"): + with _record_warnings(), pytest.warns(RuntimeWarning, match="not adding contour"): evoked.plot_image(picks=[1, 2], mask=None, mask_style="both", time_unit="s") with pytest.raises(ValueError, match="must have the same shape"): evoked.plot_image(mask=evoked.data[1:, 1:] > 0, time_unit="s") diff --git a/mne/viz/tests/test_ica.py b/mne/viz/tests/test_ica.py index 35903a5f802..7972e4d36b6 100644 --- a/mne/viz/tests/test_ica.py +++ b/mne/viz/tests/test_ica.py @@ -157,7 +157,7 @@ def test_plot_ica_properties(): ) ica = ICA(noise_cov=read_cov(cov_fname), n_components=2, max_iter=1, random_state=0) - with pytest.warns(RuntimeWarning, match="projection"): + with _record_warnings(), pytest.warns(RuntimeWarning, match="projection"): ica.fit(raw) # test _create_properties_layout @@ -240,7 +240,7 @@ def test_plot_ica_properties(): # Test handling of zeros ica = ICA(random_state=0, max_iter=1) epochs.pick(pick_names) - with pytest.warns(UserWarning, match="did not converge"): + with _record_warnings(), pytest.warns(UserWarning, match="did not converge"): ica.fit(epochs) epochs._data[0] = 0 # Usually UserWarning: Infinite value .* for epo @@ -254,7 +254,7 @@ def test_plot_ica_properties(): raw_annot.pick(np.arange(10)) raw_annot.del_proj() - with pytest.warns(UserWarning, match="did not converge"): + with _record_warnings(), pytest.warns(UserWarning, match="did not converge"): ica.fit(raw_annot) # drop bad data segments fig = ica.plot_properties(raw_annot, picks=[0, 1], **topoargs) diff --git a/mne/viz/tests/test_misc.py b/mne/viz/tests/test_misc.py index c8ef70bbbc0..180f8bb414a 100644 --- a/mne/viz/tests/test_misc.py +++ b/mne/viz/tests/test_misc.py @@ -30,6 +30,7 @@ from mne.io import read_raw_fif from mne.minimum_norm import read_inverse_operator from mne.time_frequency import CrossSpectralDensity +from mne.utils import _record_warnings from mne.viz import ( plot_bem, plot_chpi_snr, @@ -214,7 +215,9 @@ def test_plot_events(): assert fig.axes[0].get_legend() is not None with pytest.warns(RuntimeWarning, match="Color was not assigned"): plot_events(events, raw.info["sfreq"], raw.first_samp, color=color) - with pytest.warns(RuntimeWarning, match=r"vent \d+ missing from event_id"): + with _record_warnings(), pytest.warns( + RuntimeWarning, match=r"vent \d+ missing from event_id" + ): plot_events( events, raw.info["sfreq"], @@ -223,7 +226,7 @@ def test_plot_events(): color=color, ) multimatch = r"event \d+ missing from event_id|in the color dict but is" - with pytest.warns(RuntimeWarning, match=multimatch): + with _record_warnings(), pytest.warns(RuntimeWarning, match=multimatch): plot_events( events, raw.info["sfreq"], @@ -243,7 +246,9 @@ def test_plot_events(): on_missing="ignore", ) extra_id = {"aud_l": 1, "missing": 111} - with pytest.warns(RuntimeWarning, match="from event_id is not present in"): + with _record_warnings(), pytest.warns( + RuntimeWarning, match="from event_id is not present in" + ): plot_events( events, raw.info["sfreq"], @@ -251,7 +256,7 @@ def test_plot_events(): event_id=extra_id, on_missing="warn", ) - with pytest.warns(RuntimeWarning, match="event 2 missing"): + with _record_warnings(), pytest.warns(RuntimeWarning, match="event 2 missing"): plot_events( events, raw.info["sfreq"], diff --git a/mne/viz/tests/test_raw.py b/mne/viz/tests/test_raw.py index 619c79a7111..14233a4c98b 100644 --- a/mne/viz/tests/test_raw.py +++ b/mne/viz/tests/test_raw.py @@ -973,7 +973,9 @@ def test_plot_raw_psd(raw, raw_orig): # with channel information not available for idx in range(len(raw.info["chs"])): raw.info["chs"][idx]["loc"] = np.zeros(12) - with pytest.warns(RuntimeWarning, match="locations not available"): + with _record_warnings(), pytest.warns( + RuntimeWarning, match="locations not available" + ): raw.compute_psd().plot(spatial_colors=True, average=False) # with a flat channel raw[5, :] = 0 diff --git a/mne/viz/tests/test_topo.py b/mne/viz/tests/test_topo.py index 12c345e6623..5830c647edb 100644 --- a/mne/viz/tests/test_topo.py +++ b/mne/viz/tests/test_topo.py @@ -325,7 +325,7 @@ def test_plot_tfr_topo(): # test opening tfr by clicking num_figures_before = len(plt.get_fignums()) # could use np.reshape(fig.axes[-1].images[0].get_extent(), (2, 2)).mean(1) - with pytest.warns(RuntimeWarning, match="not masking"): + with _record_warnings(), pytest.warns(RuntimeWarning, match="not masking"): _fake_click(fig, fig.axes[0], (0.08, 0.65)) assert num_figures_before + 1 == len(plt.get_fignums()) plt.close("all") @@ -349,7 +349,7 @@ def test_plot_tfr_topo(): vmin, vmax = 0.0, 2.0 fig, ax = plt.subplots() tmin, tmax = epochs.times[0], epochs.times[-1] - with pytest.warns(RuntimeWarning, match="not masking"): + with _record_warnings(), pytest.warns(RuntimeWarning, match="not masking"): _imshow_tfr( ax, 3, @@ -372,7 +372,7 @@ def test_plot_tfr_topo(): # ValueError when freq[0] == 0 and yscale == 'log' these_freqs = freqs[:3].copy() these_freqs[0] = 0 - with pytest.warns(RuntimeWarning, match="not masking"): + with _record_warnings(), pytest.warns(RuntimeWarning, match="not masking"): pytest.raises( ValueError, _imshow_tfr, diff --git a/pyproject.toml b/pyproject.toml index 2090e5b2667..58a5915cd2d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -110,7 +110,7 @@ full = [ # Dependencies for running the test infrastructure test = [ - "pytest!=8.0.0rc1,!=8.0.0rc2", + "pytest>=8.0.0rc2", "pytest-cov", "pytest-timeout", "pytest-harvest", @@ -242,7 +242,10 @@ ignore-decorators = [ ] [tool.pytest.ini_options] -addopts = """--durations=20 --doctest-modules -ra --cov-report= --tb=short \ +# -r f (failed), E (error), s (skipped), x (xfail), X (xpassed), w (warnings) +# don't put in xfail for pytest 8.0+ because then it prints the tracebacks, +# which look like real errors +addopts = """--durations=20 --doctest-modules -rfEXs --cov-report= --tb=short \ --cov-branch --doctest-ignore-import-errors --junit-xml=junit-results.xml \ --ignore=doc --ignore=logo --ignore=examples --ignore=tutorials \ --ignore=mne/gui/_*.py --ignore=mne/icons --ignore=tools \ From 8419ca04f7f85e5861128593fa92462d0ccbfb27 Mon Sep 17 00:00:00 2001 From: Mathieu Scheltienne Date: Mon, 29 Jan 2024 19:24:17 +0100 Subject: [PATCH 167/405] Fix sphinx roles and parenthesis (#12385) --- doc/changes/devel/12326.other.rst | 2 +- tutorials/preprocessing/15_handling_bad_channels.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/changes/devel/12326.other.rst b/doc/changes/devel/12326.other.rst index b8f2966bbf9..f0bd6a377d6 100644 --- a/doc/changes/devel/12326.other.rst +++ b/doc/changes/devel/12326.other.rst @@ -1 +1 @@ -Updated the text in the preprocessing tutorial to use :class:`mne.io.Raw.pick()` instead of the legacy :class:`mne.io.Raw.pick_types()`, by :newcontrib:`btkcodedev`. +Updated the text in the preprocessing tutorial to use :meth:`mne.io.Raw.pick` instead of the legacy :meth:`mne.io.Raw.pick_types`, by :newcontrib:`btkcodedev`. diff --git a/tutorials/preprocessing/15_handling_bad_channels.py b/tutorials/preprocessing/15_handling_bad_channels.py index 06e9ffd6e53..7ddc36af026 100644 --- a/tutorials/preprocessing/15_handling_bad_channels.py +++ b/tutorials/preprocessing/15_handling_bad_channels.py @@ -238,7 +238,7 @@ fig.suptitle(title, size="xx-large", weight="bold") # %% -# Note that the method :meth:`~mne.io.Raw.pick()` default +# Note that the method :meth:`~mne.io.Raw.pick` default # arguments includes ``exclude=()`` which ensures that bad # channels are not # automatically dropped from the selection. Here is the corresponding example From 324899ae4f60fb5d4911bfd679e53e8815a081d1 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Tue, 30 Jan 2024 15:14:18 -0500 Subject: [PATCH 168/405] BUG: Fix bug with report CSS (#12399) --- doc/changes/devel/12399.bugfix.rst | 1 + mne/html/d3.v3.min.js | 5 ----- mne/html/mpld3.v0.2.min.js | 2 -- mne/report/js_and_css/report.css | 19 +++++++++++++++++++ mne/report/js_and_css/report.sass | 19 ------------------- mne/report/report.py | 2 +- 6 files changed, 21 insertions(+), 27 deletions(-) create mode 100644 doc/changes/devel/12399.bugfix.rst delete mode 100644 mne/html/d3.v3.min.js delete mode 100644 mne/html/mpld3.v0.2.min.js create mode 100644 mne/report/js_and_css/report.css delete mode 100644 mne/report/js_and_css/report.sass diff --git a/doc/changes/devel/12399.bugfix.rst b/doc/changes/devel/12399.bugfix.rst new file mode 100644 index 00000000000..cf53e91b5c8 --- /dev/null +++ b/doc/changes/devel/12399.bugfix.rst @@ -0,0 +1 @@ +Fix bugs with :class:`mne.Report` CSS where TOC items could disappear at the bottom of the page, by `Eric Larson`_. \ No newline at end of file diff --git a/mne/html/d3.v3.min.js b/mne/html/d3.v3.min.js deleted file mode 100644 index eed58e6a572..00000000000 --- a/mne/html/d3.v3.min.js +++ /dev/null @@ -1,5 +0,0 @@ -!function(){function n(n){return null!=n&&!isNaN(n)}function t(n){return n.length}function e(n){for(var t=1;n*t%1;)t*=10;return t}function r(n,t){try{for(var e in t)Object.defineProperty(n.prototype,e,{value:t[e],enumerable:!1})}catch(r){n.prototype=t}}function u(){}function i(n){return aa+n in this}function o(n){return n=aa+n,n in this&&delete this[n]}function a(){var n=[];return this.forEach(function(t){n.push(t)}),n}function c(){var n=0;for(var t in this)t.charCodeAt(0)===ca&&++n;return n}function s(){for(var n in this)if(n.charCodeAt(0)===ca)return!1;return!0}function l(){}function f(n,t,e){return function(){var r=e.apply(t,arguments);return r===t?n:r}}function h(n,t){if(t in n)return t;t=t.charAt(0).toUpperCase()+t.substring(1);for(var e=0,r=sa.length;r>e;++e){var u=sa[e]+t;if(u in n)return u}}function g(){}function p(){}function v(n){function t(){for(var t,r=e,u=-1,i=r.length;++ue;e++)for(var u,i=n[e],o=0,a=i.length;a>o;o++)(u=i[o])&&t(u,o,e);return n}function D(n){return fa(n,ya),n}function P(n){var t,e;return function(r,u,i){var o,a=n[i].update,c=a.length;for(i!=e&&(e=i,t=0),u>=t&&(t=u+1);!(o=a[t])&&++t0&&(n=n.substring(0,a));var s=Ma.get(n);return s&&(n=s,c=F),a?t?u:r:t?g:i}function H(n,t){return function(e){var r=Xo.event;Xo.event=e,t[0]=this.__data__;try{n.apply(this,t)}finally{Xo.event=r}}}function F(n,t){var e=H(n,t);return function(n){var t=this,r=n.relatedTarget;r&&(r===t||8&r.compareDocumentPosition(t))||e.call(t,n)}}function O(){var n=".dragsuppress-"+ ++ba,t="click"+n,e=Xo.select(Go).on("touchmove"+n,d).on("dragstart"+n,d).on("selectstart"+n,d);if(_a){var r=Jo.style,u=r[_a];r[_a]="none"}return function(i){function o(){e.on(t,null)}e.on(n,null),_a&&(r[_a]=u),i&&(e.on(t,function(){d(),o()},!0),setTimeout(o,0))}}function Y(n,t){t.changedTouches&&(t=t.changedTouches[0]);var e=n.ownerSVGElement||n;if(e.createSVGPoint){var r=e.createSVGPoint();if(0>wa&&(Go.scrollX||Go.scrollY)){e=Xo.select("body").append("svg").style({position:"absolute",top:0,left:0,margin:0,padding:0,border:"none"},"important");var u=e[0][0].getScreenCTM();wa=!(u.f||u.e),e.remove()}return wa?(r.x=t.pageX,r.y=t.pageY):(r.x=t.clientX,r.y=t.clientY),r=r.matrixTransform(n.getScreenCTM().inverse()),[r.x,r.y]}var i=n.getBoundingClientRect();return[t.clientX-i.left-n.clientLeft,t.clientY-i.top-n.clientTop]}function I(n){return n>0?1:0>n?-1:0}function Z(n,t,e){return(t[0]-n[0])*(e[1]-n[1])-(t[1]-n[1])*(e[0]-n[0])}function V(n){return n>1?0:-1>n?Sa:Math.acos(n)}function X(n){return n>1?Ea:-1>n?-Ea:Math.asin(n)}function $(n){return((n=Math.exp(n))-1/n)/2}function B(n){return((n=Math.exp(n))+1/n)/2}function W(n){return((n=Math.exp(2*n))-1)/(n+1)}function J(n){return(n=Math.sin(n/2))*n}function G(){}function K(n,t,e){return new Q(n,t,e)}function Q(n,t,e){this.h=n,this.s=t,this.l=e}function nt(n,t,e){function r(n){return n>360?n-=360:0>n&&(n+=360),60>n?i+(o-i)*n/60:180>n?o:240>n?i+(o-i)*(240-n)/60:i}function u(n){return Math.round(255*r(n))}var i,o;return n=isNaN(n)?0:(n%=360)<0?n+360:n,t=isNaN(t)?0:0>t?0:t>1?1:t,e=0>e?0:e>1?1:e,o=.5>=e?e*(1+t):e+t-e*t,i=2*e-o,gt(u(n+120),u(n),u(n-120))}function tt(n,t,e){return new et(n,t,e)}function et(n,t,e){this.h=n,this.c=t,this.l=e}function rt(n,t,e){return isNaN(n)&&(n=0),isNaN(t)&&(t=0),ut(e,Math.cos(n*=Na)*t,Math.sin(n)*t)}function ut(n,t,e){return new it(n,t,e)}function it(n,t,e){this.l=n,this.a=t,this.b=e}function ot(n,t,e){var r=(n+16)/116,u=r+t/500,i=r-e/200;return u=ct(u)*Fa,r=ct(r)*Oa,i=ct(i)*Ya,gt(lt(3.2404542*u-1.5371385*r-.4985314*i),lt(-.969266*u+1.8760108*r+.041556*i),lt(.0556434*u-.2040259*r+1.0572252*i))}function at(n,t,e){return n>0?tt(Math.atan2(e,t)*La,Math.sqrt(t*t+e*e),n):tt(0/0,0/0,n)}function ct(n){return n>.206893034?n*n*n:(n-4/29)/7.787037}function st(n){return n>.008856?Math.pow(n,1/3):7.787037*n+4/29}function lt(n){return Math.round(255*(.00304>=n?12.92*n:1.055*Math.pow(n,1/2.4)-.055))}function ft(n){return gt(n>>16,255&n>>8,255&n)}function ht(n){return ft(n)+""}function gt(n,t,e){return new pt(n,t,e)}function pt(n,t,e){this.r=n,this.g=t,this.b=e}function vt(n){return 16>n?"0"+Math.max(0,n).toString(16):Math.min(255,n).toString(16)}function dt(n,t,e){var r,u,i,o=0,a=0,c=0;if(r=/([a-z]+)\((.*)\)/i.exec(n))switch(u=r[2].split(","),r[1]){case"hsl":return e(parseFloat(u[0]),parseFloat(u[1])/100,parseFloat(u[2])/100);case"rgb":return t(Mt(u[0]),Mt(u[1]),Mt(u[2]))}return(i=Va.get(n))?t(i.r,i.g,i.b):(null!=n&&"#"===n.charAt(0)&&(4===n.length?(o=n.charAt(1),o+=o,a=n.charAt(2),a+=a,c=n.charAt(3),c+=c):7===n.length&&(o=n.substring(1,3),a=n.substring(3,5),c=n.substring(5,7)),o=parseInt(o,16),a=parseInt(a,16),c=parseInt(c,16)),t(o,a,c))}function mt(n,t,e){var r,u,i=Math.min(n/=255,t/=255,e/=255),o=Math.max(n,t,e),a=o-i,c=(o+i)/2;return a?(u=.5>c?a/(o+i):a/(2-o-i),r=n==o?(t-e)/a+(e>t?6:0):t==o?(e-n)/a+2:(n-t)/a+4,r*=60):(r=0/0,u=c>0&&1>c?0:r),K(r,u,c)}function yt(n,t,e){n=xt(n),t=xt(t),e=xt(e);var r=st((.4124564*n+.3575761*t+.1804375*e)/Fa),u=st((.2126729*n+.7151522*t+.072175*e)/Oa),i=st((.0193339*n+.119192*t+.9503041*e)/Ya);return ut(116*u-16,500*(r-u),200*(u-i))}function xt(n){return(n/=255)<=.04045?n/12.92:Math.pow((n+.055)/1.055,2.4)}function Mt(n){var t=parseFloat(n);return"%"===n.charAt(n.length-1)?Math.round(2.55*t):t}function _t(n){return"function"==typeof n?n:function(){return n}}function bt(n){return n}function wt(n){return function(t,e,r){return 2===arguments.length&&"function"==typeof e&&(r=e,e=null),St(t,e,n,r)}}function St(n,t,e,r){function u(){var n,t=c.status;if(!t&&c.responseText||t>=200&&300>t||304===t){try{n=e.call(i,c)}catch(r){return o.error.call(i,r),void 0}o.load.call(i,n)}else o.error.call(i,c)}var i={},o=Xo.dispatch("beforesend","progress","load","error"),a={},c=new XMLHttpRequest,s=null;return!Go.XDomainRequest||"withCredentials"in c||!/^(http(s)?:)?\/\//.test(n)||(c=new XDomainRequest),"onload"in c?c.onload=c.onerror=u:c.onreadystatechange=function(){c.readyState>3&&u()},c.onprogress=function(n){var t=Xo.event;Xo.event=n;try{o.progress.call(i,c)}finally{Xo.event=t}},i.header=function(n,t){return n=(n+"").toLowerCase(),arguments.length<2?a[n]:(null==t?delete a[n]:a[n]=t+"",i)},i.mimeType=function(n){return arguments.length?(t=null==n?null:n+"",i):t},i.responseType=function(n){return arguments.length?(s=n,i):s},i.response=function(n){return e=n,i},["get","post"].forEach(function(n){i[n]=function(){return i.send.apply(i,[n].concat(Bo(arguments)))}}),i.send=function(e,r,u){if(2===arguments.length&&"function"==typeof r&&(u=r,r=null),c.open(e,n,!0),null==t||"accept"in a||(a.accept=t+",*/*"),c.setRequestHeader)for(var l in a)c.setRequestHeader(l,a[l]);return null!=t&&c.overrideMimeType&&c.overrideMimeType(t),null!=s&&(c.responseType=s),null!=u&&i.on("error",u).on("load",function(n){u(null,n)}),o.beforesend.call(i,c),c.send(null==r?null:r),i},i.abort=function(){return c.abort(),i},Xo.rebind(i,o,"on"),null==r?i:i.get(kt(r))}function kt(n){return 1===n.length?function(t,e){n(null==t?e:null)}:n}function Et(){var n=At(),t=Ct()-n;t>24?(isFinite(t)&&(clearTimeout(Wa),Wa=setTimeout(Et,t)),Ba=0):(Ba=1,Ga(Et))}function At(){var n=Date.now();for(Ja=Xa;Ja;)n>=Ja.t&&(Ja.f=Ja.c(n-Ja.t)),Ja=Ja.n;return n}function Ct(){for(var n,t=Xa,e=1/0;t;)t.f?t=n?n.n=t.n:Xa=t.n:(t.t8?function(n){return n/e}:function(n){return n*e},symbol:n}}function zt(n){var t=n.decimal,e=n.thousands,r=n.grouping,u=n.currency,i=r?function(n){for(var t=n.length,u=[],i=0,o=r[0];t>0&&o>0;)u.push(n.substring(t-=o,t+o)),o=r[i=(i+1)%r.length];return u.reverse().join(e)}:bt;return function(n){var e=Qa.exec(n),r=e[1]||" ",o=e[2]||">",a=e[3]||"",c=e[4]||"",s=e[5],l=+e[6],f=e[7],h=e[8],g=e[9],p=1,v="",d="",m=!1;switch(h&&(h=+h.substring(1)),(s||"0"===r&&"="===o)&&(s=r="0",o="=",f&&(l-=Math.floor((l-1)/4))),g){case"n":f=!0,g="g";break;case"%":p=100,d="%",g="f";break;case"p":p=100,d="%",g="r";break;case"b":case"o":case"x":case"X":"#"===c&&(v="0"+g.toLowerCase());case"c":case"d":m=!0,h=0;break;case"s":p=-1,g="r"}"$"===c&&(v=u[0],d=u[1]),"r"!=g||h||(g="g"),null!=h&&("g"==g?h=Math.max(1,Math.min(21,h)):("e"==g||"f"==g)&&(h=Math.max(0,Math.min(20,h)))),g=nc.get(g)||qt;var y=s&&f;return function(n){var e=d;if(m&&n%1)return"";var u=0>n||0===n&&0>1/n?(n=-n,"-"):a;if(0>p){var c=Xo.formatPrefix(n,h);n=c.scale(n),e=c.symbol+d}else n*=p;n=g(n,h);var x=n.lastIndexOf("."),M=0>x?n:n.substring(0,x),_=0>x?"":t+n.substring(x+1);!s&&f&&(M=i(M));var b=v.length+M.length+_.length+(y?0:u.length),w=l>b?new Array(b=l-b+1).join(r):"";return y&&(M=i(w+M)),u+=v,n=M+_,("<"===o?u+n+w:">"===o?w+u+n:"^"===o?w.substring(0,b>>=1)+u+n+w.substring(b):u+(y?n:w+n))+e}}}function qt(n){return n+""}function Tt(){this._=new Date(arguments.length>1?Date.UTC.apply(this,arguments):arguments[0])}function Rt(n,t,e){function r(t){var e=n(t),r=i(e,1);return r-t>t-e?e:r}function u(e){return t(e=n(new ec(e-1)),1),e}function i(n,e){return t(n=new ec(+n),e),n}function o(n,r,i){var o=u(n),a=[];if(i>1)for(;r>o;)e(o)%i||a.push(new Date(+o)),t(o,1);else for(;r>o;)a.push(new Date(+o)),t(o,1);return a}function a(n,t,e){try{ec=Tt;var r=new Tt;return r._=n,o(r,t,e)}finally{ec=Date}}n.floor=n,n.round=r,n.ceil=u,n.offset=i,n.range=o;var c=n.utc=Dt(n);return c.floor=c,c.round=Dt(r),c.ceil=Dt(u),c.offset=Dt(i),c.range=a,n}function Dt(n){return function(t,e){try{ec=Tt;var r=new Tt;return r._=t,n(r,e)._}finally{ec=Date}}}function Pt(n){function t(n){function t(t){for(var e,u,i,o=[],a=-1,c=0;++aa;){if(r>=s)return-1;if(u=t.charCodeAt(a++),37===u){if(o=t.charAt(a++),i=N[o in uc?t.charAt(a++):o],!i||(r=i(n,e,r))<0)return-1}else if(u!=e.charCodeAt(r++))return-1}return r}function r(n,t,e){b.lastIndex=0;var r=b.exec(t.substring(e));return r?(n.w=w.get(r[0].toLowerCase()),e+r[0].length):-1}function u(n,t,e){M.lastIndex=0;var r=M.exec(t.substring(e));return r?(n.w=_.get(r[0].toLowerCase()),e+r[0].length):-1}function i(n,t,e){E.lastIndex=0;var r=E.exec(t.substring(e));return r?(n.m=A.get(r[0].toLowerCase()),e+r[0].length):-1}function o(n,t,e){S.lastIndex=0;var r=S.exec(t.substring(e));return r?(n.m=k.get(r[0].toLowerCase()),e+r[0].length):-1}function a(n,t,r){return e(n,C.c.toString(),t,r)}function c(n,t,r){return e(n,C.x.toString(),t,r)}function s(n,t,r){return e(n,C.X.toString(),t,r)}function l(n,t,e){var r=x.get(t.substring(e,e+=2).toLowerCase());return null==r?-1:(n.p=r,e)}var f=n.dateTime,h=n.date,g=n.time,p=n.periods,v=n.days,d=n.shortDays,m=n.months,y=n.shortMonths;t.utc=function(n){function e(n){try{ec=Tt;var t=new ec;return t._=n,r(t)}finally{ec=Date}}var r=t(n);return e.parse=function(n){try{ec=Tt;var t=r.parse(n);return t&&t._}finally{ec=Date}},e.toString=r.toString,e},t.multi=t.utc.multi=ee;var x=Xo.map(),M=jt(v),_=Ht(v),b=jt(d),w=Ht(d),S=jt(m),k=Ht(m),E=jt(y),A=Ht(y);p.forEach(function(n,t){x.set(n.toLowerCase(),t)});var C={a:function(n){return d[n.getDay()]},A:function(n){return v[n.getDay()]},b:function(n){return y[n.getMonth()]},B:function(n){return m[n.getMonth()]},c:t(f),d:function(n,t){return Ut(n.getDate(),t,2)},e:function(n,t){return Ut(n.getDate(),t,2)},H:function(n,t){return Ut(n.getHours(),t,2)},I:function(n,t){return Ut(n.getHours()%12||12,t,2)},j:function(n,t){return Ut(1+tc.dayOfYear(n),t,3)},L:function(n,t){return Ut(n.getMilliseconds(),t,3)},m:function(n,t){return Ut(n.getMonth()+1,t,2)},M:function(n,t){return Ut(n.getMinutes(),t,2)},p:function(n){return p[+(n.getHours()>=12)]},S:function(n,t){return Ut(n.getSeconds(),t,2)},U:function(n,t){return Ut(tc.sundayOfYear(n),t,2)},w:function(n){return n.getDay()},W:function(n,t){return Ut(tc.mondayOfYear(n),t,2)},x:t(h),X:t(g),y:function(n,t){return Ut(n.getFullYear()%100,t,2)},Y:function(n,t){return Ut(n.getFullYear()%1e4,t,4)},Z:ne,"%":function(){return"%"}},N={a:r,A:u,b:i,B:o,c:a,d:Bt,e:Bt,H:Jt,I:Jt,j:Wt,L:Qt,m:$t,M:Gt,p:l,S:Kt,U:Ot,w:Ft,W:Yt,x:c,X:s,y:Zt,Y:It,Z:Vt,"%":te};return t}function Ut(n,t,e){var r=0>n?"-":"",u=(r?-n:n)+"",i=u.length;return r+(e>i?new Array(e-i+1).join(t)+u:u)}function jt(n){return new RegExp("^(?:"+n.map(Xo.requote).join("|")+")","i")}function Ht(n){for(var t=new u,e=-1,r=n.length;++e68?1900:2e3)}function $t(n,t,e){ic.lastIndex=0;var r=ic.exec(t.substring(e,e+2));return r?(n.m=r[0]-1,e+r[0].length):-1}function Bt(n,t,e){ic.lastIndex=0;var r=ic.exec(t.substring(e,e+2));return r?(n.d=+r[0],e+r[0].length):-1}function Wt(n,t,e){ic.lastIndex=0;var r=ic.exec(t.substring(e,e+3));return r?(n.j=+r[0],e+r[0].length):-1}function Jt(n,t,e){ic.lastIndex=0;var r=ic.exec(t.substring(e,e+2));return r?(n.H=+r[0],e+r[0].length):-1}function Gt(n,t,e){ic.lastIndex=0;var r=ic.exec(t.substring(e,e+2));return r?(n.M=+r[0],e+r[0].length):-1}function Kt(n,t,e){ic.lastIndex=0;var r=ic.exec(t.substring(e,e+2));return r?(n.S=+r[0],e+r[0].length):-1}function Qt(n,t,e){ic.lastIndex=0;var r=ic.exec(t.substring(e,e+3));return r?(n.L=+r[0],e+r[0].length):-1}function ne(n){var t=n.getTimezoneOffset(),e=t>0?"-":"+",r=~~(oa(t)/60),u=oa(t)%60;return e+Ut(r,"0",2)+Ut(u,"0",2)}function te(n,t,e){oc.lastIndex=0;var r=oc.exec(t.substring(e,e+1));return r?e+r[0].length:-1}function ee(n){for(var t=n.length,e=-1;++ea;++a)u.point((e=n[a])[0],e[1]);return u.lineEnd(),void 0}var c=new ke(e,n,null,!0),s=new ke(e,null,c,!1);c.o=s,i.push(c),o.push(s),c=new ke(r,n,null,!1),s=new ke(r,null,c,!0),c.o=s,i.push(c),o.push(s)}}),o.sort(t),Se(i),Se(o),i.length){for(var a=0,c=e,s=o.length;s>a;++a)o[a].e=c=!c;for(var l,f,h=i[0];;){for(var g=h,p=!0;g.v;)if((g=g.n)===h)return;l=g.z,u.lineStart();do{if(g.v=g.o.v=!0,g.e){if(p)for(var a=0,s=l.length;s>a;++a)u.point((f=l[a])[0],f[1]);else r(g.x,g.n.x,1,u);g=g.n}else{if(p){l=g.p.z;for(var a=l.length-1;a>=0;--a)u.point((f=l[a])[0],f[1])}else r(g.x,g.p.x,-1,u);g=g.p}g=g.o,l=g.z,p=!p}while(!g.v);u.lineEnd()}}}function Se(n){if(t=n.length){for(var t,e,r=0,u=n[0];++r1&&2&t&&e.push(e.pop().concat(e.shift())),g.push(e.filter(Ae))}}var g,p,v,d=t(i),m=u.invert(r[0],r[1]),y={point:o,lineStart:c,lineEnd:s,polygonStart:function(){y.point=l,y.lineStart=f,y.lineEnd=h,g=[],p=[],i.polygonStart()},polygonEnd:function(){y.point=o,y.lineStart=c,y.lineEnd=s,g=Xo.merge(g);var n=Le(m,p);g.length?we(g,Ne,n,e,i):n&&(i.lineStart(),e(null,null,1,i),i.lineEnd()),i.polygonEnd(),g=p=null},sphere:function(){i.polygonStart(),i.lineStart(),e(null,null,1,i),i.lineEnd(),i.polygonEnd()}},x=Ce(),M=t(x);return y}}function Ae(n){return n.length>1}function Ce(){var n,t=[];return{lineStart:function(){t.push(n=[])},point:function(t,e){n.push([t,e])},lineEnd:g,buffer:function(){var e=t;return t=[],n=null,e},rejoin:function(){t.length>1&&t.push(t.pop().concat(t.shift()))}}}function Ne(n,t){return((n=n.x)[0]<0?n[1]-Ea-Aa:Ea-n[1])-((t=t.x)[0]<0?t[1]-Ea-Aa:Ea-t[1])}function Le(n,t){var e=n[0],r=n[1],u=[Math.sin(e),-Math.cos(e),0],i=0,o=0;hc.reset();for(var a=0,c=t.length;c>a;++a){var s=t[a],l=s.length;if(l)for(var f=s[0],h=f[0],g=f[1]/2+Sa/4,p=Math.sin(g),v=Math.cos(g),d=1;;){d===l&&(d=0),n=s[d];var m=n[0],y=n[1]/2+Sa/4,x=Math.sin(y),M=Math.cos(y),_=m-h,b=oa(_)>Sa,w=p*x;if(hc.add(Math.atan2(w*Math.sin(_),v*M+w*Math.cos(_))),i+=b?_+(_>=0?ka:-ka):_,b^h>=e^m>=e){var S=fe(se(f),se(n));pe(S);var k=fe(u,S);pe(k);var E=(b^_>=0?-1:1)*X(k[2]);(r>E||r===E&&(S[0]||S[1]))&&(o+=b^_>=0?1:-1)}if(!d++)break;h=m,p=x,v=M,f=n}}return(-Aa>i||Aa>i&&0>hc)^1&o}function ze(n){var t,e=0/0,r=0/0,u=0/0;return{lineStart:function(){n.lineStart(),t=1},point:function(i,o){var a=i>0?Sa:-Sa,c=oa(i-e);oa(c-Sa)0?Ea:-Ea),n.point(u,r),n.lineEnd(),n.lineStart(),n.point(a,r),n.point(i,r),t=0):u!==a&&c>=Sa&&(oa(e-u)Aa?Math.atan((Math.sin(t)*(i=Math.cos(r))*Math.sin(e)-Math.sin(r)*(u=Math.cos(t))*Math.sin(n))/(u*i*o)):(t+r)/2}function Te(n,t,e,r){var u;if(null==n)u=e*Ea,r.point(-Sa,u),r.point(0,u),r.point(Sa,u),r.point(Sa,0),r.point(Sa,-u),r.point(0,-u),r.point(-Sa,-u),r.point(-Sa,0),r.point(-Sa,u);else if(oa(n[0]-t[0])>Aa){var i=n[0]i}function e(n){var e,i,c,s,l;return{lineStart:function(){s=c=!1,l=1},point:function(f,h){var g,p=[f,h],v=t(f,h),d=o?v?0:u(f,h):v?u(f+(0>f?Sa:-Sa),h):0;if(!e&&(s=c=v)&&n.lineStart(),v!==c&&(g=r(e,p),(de(e,g)||de(p,g))&&(p[0]+=Aa,p[1]+=Aa,v=t(p[0],p[1]))),v!==c)l=0,v?(n.lineStart(),g=r(p,e),n.point(g[0],g[1])):(g=r(e,p),n.point(g[0],g[1]),n.lineEnd()),e=g;else if(a&&e&&o^v){var m;d&i||!(m=r(p,e,!0))||(l=0,o?(n.lineStart(),n.point(m[0][0],m[0][1]),n.point(m[1][0],m[1][1]),n.lineEnd()):(n.point(m[1][0],m[1][1]),n.lineEnd(),n.lineStart(),n.point(m[0][0],m[0][1])))}!v||e&&de(e,p)||n.point(p[0],p[1]),e=p,c=v,i=d},lineEnd:function(){c&&n.lineEnd(),e=null},clean:function(){return l|(s&&c)<<1}}}function r(n,t,e){var r=se(n),u=se(t),o=[1,0,0],a=fe(r,u),c=le(a,a),s=a[0],l=c-s*s;if(!l)return!e&&n;var f=i*c/l,h=-i*s/l,g=fe(o,a),p=ge(o,f),v=ge(a,h);he(p,v);var d=g,m=le(p,d),y=le(d,d),x=m*m-y*(le(p,p)-1);if(!(0>x)){var M=Math.sqrt(x),_=ge(d,(-m-M)/y);if(he(_,p),_=ve(_),!e)return _;var b,w=n[0],S=t[0],k=n[1],E=t[1];w>S&&(b=w,w=S,S=b);var A=S-w,C=oa(A-Sa)A;if(!C&&k>E&&(b=k,k=E,E=b),N?C?k+E>0^_[1]<(oa(_[0]-w)Sa^(w<=_[0]&&_[0]<=S)){var L=ge(d,(-m+M)/y);return he(L,p),[_,ve(L)]}}}function u(t,e){var r=o?n:Sa-n,u=0;return-r>t?u|=1:t>r&&(u|=2),-r>e?u|=4:e>r&&(u|=8),u}var i=Math.cos(n),o=i>0,a=oa(i)>Aa,c=cr(n,6*Na);return Ee(t,e,c,o?[0,-n]:[-Sa,n-Sa])}function De(n,t,e,r){return function(u){var i,o=u.a,a=u.b,c=o.x,s=o.y,l=a.x,f=a.y,h=0,g=1,p=l-c,v=f-s;if(i=n-c,p||!(i>0)){if(i/=p,0>p){if(h>i)return;g>i&&(g=i)}else if(p>0){if(i>g)return;i>h&&(h=i)}if(i=e-c,p||!(0>i)){if(i/=p,0>p){if(i>g)return;i>h&&(h=i)}else if(p>0){if(h>i)return;g>i&&(g=i)}if(i=t-s,v||!(i>0)){if(i/=v,0>v){if(h>i)return;g>i&&(g=i)}else if(v>0){if(i>g)return;i>h&&(h=i)}if(i=r-s,v||!(0>i)){if(i/=v,0>v){if(i>g)return;i>h&&(h=i)}else if(v>0){if(h>i)return;g>i&&(g=i)}return h>0&&(u.a={x:c+h*p,y:s+h*v}),1>g&&(u.b={x:c+g*p,y:s+g*v}),u}}}}}}function Pe(n,t,e,r){function u(r,u){return oa(r[0]-n)0?0:3:oa(r[0]-e)0?2:1:oa(r[1]-t)0?1:0:u>0?3:2}function i(n,t){return o(n.x,t.x)}function o(n,t){var e=u(n,1),r=u(t,1);return e!==r?e-r:0===e?t[1]-n[1]:1===e?n[0]-t[0]:2===e?n[1]-t[1]:t[0]-n[0]}return function(a){function c(n){for(var t=0,e=d.length,r=n[1],u=0;e>u;++u)for(var i,o=1,a=d[u],c=a.length,s=a[0];c>o;++o)i=a[o],s[1]<=r?i[1]>r&&Z(s,i,n)>0&&++t:i[1]<=r&&Z(s,i,n)<0&&--t,s=i;return 0!==t}function s(i,a,c,s){var l=0,f=0;if(null==i||(l=u(i,c))!==(f=u(a,c))||o(i,a)<0^c>0){do s.point(0===l||3===l?n:e,l>1?r:t);while((l=(l+c+4)%4)!==f)}else s.point(a[0],a[1])}function l(u,i){return u>=n&&e>=u&&i>=t&&r>=i}function f(n,t){l(n,t)&&a.point(n,t)}function h(){N.point=p,d&&d.push(m=[]),S=!0,w=!1,_=b=0/0}function g(){v&&(p(y,x),M&&w&&A.rejoin(),v.push(A.buffer())),N.point=f,w&&a.lineEnd()}function p(n,t){n=Math.max(-Ac,Math.min(Ac,n)),t=Math.max(-Ac,Math.min(Ac,t));var e=l(n,t);if(d&&m.push([n,t]),S)y=n,x=t,M=e,S=!1,e&&(a.lineStart(),a.point(n,t));else if(e&&w)a.point(n,t);else{var r={a:{x:_,y:b},b:{x:n,y:t}};C(r)?(w||(a.lineStart(),a.point(r.a.x,r.a.y)),a.point(r.b.x,r.b.y),e||a.lineEnd(),k=!1):e&&(a.lineStart(),a.point(n,t),k=!1)}_=n,b=t,w=e}var v,d,m,y,x,M,_,b,w,S,k,E=a,A=Ce(),C=De(n,t,e,r),N={point:f,lineStart:h,lineEnd:g,polygonStart:function(){a=A,v=[],d=[],k=!0},polygonEnd:function(){a=E,v=Xo.merge(v);var t=c([n,r]),e=k&&t,u=v.length;(e||u)&&(a.polygonStart(),e&&(a.lineStart(),s(null,null,1,a),a.lineEnd()),u&&we(v,i,t,s,a),a.polygonEnd()),v=d=m=null}};return N}}function Ue(n,t){function e(e,r){return e=n(e,r),t(e[0],e[1])}return n.invert&&t.invert&&(e.invert=function(e,r){return e=t.invert(e,r),e&&n.invert(e[0],e[1])}),e}function je(n){var t=0,e=Sa/3,r=nr(n),u=r(t,e);return u.parallels=function(n){return arguments.length?r(t=n[0]*Sa/180,e=n[1]*Sa/180):[180*(t/Sa),180*(e/Sa)]},u}function He(n,t){function e(n,t){var e=Math.sqrt(i-2*u*Math.sin(t))/u;return[e*Math.sin(n*=u),o-e*Math.cos(n)]}var r=Math.sin(n),u=(r+Math.sin(t))/2,i=1+r*(2*u-r),o=Math.sqrt(i)/u;return e.invert=function(n,t){var e=o-t;return[Math.atan2(n,e)/u,X((i-(n*n+e*e)*u*u)/(2*u))]},e}function Fe(){function n(n,t){Nc+=u*n-r*t,r=n,u=t}var t,e,r,u;Rc.point=function(i,o){Rc.point=n,t=r=i,e=u=o},Rc.lineEnd=function(){n(t,e)}}function Oe(n,t){Lc>n&&(Lc=n),n>qc&&(qc=n),zc>t&&(zc=t),t>Tc&&(Tc=t)}function Ye(){function n(n,t){o.push("M",n,",",t,i)}function t(n,t){o.push("M",n,",",t),a.point=e}function e(n,t){o.push("L",n,",",t)}function r(){a.point=n}function u(){o.push("Z")}var i=Ie(4.5),o=[],a={point:n,lineStart:function(){a.point=t},lineEnd:r,polygonStart:function(){a.lineEnd=u},polygonEnd:function(){a.lineEnd=r,a.point=n},pointRadius:function(n){return i=Ie(n),a},result:function(){if(o.length){var n=o.join("");return o=[],n}}};return a}function Ie(n){return"m0,"+n+"a"+n+","+n+" 0 1,1 0,"+-2*n+"a"+n+","+n+" 0 1,1 0,"+2*n+"z"}function Ze(n,t){dc+=n,mc+=t,++yc}function Ve(){function n(n,r){var u=n-t,i=r-e,o=Math.sqrt(u*u+i*i);xc+=o*(t+n)/2,Mc+=o*(e+r)/2,_c+=o,Ze(t=n,e=r)}var t,e;Pc.point=function(r,u){Pc.point=n,Ze(t=r,e=u)}}function Xe(){Pc.point=Ze}function $e(){function n(n,t){var e=n-r,i=t-u,o=Math.sqrt(e*e+i*i);xc+=o*(r+n)/2,Mc+=o*(u+t)/2,_c+=o,o=u*n-r*t,bc+=o*(r+n),wc+=o*(u+t),Sc+=3*o,Ze(r=n,u=t)}var t,e,r,u;Pc.point=function(i,o){Pc.point=n,Ze(t=r=i,e=u=o)},Pc.lineEnd=function(){n(t,e)}}function Be(n){function t(t,e){n.moveTo(t,e),n.arc(t,e,o,0,ka)}function e(t,e){n.moveTo(t,e),a.point=r}function r(t,e){n.lineTo(t,e)}function u(){a.point=t}function i(){n.closePath()}var o=4.5,a={point:t,lineStart:function(){a.point=e},lineEnd:u,polygonStart:function(){a.lineEnd=i},polygonEnd:function(){a.lineEnd=u,a.point=t},pointRadius:function(n){return o=n,a},result:g};return a}function We(n){function t(n){return(a?r:e)(n)}function e(t){return Ke(t,function(e,r){e=n(e,r),t.point(e[0],e[1])})}function r(t){function e(e,r){e=n(e,r),t.point(e[0],e[1])}function r(){x=0/0,S.point=i,t.lineStart()}function i(e,r){var i=se([e,r]),o=n(e,r);u(x,M,y,_,b,w,x=o[0],M=o[1],y=e,_=i[0],b=i[1],w=i[2],a,t),t.point(x,M)}function o(){S.point=e,t.lineEnd()}function c(){r(),S.point=s,S.lineEnd=l}function s(n,t){i(f=n,h=t),g=x,p=M,v=_,d=b,m=w,S.point=i}function l(){u(x,M,y,_,b,w,g,p,f,v,d,m,a,t),S.lineEnd=o,o()}var f,h,g,p,v,d,m,y,x,M,_,b,w,S={point:e,lineStart:r,lineEnd:o,polygonStart:function(){t.polygonStart(),S.lineStart=c},polygonEnd:function(){t.polygonEnd(),S.lineStart=r}};return S}function u(t,e,r,a,c,s,l,f,h,g,p,v,d,m){var y=l-t,x=f-e,M=y*y+x*x;if(M>4*i&&d--){var _=a+g,b=c+p,w=s+v,S=Math.sqrt(_*_+b*b+w*w),k=Math.asin(w/=S),E=oa(oa(w)-1)i||oa((y*L+x*z)/M-.5)>.3||o>a*g+c*p+s*v)&&(u(t,e,r,a,c,s,C,N,E,_/=S,b/=S,w,d,m),m.point(C,N),u(C,N,E,_,b,w,l,f,h,g,p,v,d,m))}}var i=.5,o=Math.cos(30*Na),a=16;return t.precision=function(n){return arguments.length?(a=(i=n*n)>0&&16,t):Math.sqrt(i)},t}function Je(n){var t=We(function(t,e){return n([t*La,e*La])});return function(n){return tr(t(n))}}function Ge(n){this.stream=n}function Ke(n,t){return{point:t,sphere:function(){n.sphere()},lineStart:function(){n.lineStart()},lineEnd:function(){n.lineEnd()},polygonStart:function(){n.polygonStart()},polygonEnd:function(){n.polygonEnd()}}}function Qe(n){return nr(function(){return n})()}function nr(n){function t(n){return n=a(n[0]*Na,n[1]*Na),[n[0]*h+c,s-n[1]*h]}function e(n){return n=a.invert((n[0]-c)/h,(s-n[1])/h),n&&[n[0]*La,n[1]*La]}function r(){a=Ue(o=ur(m,y,x),i);var n=i(v,d);return c=g-n[0]*h,s=p+n[1]*h,u()}function u(){return l&&(l.valid=!1,l=null),t}var i,o,a,c,s,l,f=We(function(n,t){return n=i(n,t),[n[0]*h+c,s-n[1]*h]}),h=150,g=480,p=250,v=0,d=0,m=0,y=0,x=0,M=Ec,_=bt,b=null,w=null;return t.stream=function(n){return l&&(l.valid=!1),l=tr(M(o,f(_(n)))),l.valid=!0,l},t.clipAngle=function(n){return arguments.length?(M=null==n?(b=n,Ec):Re((b=+n)*Na),u()):b -},t.clipExtent=function(n){return arguments.length?(w=n,_=n?Pe(n[0][0],n[0][1],n[1][0],n[1][1]):bt,u()):w},t.scale=function(n){return arguments.length?(h=+n,r()):h},t.translate=function(n){return arguments.length?(g=+n[0],p=+n[1],r()):[g,p]},t.center=function(n){return arguments.length?(v=n[0]%360*Na,d=n[1]%360*Na,r()):[v*La,d*La]},t.rotate=function(n){return arguments.length?(m=n[0]%360*Na,y=n[1]%360*Na,x=n.length>2?n[2]%360*Na:0,r()):[m*La,y*La,x*La]},Xo.rebind(t,f,"precision"),function(){return i=n.apply(this,arguments),t.invert=i.invert&&e,r()}}function tr(n){return Ke(n,function(t,e){n.point(t*Na,e*Na)})}function er(n,t){return[n,t]}function rr(n,t){return[n>Sa?n-ka:-Sa>n?n+ka:n,t]}function ur(n,t,e){return n?t||e?Ue(or(n),ar(t,e)):or(n):t||e?ar(t,e):rr}function ir(n){return function(t,e){return t+=n,[t>Sa?t-ka:-Sa>t?t+ka:t,e]}}function or(n){var t=ir(n);return t.invert=ir(-n),t}function ar(n,t){function e(n,t){var e=Math.cos(t),a=Math.cos(n)*e,c=Math.sin(n)*e,s=Math.sin(t),l=s*r+a*u;return[Math.atan2(c*i-l*o,a*r-s*u),X(l*i+c*o)]}var r=Math.cos(n),u=Math.sin(n),i=Math.cos(t),o=Math.sin(t);return e.invert=function(n,t){var e=Math.cos(t),a=Math.cos(n)*e,c=Math.sin(n)*e,s=Math.sin(t),l=s*i-c*o;return[Math.atan2(c*i+s*o,a*r+l*u),X(l*r-a*u)]},e}function cr(n,t){var e=Math.cos(n),r=Math.sin(n);return function(u,i,o,a){var c=o*t;null!=u?(u=sr(e,u),i=sr(e,i),(o>0?i>u:u>i)&&(u+=o*ka)):(u=n+o*ka,i=n-.5*c);for(var s,l=u;o>0?l>i:i>l;l-=c)a.point((s=ve([e,-r*Math.cos(l),-r*Math.sin(l)]))[0],s[1])}}function sr(n,t){var e=se(t);e[0]-=n,pe(e);var r=V(-e[1]);return((-e[2]<0?-r:r)+2*Math.PI-Aa)%(2*Math.PI)}function lr(n,t,e){var r=Xo.range(n,t-Aa,e).concat(t);return function(n){return r.map(function(t){return[n,t]})}}function fr(n,t,e){var r=Xo.range(n,t-Aa,e).concat(t);return function(n){return r.map(function(t){return[t,n]})}}function hr(n){return n.source}function gr(n){return n.target}function pr(n,t,e,r){var u=Math.cos(t),i=Math.sin(t),o=Math.cos(r),a=Math.sin(r),c=u*Math.cos(n),s=u*Math.sin(n),l=o*Math.cos(e),f=o*Math.sin(e),h=2*Math.asin(Math.sqrt(J(r-t)+u*o*J(e-n))),g=1/Math.sin(h),p=h?function(n){var t=Math.sin(n*=h)*g,e=Math.sin(h-n)*g,r=e*c+t*l,u=e*s+t*f,o=e*i+t*a;return[Math.atan2(u,r)*La,Math.atan2(o,Math.sqrt(r*r+u*u))*La]}:function(){return[n*La,t*La]};return p.distance=h,p}function vr(){function n(n,u){var i=Math.sin(u*=Na),o=Math.cos(u),a=oa((n*=Na)-t),c=Math.cos(a);Uc+=Math.atan2(Math.sqrt((a=o*Math.sin(a))*a+(a=r*i-e*o*c)*a),e*i+r*o*c),t=n,e=i,r=o}var t,e,r;jc.point=function(u,i){t=u*Na,e=Math.sin(i*=Na),r=Math.cos(i),jc.point=n},jc.lineEnd=function(){jc.point=jc.lineEnd=g}}function dr(n,t){function e(t,e){var r=Math.cos(t),u=Math.cos(e),i=n(r*u);return[i*u*Math.sin(t),i*Math.sin(e)]}return e.invert=function(n,e){var r=Math.sqrt(n*n+e*e),u=t(r),i=Math.sin(u),o=Math.cos(u);return[Math.atan2(n*i,r*o),Math.asin(r&&e*i/r)]},e}function mr(n,t){function e(n,t){var e=oa(oa(t)-Ea)u;u++){for(;r>1&&Z(n[e[r-2]],n[e[r-1]],n[u])<=0;)--r;e[r++]=u}return e.slice(0,r)}function kr(n,t){return n[0]-t[0]||n[1]-t[1]}function Er(n,t,e){return(e[0]-t[0])*(n[1]-t[1])<(e[1]-t[1])*(n[0]-t[0])}function Ar(n,t,e,r){var u=n[0],i=e[0],o=t[0]-u,a=r[0]-i,c=n[1],s=e[1],l=t[1]-c,f=r[1]-s,h=(a*(c-s)-f*(u-i))/(f*o-a*l);return[u+h*o,c+h*l]}function Cr(n){var t=n[0],e=n[n.length-1];return!(t[0]-e[0]||t[1]-e[1])}function Nr(){Jr(this),this.edge=this.site=this.circle=null}function Lr(n){var t=Jc.pop()||new Nr;return t.site=n,t}function zr(n){Or(n),$c.remove(n),Jc.push(n),Jr(n)}function qr(n){var t=n.circle,e=t.x,r=t.cy,u={x:e,y:r},i=n.P,o=n.N,a=[n];zr(n);for(var c=i;c.circle&&oa(e-c.circle.x)l;++l)s=a[l],c=a[l-1],$r(s.edge,c.site,s.site,u);c=a[0],s=a[f-1],s.edge=Vr(c.site,s.site,null,u),Fr(c),Fr(s)}function Tr(n){for(var t,e,r,u,i=n.x,o=n.y,a=$c._;a;)if(r=Rr(a,o)-i,r>Aa)a=a.L;else{if(u=i-Dr(a,o),!(u>Aa)){r>-Aa?(t=a.P,e=a):u>-Aa?(t=a,e=a.N):t=e=a;break}if(!a.R){t=a;break}a=a.R}var c=Lr(n);if($c.insert(t,c),t||e){if(t===e)return Or(t),e=Lr(t.site),$c.insert(c,e),c.edge=e.edge=Vr(t.site,c.site),Fr(t),Fr(e),void 0;if(!e)return c.edge=Vr(t.site,c.site),void 0;Or(t),Or(e);var s=t.site,l=s.x,f=s.y,h=n.x-l,g=n.y-f,p=e.site,v=p.x-l,d=p.y-f,m=2*(h*d-g*v),y=h*h+g*g,x=v*v+d*d,M={x:(d*y-g*x)/m+l,y:(h*x-v*y)/m+f};$r(e.edge,s,p,M),c.edge=Vr(s,n,null,M),e.edge=Vr(n,p,null,M),Fr(t),Fr(e)}}function Rr(n,t){var e=n.site,r=e.x,u=e.y,i=u-t;if(!i)return r;var o=n.P;if(!o)return-1/0;e=o.site;var a=e.x,c=e.y,s=c-t;if(!s)return a;var l=a-r,f=1/i-1/s,h=l/s;return f?(-h+Math.sqrt(h*h-2*f*(l*l/(-2*s)-c+s/2+u-i/2)))/f+r:(r+a)/2}function Dr(n,t){var e=n.N;if(e)return Rr(e,t);var r=n.site;return r.y===t?r.x:1/0}function Pr(n){this.site=n,this.edges=[]}function Ur(n){for(var t,e,r,u,i,o,a,c,s,l,f=n[0][0],h=n[1][0],g=n[0][1],p=n[1][1],v=Xc,d=v.length;d--;)if(i=v[d],i&&i.prepare())for(a=i.edges,c=a.length,o=0;c>o;)l=a[o].end(),r=l.x,u=l.y,s=a[++o%c].start(),t=s.x,e=s.y,(oa(r-t)>Aa||oa(u-e)>Aa)&&(a.splice(o,0,new Br(Xr(i.site,l,oa(r-f)Aa?{x:f,y:oa(t-f)Aa?{x:oa(e-p)Aa?{x:h,y:oa(t-h)Aa?{x:oa(e-g)=-Ca)){var g=c*c+s*s,p=l*l+f*f,v=(f*g-s*p)/h,d=(c*p-l*g)/h,f=d+a,m=Gc.pop()||new Hr;m.arc=n,m.site=u,m.x=v+o,m.y=f+Math.sqrt(v*v+d*d),m.cy=f,n.circle=m;for(var y=null,x=Wc._;x;)if(m.yd||d>=a)return;if(h>p){if(i){if(i.y>=s)return}else i={x:d,y:c};e={x:d,y:s}}else{if(i){if(i.yr||r>1)if(h>p){if(i){if(i.y>=s)return}else i={x:(c-u)/r,y:c};e={x:(s-u)/r,y:s}}else{if(i){if(i.yg){if(i){if(i.x>=a)return}else i={x:o,y:r*o+u};e={x:a,y:r*a+u}}else{if(i){if(i.xr;++r)if(o=l[r],o.x==e[0]){if(o.i)if(null==s[o.i+1])for(s[o.i-1]+=o.x,s.splice(o.i,1),u=r+1;i>u;++u)l[u].i--;else for(s[o.i-1]+=o.x+s[o.i+1],s.splice(o.i,2),u=r+1;i>u;++u)l[u].i-=2;else if(null==s[o.i+1])s[o.i]=o.x;else for(s[o.i]=o.x+s[o.i+1],s.splice(o.i+1,1),u=r+1;i>u;++u)l[u].i--;l.splice(r,1),i--,r--}else o.x=su(parseFloat(e[0]),parseFloat(o.x));for(;i>r;)o=l.pop(),null==s[o.i+1]?s[o.i]=o.x:(s[o.i]=o.x+s[o.i+1],s.splice(o.i+1,1)),i--;return 1===s.length?null==s[0]?(o=l[0].x,function(n){return o(n)+""}):function(){return t}:function(n){for(r=0;i>r;++r)s[(o=l[r]).i]=o.x(n);return s.join("")}}function fu(n,t){for(var e,r=Xo.interpolators.length;--r>=0&&!(e=Xo.interpolators[r](n,t)););return e}function hu(n,t){var e,r=[],u=[],i=n.length,o=t.length,a=Math.min(n.length,t.length);for(e=0;a>e;++e)r.push(fu(n[e],t[e]));for(;i>e;++e)u[e]=n[e];for(;o>e;++e)u[e]=t[e];return function(n){for(e=0;a>e;++e)u[e]=r[e](n);return u}}function gu(n){return function(t){return 0>=t?0:t>=1?1:n(t)}}function pu(n){return function(t){return 1-n(1-t)}}function vu(n){return function(t){return.5*(.5>t?n(2*t):2-n(2-2*t))}}function du(n){return n*n}function mu(n){return n*n*n}function yu(n){if(0>=n)return 0;if(n>=1)return 1;var t=n*n,e=t*n;return 4*(.5>n?e:3*(n-t)+e-.75)}function xu(n){return function(t){return Math.pow(t,n)}}function Mu(n){return 1-Math.cos(n*Ea)}function _u(n){return Math.pow(2,10*(n-1))}function bu(n){return 1-Math.sqrt(1-n*n)}function wu(n,t){var e;return arguments.length<2&&(t=.45),arguments.length?e=t/ka*Math.asin(1/n):(n=1,e=t/4),function(r){return 1+n*Math.pow(2,-10*r)*Math.sin((r-e)*ka/t)}}function Su(n){return n||(n=1.70158),function(t){return t*t*((n+1)*t-n)}}function ku(n){return 1/2.75>n?7.5625*n*n:2/2.75>n?7.5625*(n-=1.5/2.75)*n+.75:2.5/2.75>n?7.5625*(n-=2.25/2.75)*n+.9375:7.5625*(n-=2.625/2.75)*n+.984375}function Eu(n,t){n=Xo.hcl(n),t=Xo.hcl(t);var e=n.h,r=n.c,u=n.l,i=t.h-e,o=t.c-r,a=t.l-u;return isNaN(o)&&(o=0,r=isNaN(r)?t.c:r),isNaN(i)?(i=0,e=isNaN(e)?t.h:e):i>180?i-=360:-180>i&&(i+=360),function(n){return rt(e+i*n,r+o*n,u+a*n)+""}}function Au(n,t){n=Xo.hsl(n),t=Xo.hsl(t);var e=n.h,r=n.s,u=n.l,i=t.h-e,o=t.s-r,a=t.l-u;return isNaN(o)&&(o=0,r=isNaN(r)?t.s:r),isNaN(i)?(i=0,e=isNaN(e)?t.h:e):i>180?i-=360:-180>i&&(i+=360),function(n){return nt(e+i*n,r+o*n,u+a*n)+""}}function Cu(n,t){n=Xo.lab(n),t=Xo.lab(t);var e=n.l,r=n.a,u=n.b,i=t.l-e,o=t.a-r,a=t.b-u;return function(n){return ot(e+i*n,r+o*n,u+a*n)+""}}function Nu(n,t){return t-=n,function(e){return Math.round(n+t*e)}}function Lu(n){var t=[n.a,n.b],e=[n.c,n.d],r=qu(t),u=zu(t,e),i=qu(Tu(e,t,-u))||0;t[0]*e[1]180?l+=360:l-s>180&&(s+=360),u.push({i:r.push(r.pop()+"rotate(",null,")")-2,x:su(s,l)})):l&&r.push(r.pop()+"rotate("+l+")"),f!=h?u.push({i:r.push(r.pop()+"skewX(",null,")")-2,x:su(f,h)}):h&&r.push(r.pop()+"skewX("+h+")"),g[0]!=p[0]||g[1]!=p[1]?(e=r.push(r.pop()+"scale(",null,",",null,")"),u.push({i:e-4,x:su(g[0],p[0])},{i:e-2,x:su(g[1],p[1])})):(1!=p[0]||1!=p[1])&&r.push(r.pop()+"scale("+p+")"),e=u.length,function(n){for(var t,i=-1;++ie;++e)(t=n[e][1])>u&&(r=e,u=t);return r}function ei(n){return n.reduce(ri,0)}function ri(n,t){return n+t[1]}function ui(n,t){return ii(n,Math.ceil(Math.log(t.length)/Math.LN2+1))}function ii(n,t){for(var e=-1,r=+n[0],u=(n[1]-r)/t,i=[];++e<=t;)i[e]=u*e+r;return i}function oi(n){return[Xo.min(n),Xo.max(n)]}function ai(n,t){return n.parent==t.parent?1:2}function ci(n){var t=n.children;return t&&t.length?t[0]:n._tree.thread}function si(n){var t,e=n.children;return e&&(t=e.length)?e[t-1]:n._tree.thread}function li(n,t){var e=n.children;if(e&&(u=e.length))for(var r,u,i=-1;++i0&&(n=r);return n}function fi(n,t){return n.x-t.x}function hi(n,t){return t.x-n.x}function gi(n,t){return n.depth-t.depth}function pi(n,t){function e(n,r){var u=n.children;if(u&&(o=u.length))for(var i,o,a=null,c=-1;++c=0;)t=u[i]._tree,t.prelim+=e,t.mod+=e,e+=t.shift+(r+=t.change)}function di(n,t,e){n=n._tree,t=t._tree;var r=e/(t.number-n.number);n.change+=r,t.change-=r,t.shift+=e,t.prelim+=e,t.mod+=e}function mi(n,t,e){return n._tree.ancestor.parent==t.parent?n._tree.ancestor:e}function yi(n,t){return n.value-t.value}function xi(n,t){var e=n._pack_next;n._pack_next=t,t._pack_prev=n,t._pack_next=e,e._pack_prev=t}function Mi(n,t){n._pack_next=t,t._pack_prev=n}function _i(n,t){var e=t.x-n.x,r=t.y-n.y,u=n.r+t.r;return.999*u*u>e*e+r*r}function bi(n){function t(n){l=Math.min(n.x-n.r,l),f=Math.max(n.x+n.r,f),h=Math.min(n.y-n.r,h),g=Math.max(n.y+n.r,g)}if((e=n.children)&&(s=e.length)){var e,r,u,i,o,a,c,s,l=1/0,f=-1/0,h=1/0,g=-1/0;if(e.forEach(wi),r=e[0],r.x=-r.r,r.y=0,t(r),s>1&&(u=e[1],u.x=u.r,u.y=0,t(u),s>2))for(i=e[2],Ei(r,u,i),t(i),xi(r,i),r._pack_prev=i,xi(i,u),u=r._pack_next,o=3;s>o;o++){Ei(r,u,i=e[o]);var p=0,v=1,d=1;for(a=u._pack_next;a!==u;a=a._pack_next,v++)if(_i(a,i)){p=1;break}if(1==p)for(c=r._pack_prev;c!==a._pack_prev&&!_i(c,i);c=c._pack_prev,d++);p?(d>v||v==d&&u.ro;o++)i=e[o],i.x-=m,i.y-=y,x=Math.max(x,i.r+Math.sqrt(i.x*i.x+i.y*i.y));n.r=x,e.forEach(Si)}}function wi(n){n._pack_next=n._pack_prev=n}function Si(n){delete n._pack_next,delete n._pack_prev}function ki(n,t,e,r){var u=n.children;if(n.x=t+=r*n.x,n.y=e+=r*n.y,n.r*=r,u)for(var i=-1,o=u.length;++iu&&(e+=u/2,u=0),0>i&&(r+=i/2,i=0),{x:e,y:r,dx:u,dy:i}}function Ti(n){var t=n[0],e=n[n.length-1];return e>t?[t,e]:[e,t]}function Ri(n){return n.rangeExtent?n.rangeExtent():Ti(n.range())}function Di(n,t,e,r){var u=e(n[0],n[1]),i=r(t[0],t[1]);return function(n){return i(u(n))}}function Pi(n,t){var e,r=0,u=n.length-1,i=n[r],o=n[u];return i>o&&(e=r,r=u,u=e,e=i,i=o,o=e),n[r]=t.floor(i),n[u]=t.ceil(o),n}function Ui(n){return n?{floor:function(t){return Math.floor(t/n)*n},ceil:function(t){return Math.ceil(t/n)*n}}:ls}function ji(n,t,e,r){var u=[],i=[],o=0,a=Math.min(n.length,t.length)-1;for(n[a]2?ji:Di,c=r?Pu:Du;return o=u(n,t,c,e),a=u(t,n,c,fu),i}function i(n){return o(n)}var o,a;return i.invert=function(n){return a(n)},i.domain=function(t){return arguments.length?(n=t.map(Number),u()):n},i.range=function(n){return arguments.length?(t=n,u()):t},i.rangeRound=function(n){return i.range(n).interpolate(Nu)},i.clamp=function(n){return arguments.length?(r=n,u()):r},i.interpolate=function(n){return arguments.length?(e=n,u()):e},i.ticks=function(t){return Ii(n,t)},i.tickFormat=function(t,e){return Zi(n,t,e)},i.nice=function(t){return Oi(n,t),u()},i.copy=function(){return Hi(n,t,e,r)},u()}function Fi(n,t){return Xo.rebind(n,t,"range","rangeRound","interpolate","clamp")}function Oi(n,t){return Pi(n,Ui(Yi(n,t)[2]))}function Yi(n,t){null==t&&(t=10);var e=Ti(n),r=e[1]-e[0],u=Math.pow(10,Math.floor(Math.log(r/t)/Math.LN10)),i=t/r*u;return.15>=i?u*=10:.35>=i?u*=5:.75>=i&&(u*=2),e[0]=Math.ceil(e[0]/u)*u,e[1]=Math.floor(e[1]/u)*u+.5*u,e[2]=u,e}function Ii(n,t){return Xo.range.apply(Xo,Yi(n,t))}function Zi(n,t,e){var r=Yi(n,t);return Xo.format(e?e.replace(Qa,function(n,t,e,u,i,o,a,c,s,l){return[t,e,u,i,o,a,c,s||"."+Xi(l,r),l].join("")}):",."+Vi(r[2])+"f")}function Vi(n){return-Math.floor(Math.log(n)/Math.LN10+.01)}function Xi(n,t){var e=Vi(t[2]);return n in fs?Math.abs(e-Vi(Math.max(Math.abs(t[0]),Math.abs(t[1]))))+ +("e"!==n):e-2*("%"===n)}function $i(n,t,e,r){function u(n){return(e?Math.log(0>n?0:n):-Math.log(n>0?0:-n))/Math.log(t)}function i(n){return e?Math.pow(t,n):-Math.pow(t,-n)}function o(t){return n(u(t))}return o.invert=function(t){return i(n.invert(t))},o.domain=function(t){return arguments.length?(e=t[0]>=0,n.domain((r=t.map(Number)).map(u)),o):r},o.base=function(e){return arguments.length?(t=+e,n.domain(r.map(u)),o):t},o.nice=function(){var t=Pi(r.map(u),e?Math:gs);return n.domain(t),r=t.map(i),o},o.ticks=function(){var n=Ti(r),o=[],a=n[0],c=n[1],s=Math.floor(u(a)),l=Math.ceil(u(c)),f=t%1?2:t;if(isFinite(l-s)){if(e){for(;l>s;s++)for(var h=1;f>h;h++)o.push(i(s)*h);o.push(i(s))}else for(o.push(i(s));s++0;h--)o.push(i(s)*h);for(s=0;o[s]c;l--);o=o.slice(s,l)}return o},o.tickFormat=function(n,t){if(!arguments.length)return hs;arguments.length<2?t=hs:"function"!=typeof t&&(t=Xo.format(t));var r,a=Math.max(.1,n/o.ticks().length),c=e?(r=1e-12,Math.ceil):(r=-1e-12,Math.floor);return function(n){return n/i(c(u(n)+r))<=a?t(n):""}},o.copy=function(){return $i(n.copy(),t,e,r)},Fi(o,n)}function Bi(n,t,e){function r(t){return n(u(t))}var u=Wi(t),i=Wi(1/t);return r.invert=function(t){return i(n.invert(t))},r.domain=function(t){return arguments.length?(n.domain((e=t.map(Number)).map(u)),r):e},r.ticks=function(n){return Ii(e,n)},r.tickFormat=function(n,t){return Zi(e,n,t)},r.nice=function(n){return r.domain(Oi(e,n))},r.exponent=function(o){return arguments.length?(u=Wi(t=o),i=Wi(1/t),n.domain(e.map(u)),r):t},r.copy=function(){return Bi(n.copy(),t,e)},Fi(r,n)}function Wi(n){return function(t){return 0>t?-Math.pow(-t,n):Math.pow(t,n)}}function Ji(n,t){function e(e){return o[((i.get(e)||"range"===t.t&&i.set(e,n.push(e)))-1)%o.length]}function r(t,e){return Xo.range(n.length).map(function(n){return t+e*n})}var i,o,a;return e.domain=function(r){if(!arguments.length)return n;n=[],i=new u;for(var o,a=-1,c=r.length;++ae?[0/0,0/0]:[e>0?u[e-1]:n[0],et?0/0:t/i+n,[t,t+1/i]},r.copy=function(){return Ki(n,t,e)},u()}function Qi(n,t){function e(e){return e>=e?t[Xo.bisect(n,e)]:void 0}return e.domain=function(t){return arguments.length?(n=t,e):n},e.range=function(n){return arguments.length?(t=n,e):t},e.invertExtent=function(e){return e=t.indexOf(e),[n[e-1],n[e]]},e.copy=function(){return Qi(n,t)},e}function no(n){function t(n){return+n}return t.invert=t,t.domain=t.range=function(e){return arguments.length?(n=e.map(t),t):n},t.ticks=function(t){return Ii(n,t)},t.tickFormat=function(t,e){return Zi(n,t,e)},t.copy=function(){return no(n)},t}function to(n){return n.innerRadius}function eo(n){return n.outerRadius}function ro(n){return n.startAngle}function uo(n){return n.endAngle}function io(n){function t(t){function o(){s.push("M",i(n(l),a))}for(var c,s=[],l=[],f=-1,h=t.length,g=_t(e),p=_t(r);++f1&&u.push("H",r[0]),u.join("")}function so(n){for(var t=0,e=n.length,r=n[0],u=[r[0],",",r[1]];++t1){a=t[1],i=n[c],c++,r+="C"+(u[0]+o[0])+","+(u[1]+o[1])+","+(i[0]-a[0])+","+(i[1]-a[1])+","+i[0]+","+i[1];for(var s=2;s9&&(u=3*t/Math.sqrt(u),o[a]=u*e,o[a+1]=u*r));for(a=-1;++a<=c;)u=(n[Math.min(c,a+1)][0]-n[Math.max(0,a-1)][0])/(6*(1+o[a]*o[a])),i.push([u||0,o[a]*u||0]);return i}function Eo(n){return n.length<3?oo(n):n[0]+po(n,ko(n))}function Ao(n){for(var t,e,r,u=-1,i=n.length;++ue?s():(i.active=e,o.event&&o.event.start.call(n,l,t),o.tween.forEach(function(e,r){(r=r.call(n,l,t))&&v.push(r)}),Xo.timer(function(){return p.c=c(r||1)?be:c,1},0,a),void 0)}function c(r){if(i.active!==e)return s();for(var u=r/g,a=f(u),c=v.length;c>0;)v[--c].call(n,a);return u>=1?(o.event&&o.event.end.call(n,l,t),s()):void 0}function s(){return--i.count?delete i[e]:delete n.__transition__,1}var l=n.__data__,f=o.ease,h=o.delay,g=o.duration,p=Ja,v=[];return p.t=h+a,r>=h?u(r-h):(p.c=u,void 0)},0,a)}}function Ho(n,t){n.attr("transform",function(n){return"translate("+t(n)+",0)"})}function Fo(n,t){n.attr("transform",function(n){return"translate(0,"+t(n)+")"})}function Oo(n){return n.toISOString()}function Yo(n,t,e){function r(t){return n(t)}function u(n,e){var r=n[1]-n[0],u=r/e,i=Xo.bisect(js,u);return i==js.length?[t.year,Yi(n.map(function(n){return n/31536e6}),e)[2]]:i?t[u/js[i-1]1?{floor:function(t){for(;e(t=n.floor(t));)t=Io(t-1);return t},ceil:function(t){for(;e(t=n.ceil(t));)t=Io(+t+1);return t}}:n))},r.ticks=function(n,t){var e=Ti(r.domain()),i=null==n?u(e,10):"number"==typeof n?u(e,n):!n.range&&[{range:n},t];return i&&(n=i[0],t=i[1]),n.range(e[0],Io(+e[1]+1),1>t?1:t)},r.tickFormat=function(){return e},r.copy=function(){return Yo(n.copy(),t,e)},Fi(r,n)}function Io(n){return new Date(n)}function Zo(n){return JSON.parse(n.responseText)}function Vo(n){var t=Wo.createRange();return t.selectNode(Wo.body),t.createContextualFragment(n.responseText)}var Xo={version:"3.4.2"};Date.now||(Date.now=function(){return+new Date});var $o=[].slice,Bo=function(n){return $o.call(n)},Wo=document,Jo=Wo.documentElement,Go=window;try{Bo(Jo.childNodes)[0].nodeType}catch(Ko){Bo=function(n){for(var t=n.length,e=new Array(t);t--;)e[t]=n[t];return e}}try{Wo.createElement("div").style.setProperty("opacity",0,"")}catch(Qo){var na=Go.Element.prototype,ta=na.setAttribute,ea=na.setAttributeNS,ra=Go.CSSStyleDeclaration.prototype,ua=ra.setProperty;na.setAttribute=function(n,t){ta.call(this,n,t+"")},na.setAttributeNS=function(n,t,e){ea.call(this,n,t,e+"")},ra.setProperty=function(n,t,e){ua.call(this,n,t+"",e)}}Xo.ascending=function(n,t){return t>n?-1:n>t?1:n>=t?0:0/0},Xo.descending=function(n,t){return n>t?-1:t>n?1:t>=n?0:0/0},Xo.min=function(n,t){var e,r,u=-1,i=n.length;if(1===arguments.length){for(;++u=e);)e=void 0;for(;++ur&&(e=r)}else{for(;++u=e);)e=void 0;for(;++ur&&(e=r)}return e},Xo.max=function(n,t){var e,r,u=-1,i=n.length;if(1===arguments.length){for(;++u=e);)e=void 0;for(;++ue&&(e=r)}else{for(;++u=e);)e=void 0;for(;++ue&&(e=r)}return e},Xo.extent=function(n,t){var e,r,u,i=-1,o=n.length;if(1===arguments.length){for(;++i=e);)e=u=void 0;for(;++ir&&(e=r),r>u&&(u=r))}else{for(;++i=e);)e=void 0;for(;++ir&&(e=r),r>u&&(u=r))}return[e,u]},Xo.sum=function(n,t){var e,r=0,u=n.length,i=-1;if(1===arguments.length)for(;++i1&&(t=t.map(e)),t=t.filter(n),t.length?Xo.quantile(t.sort(Xo.ascending),.5):void 0},Xo.bisector=function(n){return{left:function(t,e,r,u){for(arguments.length<3&&(r=0),arguments.length<4&&(u=t.length);u>r;){var i=r+u>>>1;n.call(t,t[i],i)r;){var i=r+u>>>1;er?0:r);r>e;)i[e]=[t=u,u=n[++e]];return i},Xo.zip=function(){if(!(u=arguments.length))return[];for(var n=-1,e=Xo.min(arguments,t),r=new Array(e);++n=0;)for(r=n[u],t=r.length;--t>=0;)e[--o]=r[t];return e};var oa=Math.abs;Xo.range=function(n,t,r){if(arguments.length<3&&(r=1,arguments.length<2&&(t=n,n=0)),1/0===(t-n)/r)throw new Error("infinite range");var u,i=[],o=e(oa(r)),a=-1;if(n*=o,t*=o,r*=o,0>r)for(;(u=n+r*++a)>t;)i.push(u/o);else for(;(u=n+r*++a)=o.length)return r?r.call(i,a):e?a.sort(e):a;for(var s,l,f,h,g=-1,p=a.length,v=o[c++],d=new u;++g=o.length)return n;var r=[],u=a[e++];return n.forEach(function(n,u){r.push({key:n,values:t(u,e)})}),u?r.sort(function(n,t){return u(n.key,t.key)}):r}var e,r,i={},o=[],a=[];return i.map=function(t,e){return n(e,t,0)},i.entries=function(e){return t(n(Xo.map,e,0),0)},i.key=function(n){return o.push(n),i},i.sortKeys=function(n){return a[o.length-1]=n,i},i.sortValues=function(n){return e=n,i},i.rollup=function(n){return r=n,i},i},Xo.set=function(n){var t=new l;if(n)for(var e=0,r=n.length;r>e;++e)t.add(n[e]);return t},r(l,{has:i,add:function(n){return this[aa+n]=!0,n},remove:function(n){return n=aa+n,n in this&&delete this[n]},values:a,size:c,empty:s,forEach:function(n){for(var t in this)t.charCodeAt(0)===ca&&n.call(this,t.substring(1))}}),Xo.behavior={},Xo.rebind=function(n,t){for(var e,r=1,u=arguments.length;++r=0&&(r=n.substring(e+1),n=n.substring(0,e)),n)return arguments.length<2?this[n].on(r):this[n].on(r,t);if(2===arguments.length){if(null==t)for(n in this)this.hasOwnProperty(n)&&this[n].on(r,null);return this}},Xo.event=null,Xo.requote=function(n){return n.replace(la,"\\$&")};var la=/[\\\^\$\*\+\?\|\[\]\(\)\.\{\}]/g,fa={}.__proto__?function(n,t){n.__proto__=t}:function(n,t){for(var e in t)n[e]=t[e]},ha=function(n,t){return t.querySelector(n)},ga=function(n,t){return t.querySelectorAll(n)},pa=Jo[h(Jo,"matchesSelector")],va=function(n,t){return pa.call(n,t)};"function"==typeof Sizzle&&(ha=function(n,t){return Sizzle(n,t)[0]||null},ga=function(n,t){return Sizzle.uniqueSort(Sizzle(n,t))},va=Sizzle.matchesSelector),Xo.selection=function(){return xa};var da=Xo.selection.prototype=[];da.select=function(n){var t,e,r,u,i=[];n=M(n);for(var o=-1,a=this.length;++o=0&&(e=n.substring(0,t),n=n.substring(t+1)),ma.hasOwnProperty(e)?{space:ma[e],local:n}:n}},da.attr=function(n,t){if(arguments.length<2){if("string"==typeof n){var e=this.node();return n=Xo.ns.qualify(n),n.local?e.getAttributeNS(n.space,n.local):e.getAttribute(n)}for(t in n)this.each(b(t,n[t]));return this}return this.each(b(n,t))},da.classed=function(n,t){if(arguments.length<2){if("string"==typeof n){var e=this.node(),r=(n=k(n)).length,u=-1;if(t=e.classList){for(;++ur){if("string"!=typeof n){2>r&&(t="");for(e in n)this.each(C(e,n[e],t));return this}if(2>r)return Go.getComputedStyle(this.node(),null).getPropertyValue(n);e=""}return this.each(C(n,t,e))},da.property=function(n,t){if(arguments.length<2){if("string"==typeof n)return this.node()[n];for(t in n)this.each(N(t,n[t]));return this}return this.each(N(n,t))},da.text=function(n){return arguments.length?this.each("function"==typeof n?function(){var t=n.apply(this,arguments);this.textContent=null==t?"":t}:null==n?function(){this.textContent=""}:function(){this.textContent=n}):this.node().textContent},da.html=function(n){return arguments.length?this.each("function"==typeof n?function(){var t=n.apply(this,arguments);this.innerHTML=null==t?"":t}:null==n?function(){this.innerHTML=""}:function(){this.innerHTML=n}):this.node().innerHTML},da.append=function(n){return n=L(n),this.select(function(){return this.appendChild(n.apply(this,arguments))})},da.insert=function(n,t){return n=L(n),t=M(t),this.select(function(){return this.insertBefore(n.apply(this,arguments),t.apply(this,arguments)||null)})},da.remove=function(){return this.each(function(){var n=this.parentNode;n&&n.removeChild(this)})},da.data=function(n,t){function e(n,e){var r,i,o,a=n.length,f=e.length,h=Math.min(a,f),g=new Array(f),p=new Array(f),v=new Array(a);if(t){var d,m=new u,y=new u,x=[];for(r=-1;++rr;++r)p[r]=z(e[r]);for(;a>r;++r)v[r]=n[r]}p.update=g,p.parentNode=g.parentNode=v.parentNode=n.parentNode,c.push(p),s.push(g),l.push(v)}var r,i,o=-1,a=this.length;if(!arguments.length){for(n=new Array(a=(r=this[0]).length);++oi;i++){u.push(t=[]),t.parentNode=(e=this[i]).parentNode;for(var a=0,c=e.length;c>a;a++)(r=e[a])&&n.call(r,r.__data__,a,i)&&t.push(r)}return x(u)},da.order=function(){for(var n=-1,t=this.length;++n=0;)(e=r[u])&&(i&&i!==e.nextSibling&&i.parentNode.insertBefore(e,i),i=e);return this},da.sort=function(n){n=T.apply(this,arguments);for(var t=-1,e=this.length;++tn;n++)for(var e=this[n],r=0,u=e.length;u>r;r++){var i=e[r];if(i)return i}return null},da.size=function(){var n=0;return this.each(function(){++n}),n};var ya=[];Xo.selection.enter=D,Xo.selection.enter.prototype=ya,ya.append=da.append,ya.empty=da.empty,ya.node=da.node,ya.call=da.call,ya.size=da.size,ya.select=function(n){for(var t,e,r,u,i,o=[],a=-1,c=this.length;++ar){if("string"!=typeof n){2>r&&(t=!1);for(e in n)this.each(j(e,n[e],t));return this}if(2>r)return(r=this.node()["__on"+n])&&r._;e=!1}return this.each(j(n,t,e))};var Ma=Xo.map({mouseenter:"mouseover",mouseleave:"mouseout"});Ma.forEach(function(n){"on"+n in Wo&&Ma.remove(n)});var _a="onselectstart"in Wo?null:h(Jo.style,"userSelect"),ba=0;Xo.mouse=function(n){return Y(n,m())};var wa=/WebKit/.test(Go.navigator.userAgent)?-1:0;Xo.touches=function(n,t){return arguments.length<2&&(t=m().touches),t?Bo(t).map(function(t){var e=Y(n,t);return e.identifier=t.identifier,e}):[]},Xo.behavior.drag=function(){function n(){this.on("mousedown.drag",o).on("touchstart.drag",a)}function t(){return Xo.event.changedTouches[0].identifier}function e(n,t){return Xo.touches(n).filter(function(n){return n.identifier===t})[0]}function r(n,t,e,r){return function(){function o(){var n=t(l,g),e=n[0]-v[0],r=n[1]-v[1];d|=e|r,v=n,f({type:"drag",x:n[0]+c[0],y:n[1]+c[1],dx:e,dy:r})}function a(){m.on(e+"."+p,null).on(r+"."+p,null),y(d&&Xo.event.target===h),f({type:"dragend"})}var c,s=this,l=s.parentNode,f=u.of(s,arguments),h=Xo.event.target,g=n(),p=null==g?"drag":"drag-"+g,v=t(l,g),d=0,m=Xo.select(Go).on(e+"."+p,o).on(r+"."+p,a),y=O();i?(c=i.apply(s,arguments),c=[c.x-v[0],c.y-v[1]]):c=[0,0],f({type:"dragstart"})}}var u=y(n,"drag","dragstart","dragend"),i=null,o=r(g,Xo.mouse,"mousemove","mouseup"),a=r(t,e,"touchmove","touchend");return n.origin=function(t){return arguments.length?(i=t,n):i},Xo.rebind(n,u,"on")};var Sa=Math.PI,ka=2*Sa,Ea=Sa/2,Aa=1e-6,Ca=Aa*Aa,Na=Sa/180,La=180/Sa,za=Math.SQRT2,qa=2,Ta=4;Xo.interpolateZoom=function(n,t){function e(n){var t=n*y;if(m){var e=B(v),o=i/(qa*h)*(e*W(za*t+v)-$(v));return[r+o*s,u+o*l,i*e/B(za*t+v)]}return[r+n*s,u+n*l,i*Math.exp(za*t)]}var r=n[0],u=n[1],i=n[2],o=t[0],a=t[1],c=t[2],s=o-r,l=a-u,f=s*s+l*l,h=Math.sqrt(f),g=(c*c-i*i+Ta*f)/(2*i*qa*h),p=(c*c-i*i-Ta*f)/(2*c*qa*h),v=Math.log(Math.sqrt(g*g+1)-g),d=Math.log(Math.sqrt(p*p+1)-p),m=d-v,y=(m||Math.log(c/i))/za;return e.duration=1e3*y,e},Xo.behavior.zoom=function(){function n(n){n.on(A,s).on(Pa+".zoom",f).on(C,h).on("dblclick.zoom",g).on(L,l)}function t(n){return[(n[0]-S.x)/S.k,(n[1]-S.y)/S.k]}function e(n){return[n[0]*S.k+S.x,n[1]*S.k+S.y]}function r(n){S.k=Math.max(E[0],Math.min(E[1],n))}function u(n,t){t=e(t),S.x+=n[0]-t[0],S.y+=n[1]-t[1]}function i(){_&&_.domain(M.range().map(function(n){return(n-S.x)/S.k}).map(M.invert)),w&&w.domain(b.range().map(function(n){return(n-S.y)/S.k}).map(b.invert))}function o(n){n({type:"zoomstart"})}function a(n){i(),n({type:"zoom",scale:S.k,translate:[S.x,S.y]})}function c(n){n({type:"zoomend"})}function s(){function n(){l=1,u(Xo.mouse(r),g),a(i)}function e(){f.on(C,Go===r?h:null).on(N,null),p(l&&Xo.event.target===s),c(i)}var r=this,i=z.of(r,arguments),s=Xo.event.target,l=0,f=Xo.select(Go).on(C,n).on(N,e),g=t(Xo.mouse(r)),p=O();U.call(r),o(i)}function l(){function n(){var n=Xo.touches(g);return h=S.k,n.forEach(function(n){n.identifier in v&&(v[n.identifier]=t(n))}),n}function e(){for(var t=Xo.event.changedTouches,e=0,i=t.length;i>e;++e)v[t[e].identifier]=null;var o=n(),c=Date.now();if(1===o.length){if(500>c-x){var s=o[0],l=v[s.identifier];r(2*S.k),u(s,l),d(),a(p)}x=c}else if(o.length>1){var s=o[0],f=o[1],h=s[0]-f[0],g=s[1]-f[1];m=h*h+g*g}}function i(){for(var n,t,e,i,o=Xo.touches(g),c=0,s=o.length;s>c;++c,i=null)if(e=o[c],i=v[e.identifier]){if(t)break;n=e,t=i}if(i){var l=(l=e[0]-n[0])*l+(l=e[1]-n[1])*l,f=m&&Math.sqrt(l/m);n=[(n[0]+e[0])/2,(n[1]+e[1])/2],t=[(t[0]+i[0])/2,(t[1]+i[1])/2],r(f*h)}x=null,u(n,t),a(p)}function f(){if(Xo.event.touches.length){for(var t=Xo.event.changedTouches,e=0,r=t.length;r>e;++e)delete v[t[e].identifier];for(var u in v)return void n()}b.on(M,null).on(_,null),w.on(A,s).on(L,l),k(),c(p)}var h,g=this,p=z.of(g,arguments),v={},m=0,y=Xo.event.changedTouches[0].identifier,M="touchmove.zoom-"+y,_="touchend.zoom-"+y,b=Xo.select(Go).on(M,i).on(_,f),w=Xo.select(g).on(A,null).on(L,e),k=O();U.call(g),e(),o(p)}function f(){var n=z.of(this,arguments);m?clearTimeout(m):(U.call(this),o(n)),m=setTimeout(function(){m=null,c(n)},50),d();var e=v||Xo.mouse(this);p||(p=t(e)),r(Math.pow(2,.002*Ra())*S.k),u(e,p),a(n)}function h(){p=null}function g(){var n=z.of(this,arguments),e=Xo.mouse(this),i=t(e),s=Math.log(S.k)/Math.LN2;o(n),r(Math.pow(2,Xo.event.shiftKey?Math.ceil(s)-1:Math.floor(s)+1)),u(e,i),a(n),c(n)}var p,v,m,x,M,_,b,w,S={x:0,y:0,k:1},k=[960,500],E=Da,A="mousedown.zoom",C="mousemove.zoom",N="mouseup.zoom",L="touchstart.zoom",z=y(n,"zoomstart","zoom","zoomend");return n.event=function(n){n.each(function(){var n=z.of(this,arguments),t=S;ks?Xo.select(this).transition().each("start.zoom",function(){S=this.__chart__||{x:0,y:0,k:1},o(n)}).tween("zoom:zoom",function(){var e=k[0],r=k[1],u=e/2,i=r/2,o=Xo.interpolateZoom([(u-S.x)/S.k,(i-S.y)/S.k,e/S.k],[(u-t.x)/t.k,(i-t.y)/t.k,e/t.k]);return function(t){var r=o(t),c=e/r[2];this.__chart__=S={x:u-r[0]*c,y:i-r[1]*c,k:c},a(n)}}).each("end.zoom",function(){c(n)}):(this.__chart__=S,o(n),a(n),c(n))})},n.translate=function(t){return arguments.length?(S={x:+t[0],y:+t[1],k:S.k},i(),n):[S.x,S.y]},n.scale=function(t){return arguments.length?(S={x:S.x,y:S.y,k:+t},i(),n):S.k},n.scaleExtent=function(t){return arguments.length?(E=null==t?Da:[+t[0],+t[1]],n):E},n.center=function(t){return arguments.length?(v=t&&[+t[0],+t[1]],n):v},n.size=function(t){return arguments.length?(k=t&&[+t[0],+t[1]],n):k},n.x=function(t){return arguments.length?(_=t,M=t.copy(),S={x:0,y:0,k:1},n):_},n.y=function(t){return arguments.length?(w=t,b=t.copy(),S={x:0,y:0,k:1},n):w},Xo.rebind(n,z,"on")};var Ra,Da=[0,1/0],Pa="onwheel"in Wo?(Ra=function(){return-Xo.event.deltaY*(Xo.event.deltaMode?120:1)},"wheel"):"onmousewheel"in Wo?(Ra=function(){return Xo.event.wheelDelta},"mousewheel"):(Ra=function(){return-Xo.event.detail},"MozMousePixelScroll");G.prototype.toString=function(){return this.rgb()+""},Xo.hsl=function(n,t,e){return 1===arguments.length?n instanceof Q?K(n.h,n.s,n.l):dt(""+n,mt,K):K(+n,+t,+e)};var Ua=Q.prototype=new G;Ua.brighter=function(n){return n=Math.pow(.7,arguments.length?n:1),K(this.h,this.s,this.l/n)},Ua.darker=function(n){return n=Math.pow(.7,arguments.length?n:1),K(this.h,this.s,n*this.l)},Ua.rgb=function(){return nt(this.h,this.s,this.l)},Xo.hcl=function(n,t,e){return 1===arguments.length?n instanceof et?tt(n.h,n.c,n.l):n instanceof it?at(n.l,n.a,n.b):at((n=yt((n=Xo.rgb(n)).r,n.g,n.b)).l,n.a,n.b):tt(+n,+t,+e)};var ja=et.prototype=new G;ja.brighter=function(n){return tt(this.h,this.c,Math.min(100,this.l+Ha*(arguments.length?n:1)))},ja.darker=function(n){return tt(this.h,this.c,Math.max(0,this.l-Ha*(arguments.length?n:1)))},ja.rgb=function(){return rt(this.h,this.c,this.l).rgb()},Xo.lab=function(n,t,e){return 1===arguments.length?n instanceof it?ut(n.l,n.a,n.b):n instanceof et?rt(n.l,n.c,n.h):yt((n=Xo.rgb(n)).r,n.g,n.b):ut(+n,+t,+e)};var Ha=18,Fa=.95047,Oa=1,Ya=1.08883,Ia=it.prototype=new G;Ia.brighter=function(n){return ut(Math.min(100,this.l+Ha*(arguments.length?n:1)),this.a,this.b)},Ia.darker=function(n){return ut(Math.max(0,this.l-Ha*(arguments.length?n:1)),this.a,this.b)},Ia.rgb=function(){return ot(this.l,this.a,this.b)},Xo.rgb=function(n,t,e){return 1===arguments.length?n instanceof pt?gt(n.r,n.g,n.b):dt(""+n,gt,nt):gt(~~n,~~t,~~e)};var Za=pt.prototype=new G;Za.brighter=function(n){n=Math.pow(.7,arguments.length?n:1);var t=this.r,e=this.g,r=this.b,u=30;return t||e||r?(t&&u>t&&(t=u),e&&u>e&&(e=u),r&&u>r&&(r=u),gt(Math.min(255,~~(t/n)),Math.min(255,~~(e/n)),Math.min(255,~~(r/n)))):gt(u,u,u)},Za.darker=function(n){return n=Math.pow(.7,arguments.length?n:1),gt(~~(n*this.r),~~(n*this.g),~~(n*this.b))},Za.hsl=function(){return mt(this.r,this.g,this.b)},Za.toString=function(){return"#"+vt(this.r)+vt(this.g)+vt(this.b)};var Va=Xo.map({aliceblue:15792383,antiquewhite:16444375,aqua:65535,aquamarine:8388564,azure:15794175,beige:16119260,bisque:16770244,black:0,blanchedalmond:16772045,blue:255,blueviolet:9055202,brown:10824234,burlywood:14596231,cadetblue:6266528,chartreuse:8388352,chocolate:13789470,coral:16744272,cornflowerblue:6591981,cornsilk:16775388,crimson:14423100,cyan:65535,darkblue:139,darkcyan:35723,darkgoldenrod:12092939,darkgray:11119017,darkgreen:25600,darkgrey:11119017,darkkhaki:12433259,darkmagenta:9109643,darkolivegreen:5597999,darkorange:16747520,darkorchid:10040012,darkred:9109504,darksalmon:15308410,darkseagreen:9419919,darkslateblue:4734347,darkslategray:3100495,darkslategrey:3100495,darkturquoise:52945,darkviolet:9699539,deeppink:16716947,deepskyblue:49151,dimgray:6908265,dimgrey:6908265,dodgerblue:2003199,firebrick:11674146,floralwhite:16775920,forestgreen:2263842,fuchsia:16711935,gainsboro:14474460,ghostwhite:16316671,gold:16766720,goldenrod:14329120,gray:8421504,green:32768,greenyellow:11403055,grey:8421504,honeydew:15794160,hotpink:16738740,indianred:13458524,indigo:4915330,ivory:16777200,khaki:15787660,lavender:15132410,lavenderblush:16773365,lawngreen:8190976,lemonchiffon:16775885,lightblue:11393254,lightcoral:15761536,lightcyan:14745599,lightgoldenrodyellow:16448210,lightgray:13882323,lightgreen:9498256,lightgrey:13882323,lightpink:16758465,lightsalmon:16752762,lightseagreen:2142890,lightskyblue:8900346,lightslategray:7833753,lightslategrey:7833753,lightsteelblue:11584734,lightyellow:16777184,lime:65280,limegreen:3329330,linen:16445670,magenta:16711935,maroon:8388608,mediumaquamarine:6737322,mediumblue:205,mediumorchid:12211667,mediumpurple:9662683,mediumseagreen:3978097,mediumslateblue:8087790,mediumspringgreen:64154,mediumturquoise:4772300,mediumvioletred:13047173,midnightblue:1644912,mintcream:16121850,mistyrose:16770273,moccasin:16770229,navajowhite:16768685,navy:128,oldlace:16643558,olive:8421376,olivedrab:7048739,orange:16753920,orangered:16729344,orchid:14315734,palegoldenrod:15657130,palegreen:10025880,paleturquoise:11529966,palevioletred:14381203,papayawhip:16773077,peachpuff:16767673,peru:13468991,pink:16761035,plum:14524637,powderblue:11591910,purple:8388736,red:16711680,rosybrown:12357519,royalblue:4286945,saddlebrown:9127187,salmon:16416882,sandybrown:16032864,seagreen:3050327,seashell:16774638,sienna:10506797,silver:12632256,skyblue:8900331,slateblue:6970061,slategray:7372944,slategrey:7372944,snow:16775930,springgreen:65407,steelblue:4620980,tan:13808780,teal:32896,thistle:14204888,tomato:16737095,turquoise:4251856,violet:15631086,wheat:16113331,white:16777215,whitesmoke:16119285,yellow:16776960,yellowgreen:10145074});Va.forEach(function(n,t){Va.set(n,ft(t))}),Xo.functor=_t,Xo.xhr=wt(bt),Xo.dsv=function(n,t){function e(n,e,i){arguments.length<3&&(i=e,e=null);var o=St(n,t,null==e?r:u(e),i);return o.row=function(n){return arguments.length?o.response(null==(e=n)?r:u(n)):e},o}function r(n){return e.parse(n.responseText)}function u(n){return function(t){return e.parse(t.responseText,n)}}function i(t){return t.map(o).join(n)}function o(n){return a.test(n)?'"'+n.replace(/\"/g,'""')+'"':n}var a=new RegExp('["'+n+"\n]"),c=n.charCodeAt(0);return e.parse=function(n,t){var r;return e.parseRows(n,function(n,e){if(r)return r(n,e-1);var u=new Function("d","return {"+n.map(function(n,t){return JSON.stringify(n)+": d["+t+"]"}).join(",")+"}");r=t?function(n,e){return t(u(n),e)}:u})},e.parseRows=function(n,t){function e(){if(l>=s)return o;if(u)return u=!1,i;var t=l;if(34===n.charCodeAt(t)){for(var e=t;e++l;){var r=n.charCodeAt(l++),a=1;if(10===r)u=!0;else if(13===r)u=!0,10===n.charCodeAt(l)&&(++l,++a);else if(r!==c)continue;return n.substring(t,l-a)}return n.substring(t)}for(var r,u,i={},o={},a=[],s=n.length,l=0,f=0;(r=e())!==o;){for(var h=[];r!==i&&r!==o;)h.push(r),r=e();(!t||(h=t(h,f++)))&&a.push(h)}return a},e.format=function(t){if(Array.isArray(t[0]))return e.formatRows(t);var r=new l,u=[];return t.forEach(function(n){for(var t in n)r.has(t)||u.push(r.add(t))}),[u.map(o).join(n)].concat(t.map(function(t){return u.map(function(n){return o(t[n])}).join(n)})).join("\n")},e.formatRows=function(n){return n.map(i).join("\n")},e},Xo.csv=Xo.dsv(",","text/csv"),Xo.tsv=Xo.dsv(" ","text/tab-separated-values");var Xa,$a,Ba,Wa,Ja,Ga=Go[h(Go,"requestAnimationFrame")]||function(n){setTimeout(n,17)};Xo.timer=function(n,t,e){var r=arguments.length;2>r&&(t=0),3>r&&(e=Date.now());var u=e+t,i={c:n,t:u,f:!1,n:null};$a?$a.n=i:Xa=i,$a=i,Ba||(Wa=clearTimeout(Wa),Ba=1,Ga(Et))},Xo.timer.flush=function(){At(),Ct()},Xo.round=function(n,t){return t?Math.round(n*(t=Math.pow(10,t)))/t:Math.round(n)};var Ka=["y","z","a","f","p","n","\xb5","m","","k","M","G","T","P","E","Z","Y"].map(Lt);Xo.formatPrefix=function(n,t){var e=0;return n&&(0>n&&(n*=-1),t&&(n=Xo.round(n,Nt(n,t))),e=1+Math.floor(1e-12+Math.log(n)/Math.LN10),e=Math.max(-24,Math.min(24,3*Math.floor((0>=e?e+1:e-1)/3)))),Ka[8+e/3]};var Qa=/(?:([^{])?([<>=^]))?([+\- ])?([$#])?(0)?(\d+)?(,)?(\.-?\d+)?([a-z%])?/i,nc=Xo.map({b:function(n){return n.toString(2)},c:function(n){return String.fromCharCode(n)},o:function(n){return n.toString(8)},x:function(n){return n.toString(16)},X:function(n){return n.toString(16).toUpperCase()},g:function(n,t){return n.toPrecision(t)},e:function(n,t){return n.toExponential(t)},f:function(n,t){return n.toFixed(t)},r:function(n,t){return(n=Xo.round(n,Nt(n,t))).toFixed(Math.max(0,Math.min(20,Nt(n*(1+1e-15),t))))}}),tc=Xo.time={},ec=Date;Tt.prototype={getDate:function(){return this._.getUTCDate()},getDay:function(){return this._.getUTCDay()},getFullYear:function(){return this._.getUTCFullYear()},getHours:function(){return this._.getUTCHours()},getMilliseconds:function(){return this._.getUTCMilliseconds()},getMinutes:function(){return this._.getUTCMinutes()},getMonth:function(){return this._.getUTCMonth()},getSeconds:function(){return this._.getUTCSeconds()},getTime:function(){return this._.getTime()},getTimezoneOffset:function(){return 0},valueOf:function(){return this._.valueOf()},setDate:function(){rc.setUTCDate.apply(this._,arguments)},setDay:function(){rc.setUTCDay.apply(this._,arguments)},setFullYear:function(){rc.setUTCFullYear.apply(this._,arguments)},setHours:function(){rc.setUTCHours.apply(this._,arguments)},setMilliseconds:function(){rc.setUTCMilliseconds.apply(this._,arguments)},setMinutes:function(){rc.setUTCMinutes.apply(this._,arguments)},setMonth:function(){rc.setUTCMonth.apply(this._,arguments)},setSeconds:function(){rc.setUTCSeconds.apply(this._,arguments)},setTime:function(){rc.setTime.apply(this._,arguments)}};var rc=Date.prototype;tc.year=Rt(function(n){return n=tc.day(n),n.setMonth(0,1),n},function(n,t){n.setFullYear(n.getFullYear()+t)},function(n){return n.getFullYear()}),tc.years=tc.year.range,tc.years.utc=tc.year.utc.range,tc.day=Rt(function(n){var t=new ec(2e3,0);return t.setFullYear(n.getFullYear(),n.getMonth(),n.getDate()),t},function(n,t){n.setDate(n.getDate()+t)},function(n){return n.getDate()-1}),tc.days=tc.day.range,tc.days.utc=tc.day.utc.range,tc.dayOfYear=function(n){var t=tc.year(n);return Math.floor((n-t-6e4*(n.getTimezoneOffset()-t.getTimezoneOffset()))/864e5)},["sunday","monday","tuesday","wednesday","thursday","friday","saturday"].forEach(function(n,t){t=7-t;var e=tc[n]=Rt(function(n){return(n=tc.day(n)).setDate(n.getDate()-(n.getDay()+t)%7),n},function(n,t){n.setDate(n.getDate()+7*Math.floor(t))},function(n){var e=tc.year(n).getDay();return Math.floor((tc.dayOfYear(n)+(e+t)%7)/7)-(e!==t)});tc[n+"s"]=e.range,tc[n+"s"].utc=e.utc.range,tc[n+"OfYear"]=function(n){var e=tc.year(n).getDay();return Math.floor((tc.dayOfYear(n)+(e+t)%7)/7)}}),tc.week=tc.sunday,tc.weeks=tc.sunday.range,tc.weeks.utc=tc.sunday.utc.range,tc.weekOfYear=tc.sundayOfYear;var uc={"-":"",_:" ",0:"0"},ic=/^\s*\d+/,oc=/^%/;Xo.locale=function(n){return{numberFormat:zt(n),timeFormat:Pt(n)}};var ac=Xo.locale({decimal:".",thousands:",",grouping:[3],currency:["$",""],dateTime:"%a %b %e %X %Y",date:"%m/%d/%Y",time:"%H:%M:%S",periods:["AM","PM"],days:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],shortDays:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],shortMonths:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]});Xo.format=ac.numberFormat,Xo.geo={},re.prototype={s:0,t:0,add:function(n){ue(n,this.t,cc),ue(cc.s,this.s,this),this.s?this.t+=cc.t:this.s=cc.t},reset:function(){this.s=this.t=0},valueOf:function(){return this.s}};var cc=new re;Xo.geo.stream=function(n,t){n&&sc.hasOwnProperty(n.type)?sc[n.type](n,t):ie(n,t)};var sc={Feature:function(n,t){ie(n.geometry,t)},FeatureCollection:function(n,t){for(var e=n.features,r=-1,u=e.length;++rn?4*Sa+n:n,gc.lineStart=gc.lineEnd=gc.point=g}};Xo.geo.bounds=function(){function n(n,t){x.push(M=[l=n,h=n]),f>t&&(f=t),t>g&&(g=t)}function t(t,e){var r=se([t*Na,e*Na]);if(m){var u=fe(m,r),i=[u[1],-u[0],0],o=fe(i,u);pe(o),o=ve(o);var c=t-p,s=c>0?1:-1,v=o[0]*La*s,d=oa(c)>180;if(d^(v>s*p&&s*t>v)){var y=o[1]*La;y>g&&(g=y)}else if(v=(v+360)%360-180,d^(v>s*p&&s*t>v)){var y=-o[1]*La;f>y&&(f=y)}else f>e&&(f=e),e>g&&(g=e);d?p>t?a(l,t)>a(l,h)&&(h=t):a(t,h)>a(l,h)&&(l=t):h>=l?(l>t&&(l=t),t>h&&(h=t)):t>p?a(l,t)>a(l,h)&&(h=t):a(t,h)>a(l,h)&&(l=t)}else n(t,e);m=r,p=t}function e(){_.point=t}function r(){M[0]=l,M[1]=h,_.point=n,m=null}function u(n,e){if(m){var r=n-p;y+=oa(r)>180?r+(r>0?360:-360):r}else v=n,d=e;gc.point(n,e),t(n,e)}function i(){gc.lineStart()}function o(){u(v,d),gc.lineEnd(),oa(y)>Aa&&(l=-(h=180)),M[0]=l,M[1]=h,m=null}function a(n,t){return(t-=n)<0?t+360:t}function c(n,t){return n[0]-t[0]}function s(n,t){return t[0]<=t[1]?t[0]<=n&&n<=t[1]:nhc?(l=-(h=180),f=-(g=90)):y>Aa?g=90:-Aa>y&&(f=-90),M[0]=l,M[1]=h -}};return function(n){g=h=-(l=f=1/0),x=[],Xo.geo.stream(n,_);var t=x.length;if(t){x.sort(c);for(var e,r=1,u=x[0],i=[u];t>r;++r)e=x[r],s(e[0],u)||s(e[1],u)?(a(u[0],e[1])>a(u[0],u[1])&&(u[1]=e[1]),a(e[0],u[1])>a(u[0],u[1])&&(u[0]=e[0])):i.push(u=e);for(var o,e,p=-1/0,t=i.length-1,r=0,u=i[t];t>=r;u=e,++r)e=i[r],(o=a(u[1],e[0]))>p&&(p=o,l=e[0],h=u[1])}return x=M=null,1/0===l||1/0===f?[[0/0,0/0],[0/0,0/0]]:[[l,f],[h,g]]}}(),Xo.geo.centroid=function(n){pc=vc=dc=mc=yc=xc=Mc=_c=bc=wc=Sc=0,Xo.geo.stream(n,kc);var t=bc,e=wc,r=Sc,u=t*t+e*e+r*r;return Ca>u&&(t=xc,e=Mc,r=_c,Aa>vc&&(t=dc,e=mc,r=yc),u=t*t+e*e+r*r,Ca>u)?[0/0,0/0]:[Math.atan2(e,t)*La,X(r/Math.sqrt(u))*La]};var pc,vc,dc,mc,yc,xc,Mc,_c,bc,wc,Sc,kc={sphere:g,point:me,lineStart:xe,lineEnd:Me,polygonStart:function(){kc.lineStart=_e},polygonEnd:function(){kc.lineStart=xe}},Ec=Ee(be,ze,Te,[-Sa,-Sa/2]),Ac=1e9;Xo.geo.clipExtent=function(){var n,t,e,r,u,i,o={stream:function(n){return u&&(u.valid=!1),u=i(n),u.valid=!0,u},extent:function(a){return arguments.length?(i=Pe(n=+a[0][0],t=+a[0][1],e=+a[1][0],r=+a[1][1]),u&&(u.valid=!1,u=null),o):[[n,t],[e,r]]}};return o.extent([[0,0],[960,500]])},(Xo.geo.conicEqualArea=function(){return je(He)}).raw=He,Xo.geo.albers=function(){return Xo.geo.conicEqualArea().rotate([96,0]).center([-.6,38.7]).parallels([29.5,45.5]).scale(1070)},Xo.geo.albersUsa=function(){function n(n){var i=n[0],o=n[1];return t=null,e(i,o),t||(r(i,o),t)||u(i,o),t}var t,e,r,u,i=Xo.geo.albers(),o=Xo.geo.conicEqualArea().rotate([154,0]).center([-2,58.5]).parallels([55,65]),a=Xo.geo.conicEqualArea().rotate([157,0]).center([-3,19.9]).parallels([8,18]),c={point:function(n,e){t=[n,e]}};return n.invert=function(n){var t=i.scale(),e=i.translate(),r=(n[0]-e[0])/t,u=(n[1]-e[1])/t;return(u>=.12&&.234>u&&r>=-.425&&-.214>r?o:u>=.166&&.234>u&&r>=-.214&&-.115>r?a:i).invert(n)},n.stream=function(n){var t=i.stream(n),e=o.stream(n),r=a.stream(n);return{point:function(n,u){t.point(n,u),e.point(n,u),r.point(n,u)},sphere:function(){t.sphere(),e.sphere(),r.sphere()},lineStart:function(){t.lineStart(),e.lineStart(),r.lineStart()},lineEnd:function(){t.lineEnd(),e.lineEnd(),r.lineEnd()},polygonStart:function(){t.polygonStart(),e.polygonStart(),r.polygonStart()},polygonEnd:function(){t.polygonEnd(),e.polygonEnd(),r.polygonEnd()}}},n.precision=function(t){return arguments.length?(i.precision(t),o.precision(t),a.precision(t),n):i.precision()},n.scale=function(t){return arguments.length?(i.scale(t),o.scale(.35*t),a.scale(t),n.translate(i.translate())):i.scale()},n.translate=function(t){if(!arguments.length)return i.translate();var s=i.scale(),l=+t[0],f=+t[1];return e=i.translate(t).clipExtent([[l-.455*s,f-.238*s],[l+.455*s,f+.238*s]]).stream(c).point,r=o.translate([l-.307*s,f+.201*s]).clipExtent([[l-.425*s+Aa,f+.12*s+Aa],[l-.214*s-Aa,f+.234*s-Aa]]).stream(c).point,u=a.translate([l-.205*s,f+.212*s]).clipExtent([[l-.214*s+Aa,f+.166*s+Aa],[l-.115*s-Aa,f+.234*s-Aa]]).stream(c).point,n},n.scale(1070)};var Cc,Nc,Lc,zc,qc,Tc,Rc={point:g,lineStart:g,lineEnd:g,polygonStart:function(){Nc=0,Rc.lineStart=Fe},polygonEnd:function(){Rc.lineStart=Rc.lineEnd=Rc.point=g,Cc+=oa(Nc/2)}},Dc={point:Oe,lineStart:g,lineEnd:g,polygonStart:g,polygonEnd:g},Pc={point:Ze,lineStart:Ve,lineEnd:Xe,polygonStart:function(){Pc.lineStart=$e},polygonEnd:function(){Pc.point=Ze,Pc.lineStart=Ve,Pc.lineEnd=Xe}};Xo.geo.path=function(){function n(n){return n&&("function"==typeof a&&i.pointRadius(+a.apply(this,arguments)),o&&o.valid||(o=u(i)),Xo.geo.stream(n,o)),i.result()}function t(){return o=null,n}var e,r,u,i,o,a=4.5;return n.area=function(n){return Cc=0,Xo.geo.stream(n,u(Rc)),Cc},n.centroid=function(n){return dc=mc=yc=xc=Mc=_c=bc=wc=Sc=0,Xo.geo.stream(n,u(Pc)),Sc?[bc/Sc,wc/Sc]:_c?[xc/_c,Mc/_c]:yc?[dc/yc,mc/yc]:[0/0,0/0]},n.bounds=function(n){return qc=Tc=-(Lc=zc=1/0),Xo.geo.stream(n,u(Dc)),[[Lc,zc],[qc,Tc]]},n.projection=function(n){return arguments.length?(u=(e=n)?n.stream||Je(n):bt,t()):e},n.context=function(n){return arguments.length?(i=null==(r=n)?new Ye:new Be(n),"function"!=typeof a&&i.pointRadius(a),t()):r},n.pointRadius=function(t){return arguments.length?(a="function"==typeof t?t:(i.pointRadius(+t),+t),n):a},n.projection(Xo.geo.albersUsa()).context(null)},Xo.geo.transform=function(n){return{stream:function(t){var e=new Ge(t);for(var r in n)e[r]=n[r];return e}}},Ge.prototype={point:function(n,t){this.stream.point(n,t)},sphere:function(){this.stream.sphere()},lineStart:function(){this.stream.lineStart()},lineEnd:function(){this.stream.lineEnd()},polygonStart:function(){this.stream.polygonStart()},polygonEnd:function(){this.stream.polygonEnd()}},Xo.geo.projection=Qe,Xo.geo.projectionMutator=nr,(Xo.geo.equirectangular=function(){return Qe(er)}).raw=er.invert=er,Xo.geo.rotation=function(n){function t(t){return t=n(t[0]*Na,t[1]*Na),t[0]*=La,t[1]*=La,t}return n=ur(n[0]%360*Na,n[1]*Na,n.length>2?n[2]*Na:0),t.invert=function(t){return t=n.invert(t[0]*Na,t[1]*Na),t[0]*=La,t[1]*=La,t},t},rr.invert=er,Xo.geo.circle=function(){function n(){var n="function"==typeof r?r.apply(this,arguments):r,t=ur(-n[0]*Na,-n[1]*Na,0).invert,u=[];return e(null,null,1,{point:function(n,e){u.push(n=t(n,e)),n[0]*=La,n[1]*=La}}),{type:"Polygon",coordinates:[u]}}var t,e,r=[0,0],u=6;return n.origin=function(t){return arguments.length?(r=t,n):r},n.angle=function(r){return arguments.length?(e=cr((t=+r)*Na,u*Na),n):t},n.precision=function(r){return arguments.length?(e=cr(t*Na,(u=+r)*Na),n):u},n.angle(90)},Xo.geo.distance=function(n,t){var e,r=(t[0]-n[0])*Na,u=n[1]*Na,i=t[1]*Na,o=Math.sin(r),a=Math.cos(r),c=Math.sin(u),s=Math.cos(u),l=Math.sin(i),f=Math.cos(i);return Math.atan2(Math.sqrt((e=f*o)*e+(e=s*l-c*f*a)*e),c*l+s*f*a)},Xo.geo.graticule=function(){function n(){return{type:"MultiLineString",coordinates:t()}}function t(){return Xo.range(Math.ceil(i/d)*d,u,d).map(h).concat(Xo.range(Math.ceil(s/m)*m,c,m).map(g)).concat(Xo.range(Math.ceil(r/p)*p,e,p).filter(function(n){return oa(n%d)>Aa}).map(l)).concat(Xo.range(Math.ceil(a/v)*v,o,v).filter(function(n){return oa(n%m)>Aa}).map(f))}var e,r,u,i,o,a,c,s,l,f,h,g,p=10,v=p,d=90,m=360,y=2.5;return n.lines=function(){return t().map(function(n){return{type:"LineString",coordinates:n}})},n.outline=function(){return{type:"Polygon",coordinates:[h(i).concat(g(c).slice(1),h(u).reverse().slice(1),g(s).reverse().slice(1))]}},n.extent=function(t){return arguments.length?n.majorExtent(t).minorExtent(t):n.minorExtent()},n.majorExtent=function(t){return arguments.length?(i=+t[0][0],u=+t[1][0],s=+t[0][1],c=+t[1][1],i>u&&(t=i,i=u,u=t),s>c&&(t=s,s=c,c=t),n.precision(y)):[[i,s],[u,c]]},n.minorExtent=function(t){return arguments.length?(r=+t[0][0],e=+t[1][0],a=+t[0][1],o=+t[1][1],r>e&&(t=r,r=e,e=t),a>o&&(t=a,a=o,o=t),n.precision(y)):[[r,a],[e,o]]},n.step=function(t){return arguments.length?n.majorStep(t).minorStep(t):n.minorStep()},n.majorStep=function(t){return arguments.length?(d=+t[0],m=+t[1],n):[d,m]},n.minorStep=function(t){return arguments.length?(p=+t[0],v=+t[1],n):[p,v]},n.precision=function(t){return arguments.length?(y=+t,l=lr(a,o,90),f=fr(r,e,y),h=lr(s,c,90),g=fr(i,u,y),n):y},n.majorExtent([[-180,-90+Aa],[180,90-Aa]]).minorExtent([[-180,-80-Aa],[180,80+Aa]])},Xo.geo.greatArc=function(){function n(){return{type:"LineString",coordinates:[t||r.apply(this,arguments),e||u.apply(this,arguments)]}}var t,e,r=hr,u=gr;return n.distance=function(){return Xo.geo.distance(t||r.apply(this,arguments),e||u.apply(this,arguments))},n.source=function(e){return arguments.length?(r=e,t="function"==typeof e?null:e,n):r},n.target=function(t){return arguments.length?(u=t,e="function"==typeof t?null:t,n):u},n.precision=function(){return arguments.length?n:0},n},Xo.geo.interpolate=function(n,t){return pr(n[0]*Na,n[1]*Na,t[0]*Na,t[1]*Na)},Xo.geo.length=function(n){return Uc=0,Xo.geo.stream(n,jc),Uc};var Uc,jc={sphere:g,point:g,lineStart:vr,lineEnd:g,polygonStart:g,polygonEnd:g},Hc=dr(function(n){return Math.sqrt(2/(1+n))},function(n){return 2*Math.asin(n/2)});(Xo.geo.azimuthalEqualArea=function(){return Qe(Hc)}).raw=Hc;var Fc=dr(function(n){var t=Math.acos(n);return t&&t/Math.sin(t)},bt);(Xo.geo.azimuthalEquidistant=function(){return Qe(Fc)}).raw=Fc,(Xo.geo.conicConformal=function(){return je(mr)}).raw=mr,(Xo.geo.conicEquidistant=function(){return je(yr)}).raw=yr;var Oc=dr(function(n){return 1/n},Math.atan);(Xo.geo.gnomonic=function(){return Qe(Oc)}).raw=Oc,xr.invert=function(n,t){return[n,2*Math.atan(Math.exp(t))-Ea]},(Xo.geo.mercator=function(){return Mr(xr)}).raw=xr;var Yc=dr(function(){return 1},Math.asin);(Xo.geo.orthographic=function(){return Qe(Yc)}).raw=Yc;var Ic=dr(function(n){return 1/(1+n)},function(n){return 2*Math.atan(n)});(Xo.geo.stereographic=function(){return Qe(Ic)}).raw=Ic,_r.invert=function(n,t){return[-t,2*Math.atan(Math.exp(n))-Ea]},(Xo.geo.transverseMercator=function(){var n=Mr(_r),t=n.center,e=n.rotate;return n.center=function(n){return n?t([-n[1],n[0]]):(n=t(),[-n[1],n[0]])},n.rotate=function(n){return n?e([n[0],n[1],n.length>2?n[2]+90:90]):(n=e(),[n[0],n[1],n[2]-90])},n.rotate([0,0])}).raw=_r,Xo.geom={},Xo.geom.hull=function(n){function t(n){if(n.length<3)return[];var t,u=_t(e),i=_t(r),o=n.length,a=[],c=[];for(t=0;o>t;t++)a.push([+u.call(this,n[t],t),+i.call(this,n[t],t),t]);for(a.sort(kr),t=0;o>t;t++)c.push([a[t][0],-a[t][1]]);var s=Sr(a),l=Sr(c),f=l[0]===s[0],h=l[l.length-1]===s[s.length-1],g=[];for(t=s.length-1;t>=0;--t)g.push(n[a[s[t]][2]]);for(t=+f;t=r&&s.x<=i&&s.y>=u&&s.y<=o?[[r,o],[i,o],[i,u],[r,u]]:[];l.point=n[a]}),t}function e(n){return n.map(function(n,t){return{x:Math.round(i(n,t)/Aa)*Aa,y:Math.round(o(n,t)/Aa)*Aa,i:t}})}var r=br,u=wr,i=r,o=u,a=Kc;return n?t(n):(t.links=function(n){return nu(e(n)).edges.filter(function(n){return n.l&&n.r}).map(function(t){return{source:n[t.l.i],target:n[t.r.i]}})},t.triangles=function(n){var t=[];return nu(e(n)).cells.forEach(function(e,r){for(var u,i,o=e.site,a=e.edges.sort(jr),c=-1,s=a.length,l=a[s-1].edge,f=l.l===o?l.r:l.l;++c=s,h=r>=l,g=(h<<1)+f;n.leaf=!1,n=n.nodes[g]||(n.nodes[g]=iu()),f?u=s:a=s,h?o=l:c=l,i(n,t,e,r,u,o,a,c)}var l,f,h,g,p,v,d,m,y,x=_t(a),M=_t(c);if(null!=t)v=t,d=e,m=r,y=u;else if(m=y=-(v=d=1/0),f=[],h=[],p=n.length,o)for(g=0;p>g;++g)l=n[g],l.xm&&(m=l.x),l.y>y&&(y=l.y),f.push(l.x),h.push(l.y);else for(g=0;p>g;++g){var _=+x(l=n[g],g),b=+M(l,g);v>_&&(v=_),d>b&&(d=b),_>m&&(m=_),b>y&&(y=b),f.push(_),h.push(b)}var w=m-v,S=y-d;w>S?y=d+w:m=v+S;var k=iu();if(k.add=function(n){i(k,n,+x(n,++g),+M(n,g),v,d,m,y)},k.visit=function(n){ou(n,k,v,d,m,y)},g=-1,null==t){for(;++g=0?n.substring(0,t):n,r=t>=0?n.substring(t+1):"in";return e=ts.get(e)||ns,r=es.get(r)||bt,gu(r(e.apply(null,$o.call(arguments,1))))},Xo.interpolateHcl=Eu,Xo.interpolateHsl=Au,Xo.interpolateLab=Cu,Xo.interpolateRound=Nu,Xo.transform=function(n){var t=Wo.createElementNS(Xo.ns.prefix.svg,"g");return(Xo.transform=function(n){if(null!=n){t.setAttribute("transform",n);var e=t.transform.baseVal.consolidate()}return new Lu(e?e.matrix:rs)})(n)},Lu.prototype.toString=function(){return"translate("+this.translate+")rotate("+this.rotate+")skewX("+this.skew+")scale("+this.scale+")"};var rs={a:1,b:0,c:0,d:1,e:0,f:0};Xo.interpolateTransform=Ru,Xo.layout={},Xo.layout.bundle=function(){return function(n){for(var t=[],e=-1,r=n.length;++ea*a/d){if(p>c){var s=t.charge/c;n.px-=i*s,n.py-=o*s}return!0}if(t.point&&c&&p>c){var s=t.pointCharge/c;n.px-=i*s,n.py-=o*s}}return!t.charge}}function t(n){n.px=Xo.event.x,n.py=Xo.event.y,a.resume()}var e,r,u,i,o,a={},c=Xo.dispatch("start","tick","end"),s=[1,1],l=.9,f=us,h=is,g=-30,p=os,v=.1,d=.64,m=[],y=[];return a.tick=function(){if((r*=.99)<.005)return c.end({type:"end",alpha:r=0}),!0;var t,e,a,f,h,p,d,x,M,_=m.length,b=y.length;for(e=0;b>e;++e)a=y[e],f=a.source,h=a.target,x=h.x-f.x,M=h.y-f.y,(p=x*x+M*M)&&(p=r*i[e]*((p=Math.sqrt(p))-u[e])/p,x*=p,M*=p,h.x-=x*(d=f.weight/(h.weight+f.weight)),h.y-=M*d,f.x+=x*(d=1-d),f.y+=M*d);if((d=r*v)&&(x=s[0]/2,M=s[1]/2,e=-1,d))for(;++e<_;)a=m[e],a.x+=(x-a.x)*d,a.y+=(M-a.y)*d;if(g)for(Zu(t=Xo.geom.quadtree(m),r,o),e=-1;++e<_;)(a=m[e]).fixed||t.visit(n(a));for(e=-1;++e<_;)a=m[e],a.fixed?(a.x=a.px,a.y=a.py):(a.x-=(a.px-(a.px=a.x))*l,a.y-=(a.py-(a.py=a.y))*l);c.tick({type:"tick",alpha:r})},a.nodes=function(n){return arguments.length?(m=n,a):m},a.links=function(n){return arguments.length?(y=n,a):y},a.size=function(n){return arguments.length?(s=n,a):s},a.linkDistance=function(n){return arguments.length?(f="function"==typeof n?n:+n,a):f},a.distance=a.linkDistance,a.linkStrength=function(n){return arguments.length?(h="function"==typeof n?n:+n,a):h},a.friction=function(n){return arguments.length?(l=+n,a):l},a.charge=function(n){return arguments.length?(g="function"==typeof n?n:+n,a):g},a.chargeDistance=function(n){return arguments.length?(p=n*n,a):Math.sqrt(p)},a.gravity=function(n){return arguments.length?(v=+n,a):v},a.theta=function(n){return arguments.length?(d=n*n,a):Math.sqrt(d)},a.alpha=function(n){return arguments.length?(n=+n,r?r=n>0?n:0:n>0&&(c.start({type:"start",alpha:r=n}),Xo.timer(a.tick)),a):r},a.start=function(){function n(n,r){if(!e){for(e=new Array(c),a=0;c>a;++a)e[a]=[];for(a=0;s>a;++a){var u=y[a];e[u.source.index].push(u.target),e[u.target.index].push(u.source)}}for(var i,o=e[t],a=-1,s=o.length;++at;++t)(r=m[t]).index=t,r.weight=0;for(t=0;l>t;++t)r=y[t],"number"==typeof r.source&&(r.source=m[r.source]),"number"==typeof r.target&&(r.target=m[r.target]),++r.source.weight,++r.target.weight;for(t=0;c>t;++t)r=m[t],isNaN(r.x)&&(r.x=n("x",p)),isNaN(r.y)&&(r.y=n("y",v)),isNaN(r.px)&&(r.px=r.x),isNaN(r.py)&&(r.py=r.y);if(u=[],"function"==typeof f)for(t=0;l>t;++t)u[t]=+f.call(this,y[t],t);else for(t=0;l>t;++t)u[t]=f;if(i=[],"function"==typeof h)for(t=0;l>t;++t)i[t]=+h.call(this,y[t],t);else for(t=0;l>t;++t)i[t]=h;if(o=[],"function"==typeof g)for(t=0;c>t;++t)o[t]=+g.call(this,m[t],t);else for(t=0;c>t;++t)o[t]=g;return a.resume()},a.resume=function(){return a.alpha(.1)},a.stop=function(){return a.alpha(0)},a.drag=function(){return e||(e=Xo.behavior.drag().origin(bt).on("dragstart.force",Fu).on("drag.force",t).on("dragend.force",Ou)),arguments.length?(this.on("mouseover.force",Yu).on("mouseout.force",Iu).call(e),void 0):e},Xo.rebind(a,c,"on")};var us=20,is=1,os=1/0;Xo.layout.hierarchy=function(){function n(t,o,a){var c=u.call(e,t,o);if(t.depth=o,a.push(t),c&&(s=c.length)){for(var s,l,f=-1,h=t.children=new Array(s),g=0,p=o+1;++fg;++g)for(u.call(n,s[0][g],p=v[g],l[0][g][1]),h=1;d>h;++h)u.call(n,s[h][g],p+=l[h-1][g][1],l[h][g][1]);return a}var t=bt,e=Qu,r=ni,u=Ku,i=Ju,o=Gu;return n.values=function(e){return arguments.length?(t=e,n):t},n.order=function(t){return arguments.length?(e="function"==typeof t?t:cs.get(t)||Qu,n):e},n.offset=function(t){return arguments.length?(r="function"==typeof t?t:ss.get(t)||ni,n):r},n.x=function(t){return arguments.length?(i=t,n):i},n.y=function(t){return arguments.length?(o=t,n):o},n.out=function(t){return arguments.length?(u=t,n):u},n};var cs=Xo.map({"inside-out":function(n){var t,e,r=n.length,u=n.map(ti),i=n.map(ei),o=Xo.range(r).sort(function(n,t){return u[n]-u[t]}),a=0,c=0,s=[],l=[];for(t=0;r>t;++t)e=o[t],c>a?(a+=i[e],s.push(e)):(c+=i[e],l.push(e));return l.reverse().concat(s)},reverse:function(n){return Xo.range(n.length).reverse()},"default":Qu}),ss=Xo.map({silhouette:function(n){var t,e,r,u=n.length,i=n[0].length,o=[],a=0,c=[];for(e=0;i>e;++e){for(t=0,r=0;u>t;t++)r+=n[t][e][1];r>a&&(a=r),o.push(r)}for(e=0;i>e;++e)c[e]=(a-o[e])/2;return c},wiggle:function(n){var t,e,r,u,i,o,a,c,s,l=n.length,f=n[0],h=f.length,g=[];for(g[0]=c=s=0,e=1;h>e;++e){for(t=0,u=0;l>t;++t)u+=n[t][e][1];for(t=0,i=0,a=f[e][0]-f[e-1][0];l>t;++t){for(r=0,o=(n[t][e][1]-n[t][e-1][1])/(2*a);t>r;++r)o+=(n[r][e][1]-n[r][e-1][1])/a;i+=o*n[t][e][1]}g[e]=c-=u?i/u*a:0,s>c&&(s=c)}for(e=0;h>e;++e)g[e]-=s;return g},expand:function(n){var t,e,r,u=n.length,i=n[0].length,o=1/u,a=[];for(e=0;i>e;++e){for(t=0,r=0;u>t;t++)r+=n[t][e][1];if(r)for(t=0;u>t;t++)n[t][e][1]/=r;else for(t=0;u>t;t++)n[t][e][1]=o}for(e=0;i>e;++e)a[e]=0;return a},zero:ni});Xo.layout.histogram=function(){function n(n,i){for(var o,a,c=[],s=n.map(e,this),l=r.call(this,s,i),f=u.call(this,l,s,i),i=-1,h=s.length,g=f.length-1,p=t?1:1/h;++i0)for(i=-1;++i=l[0]&&a<=l[1]&&(o=c[Xo.bisect(f,a,1,g)-1],o.y+=p,o.push(n[i]));return c}var t=!0,e=Number,r=oi,u=ui;return n.value=function(t){return arguments.length?(e=t,n):e},n.range=function(t){return arguments.length?(r=_t(t),n):r},n.bins=function(t){return arguments.length?(u="number"==typeof t?function(n){return ii(n,t)}:_t(t),n):u},n.frequency=function(e){return arguments.length?(t=!!e,n):t},n},Xo.layout.tree=function(){function n(n,i){function o(n,t){var r=n.children,u=n._tree;if(r&&(i=r.length)){for(var i,a,s,l=r[0],f=l,h=-1;++h0&&(di(mi(a,n,r),n,u),s+=u,l+=u),f+=a._tree.mod,s+=i._tree.mod,h+=c._tree.mod,l+=o._tree.mod;a&&!si(o)&&(o._tree.thread=a,o._tree.mod+=f-l),i&&!ci(c)&&(c._tree.thread=i,c._tree.mod+=s-h,r=n)}return r}var s=t.call(this,n,i),l=s[0];pi(l,function(n,t){n._tree={ancestor:n,prelim:0,mod:0,change:0,shift:0,number:t?t._tree.number+1:0}}),o(l),a(l,-l._tree.prelim);var f=li(l,hi),h=li(l,fi),g=li(l,gi),p=f.x-e(f,h)/2,v=h.x+e(h,f)/2,d=g.depth||1;return pi(l,u?function(n){n.x*=r[0],n.y=n.depth*r[1],delete n._tree}:function(n){n.x=(n.x-p)/(v-p)*r[0],n.y=n.depth/d*r[1],delete n._tree}),s}var t=Xo.layout.hierarchy().sort(null).value(null),e=ai,r=[1,1],u=!1;return n.separation=function(t){return arguments.length?(e=t,n):e},n.size=function(t){return arguments.length?(u=null==(r=t),n):u?null:r},n.nodeSize=function(t){return arguments.length?(u=null!=(r=t),n):u?r:null},Vu(n,t)},Xo.layout.pack=function(){function n(n,i){var o=e.call(this,n,i),a=o[0],c=u[0],s=u[1],l=null==t?Math.sqrt:"function"==typeof t?t:function(){return t};if(a.x=a.y=0,pi(a,function(n){n.r=+l(n.value)}),pi(a,bi),r){var f=r*(t?1:Math.max(2*a.r/c,2*a.r/s))/2;pi(a,function(n){n.r+=f}),pi(a,bi),pi(a,function(n){n.r-=f})}return ki(a,c/2,s/2,t?1:1/Math.max(2*a.r/c,2*a.r/s)),o}var t,e=Xo.layout.hierarchy().sort(yi),r=0,u=[1,1];return n.size=function(t){return arguments.length?(u=t,n):u},n.radius=function(e){return arguments.length?(t=null==e||"function"==typeof e?e:+e,n):t},n.padding=function(t){return arguments.length?(r=+t,n):r},Vu(n,e)},Xo.layout.cluster=function(){function n(n,i){var o,a=t.call(this,n,i),c=a[0],s=0;pi(c,function(n){var t=n.children;t&&t.length?(n.x=Ci(t),n.y=Ai(t)):(n.x=o?s+=e(n,o):0,n.y=0,o=n)});var l=Ni(c),f=Li(c),h=l.x-e(l,f)/2,g=f.x+e(f,l)/2;return pi(c,u?function(n){n.x=(n.x-c.x)*r[0],n.y=(c.y-n.y)*r[1]}:function(n){n.x=(n.x-h)/(g-h)*r[0],n.y=(1-(c.y?n.y/c.y:1))*r[1]}),a}var t=Xo.layout.hierarchy().sort(null).value(null),e=ai,r=[1,1],u=!1;return n.separation=function(t){return arguments.length?(e=t,n):e},n.size=function(t){return arguments.length?(u=null==(r=t),n):u?null:r},n.nodeSize=function(t){return arguments.length?(u=null!=(r=t),n):u?r:null},Vu(n,t)},Xo.layout.treemap=function(){function n(n,t){for(var e,r,u=-1,i=n.length;++ut?0:t),e.area=isNaN(r)||0>=r?0:r}function t(e){var i=e.children;if(i&&i.length){var o,a,c,s=f(e),l=[],h=i.slice(),p=1/0,v="slice"===g?s.dx:"dice"===g?s.dy:"slice-dice"===g?1&e.depth?s.dy:s.dx:Math.min(s.dx,s.dy);for(n(h,s.dx*s.dy/e.value),l.area=0;(c=h.length)>0;)l.push(o=h[c-1]),l.area+=o.area,"squarify"!==g||(a=r(l,v))<=p?(h.pop(),p=a):(l.area-=l.pop().area,u(l,v,s,!1),v=Math.min(s.dx,s.dy),l.length=l.area=0,p=1/0);l.length&&(u(l,v,s,!0),l.length=l.area=0),i.forEach(t)}}function e(t){var r=t.children;if(r&&r.length){var i,o=f(t),a=r.slice(),c=[];for(n(a,o.dx*o.dy/t.value),c.area=0;i=a.pop();)c.push(i),c.area+=i.area,null!=i.z&&(u(c,i.z?o.dx:o.dy,o,!a.length),c.length=c.area=0);r.forEach(e)}}function r(n,t){for(var e,r=n.area,u=0,i=1/0,o=-1,a=n.length;++oe&&(i=e),e>u&&(u=e));return r*=r,t*=t,r?Math.max(t*u*p/r,r/(t*i*p)):1/0}function u(n,t,e,r){var u,i=-1,o=n.length,a=e.x,s=e.y,l=t?c(n.area/t):0;if(t==e.dx){for((r||l>e.dy)&&(l=e.dy);++ie.dx)&&(l=e.dx);++ie&&(t=1),1>e&&(n=0),function(){var e,r,u;do e=2*Math.random()-1,r=2*Math.random()-1,u=e*e+r*r;while(!u||u>1);return n+t*e*Math.sqrt(-2*Math.log(u)/u)}},logNormal:function(){var n=Xo.random.normal.apply(Xo,arguments);return function(){return Math.exp(n())}},bates:function(n){var t=Xo.random.irwinHall(n);return function(){return t()/n}},irwinHall:function(n){return function(){for(var t=0,e=0;n>e;e++)t+=Math.random();return t}}},Xo.scale={};var ls={floor:bt,ceil:bt};Xo.scale.linear=function(){return Hi([0,1],[0,1],fu,!1)};var fs={s:1,g:1,p:1,r:1,e:1};Xo.scale.log=function(){return $i(Xo.scale.linear().domain([0,1]),10,!0,[1,10])};var hs=Xo.format(".0e"),gs={floor:function(n){return-Math.ceil(-n)},ceil:function(n){return-Math.floor(-n)}};Xo.scale.pow=function(){return Bi(Xo.scale.linear(),1,[0,1])},Xo.scale.sqrt=function(){return Xo.scale.pow().exponent(.5)},Xo.scale.ordinal=function(){return Ji([],{t:"range",a:[[]]})},Xo.scale.category10=function(){return Xo.scale.ordinal().range(ps)},Xo.scale.category20=function(){return Xo.scale.ordinal().range(vs)},Xo.scale.category20b=function(){return Xo.scale.ordinal().range(ds)},Xo.scale.category20c=function(){return Xo.scale.ordinal().range(ms)};var ps=[2062260,16744206,2924588,14034728,9725885,9197131,14907330,8355711,12369186,1556175].map(ht),vs=[2062260,11454440,16744206,16759672,2924588,10018698,14034728,16750742,9725885,12955861,9197131,12885140,14907330,16234194,8355711,13092807,12369186,14408589,1556175,10410725].map(ht),ds=[3750777,5395619,7040719,10264286,6519097,9216594,11915115,13556636,9202993,12426809,15186514,15190932,8666169,11356490,14049643,15177372,8077683,10834324,13528509,14589654].map(ht),ms=[3244733,7057110,10406625,13032431,15095053,16616764,16625259,16634018,3253076,7652470,10607003,13101504,7695281,10394312,12369372,14342891,6513507,9868950,12434877,14277081].map(ht);Xo.scale.quantile=function(){return Gi([],[]) -},Xo.scale.quantize=function(){return Ki(0,1,[0,1])},Xo.scale.threshold=function(){return Qi([.5],[0,1])},Xo.scale.identity=function(){return no([0,1])},Xo.svg={},Xo.svg.arc=function(){function n(){var n=t.apply(this,arguments),i=e.apply(this,arguments),o=r.apply(this,arguments)+ys,a=u.apply(this,arguments)+ys,c=(o>a&&(c=o,o=a,a=c),a-o),s=Sa>c?"0":"1",l=Math.cos(o),f=Math.sin(o),h=Math.cos(a),g=Math.sin(a);return c>=xs?n?"M0,"+i+"A"+i+","+i+" 0 1,1 0,"+-i+"A"+i+","+i+" 0 1,1 0,"+i+"M0,"+n+"A"+n+","+n+" 0 1,0 0,"+-n+"A"+n+","+n+" 0 1,0 0,"+n+"Z":"M0,"+i+"A"+i+","+i+" 0 1,1 0,"+-i+"A"+i+","+i+" 0 1,1 0,"+i+"Z":n?"M"+i*l+","+i*f+"A"+i+","+i+" 0 "+s+",1 "+i*h+","+i*g+"L"+n*h+","+n*g+"A"+n+","+n+" 0 "+s+",0 "+n*l+","+n*f+"Z":"M"+i*l+","+i*f+"A"+i+","+i+" 0 "+s+",1 "+i*h+","+i*g+"L0,0"+"Z"}var t=to,e=eo,r=ro,u=uo;return n.innerRadius=function(e){return arguments.length?(t=_t(e),n):t},n.outerRadius=function(t){return arguments.length?(e=_t(t),n):e},n.startAngle=function(t){return arguments.length?(r=_t(t),n):r},n.endAngle=function(t){return arguments.length?(u=_t(t),n):u},n.centroid=function(){var n=(t.apply(this,arguments)+e.apply(this,arguments))/2,i=(r.apply(this,arguments)+u.apply(this,arguments))/2+ys;return[Math.cos(i)*n,Math.sin(i)*n]},n};var ys=-Ea,xs=ka-Aa;Xo.svg.line=function(){return io(bt)};var Ms=Xo.map({linear:oo,"linear-closed":ao,step:co,"step-before":so,"step-after":lo,basis:mo,"basis-open":yo,"basis-closed":xo,bundle:Mo,cardinal:go,"cardinal-open":fo,"cardinal-closed":ho,monotone:Eo});Ms.forEach(function(n,t){t.key=n,t.closed=/-closed$/.test(n)});var _s=[0,2/3,1/3,0],bs=[0,1/3,2/3,0],ws=[0,1/6,2/3,1/6];Xo.svg.line.radial=function(){var n=io(Ao);return n.radius=n.x,delete n.x,n.angle=n.y,delete n.y,n},so.reverse=lo,lo.reverse=so,Xo.svg.area=function(){return Co(bt)},Xo.svg.area.radial=function(){var n=Co(Ao);return n.radius=n.x,delete n.x,n.innerRadius=n.x0,delete n.x0,n.outerRadius=n.x1,delete n.x1,n.angle=n.y,delete n.y,n.startAngle=n.y0,delete n.y0,n.endAngle=n.y1,delete n.y1,n},Xo.svg.chord=function(){function n(n,a){var c=t(this,i,n,a),s=t(this,o,n,a);return"M"+c.p0+r(c.r,c.p1,c.a1-c.a0)+(e(c,s)?u(c.r,c.p1,c.r,c.p0):u(c.r,c.p1,s.r,s.p0)+r(s.r,s.p1,s.a1-s.a0)+u(s.r,s.p1,c.r,c.p0))+"Z"}function t(n,t,e,r){var u=t.call(n,e,r),i=a.call(n,u,r),o=c.call(n,u,r)+ys,l=s.call(n,u,r)+ys;return{r:i,a0:o,a1:l,p0:[i*Math.cos(o),i*Math.sin(o)],p1:[i*Math.cos(l),i*Math.sin(l)]}}function e(n,t){return n.a0==t.a0&&n.a1==t.a1}function r(n,t,e){return"A"+n+","+n+" 0 "+ +(e>Sa)+",1 "+t}function u(n,t,e,r){return"Q 0,0 "+r}var i=hr,o=gr,a=No,c=ro,s=uo;return n.radius=function(t){return arguments.length?(a=_t(t),n):a},n.source=function(t){return arguments.length?(i=_t(t),n):i},n.target=function(t){return arguments.length?(o=_t(t),n):o},n.startAngle=function(t){return arguments.length?(c=_t(t),n):c},n.endAngle=function(t){return arguments.length?(s=_t(t),n):s},n},Xo.svg.diagonal=function(){function n(n,u){var i=t.call(this,n,u),o=e.call(this,n,u),a=(i.y+o.y)/2,c=[i,{x:i.x,y:a},{x:o.x,y:a},o];return c=c.map(r),"M"+c[0]+"C"+c[1]+" "+c[2]+" "+c[3]}var t=hr,e=gr,r=Lo;return n.source=function(e){return arguments.length?(t=_t(e),n):t},n.target=function(t){return arguments.length?(e=_t(t),n):e},n.projection=function(t){return arguments.length?(r=t,n):r},n},Xo.svg.diagonal.radial=function(){var n=Xo.svg.diagonal(),t=Lo,e=n.projection;return n.projection=function(n){return arguments.length?e(zo(t=n)):t},n},Xo.svg.symbol=function(){function n(n,r){return(Ss.get(t.call(this,n,r))||Ro)(e.call(this,n,r))}var t=To,e=qo;return n.type=function(e){return arguments.length?(t=_t(e),n):t},n.size=function(t){return arguments.length?(e=_t(t),n):e},n};var Ss=Xo.map({circle:Ro,cross:function(n){var t=Math.sqrt(n/5)/2;return"M"+-3*t+","+-t+"H"+-t+"V"+-3*t+"H"+t+"V"+-t+"H"+3*t+"V"+t+"H"+t+"V"+3*t+"H"+-t+"V"+t+"H"+-3*t+"Z"},diamond:function(n){var t=Math.sqrt(n/(2*Cs)),e=t*Cs;return"M0,"+-t+"L"+e+",0"+" 0,"+t+" "+-e+",0"+"Z"},square:function(n){var t=Math.sqrt(n)/2;return"M"+-t+","+-t+"L"+t+","+-t+" "+t+","+t+" "+-t+","+t+"Z"},"triangle-down":function(n){var t=Math.sqrt(n/As),e=t*As/2;return"M0,"+e+"L"+t+","+-e+" "+-t+","+-e+"Z"},"triangle-up":function(n){var t=Math.sqrt(n/As),e=t*As/2;return"M0,"+-e+"L"+t+","+e+" "+-t+","+e+"Z"}});Xo.svg.symbolTypes=Ss.keys();var ks,Es,As=Math.sqrt(3),Cs=Math.tan(30*Na),Ns=[],Ls=0;Ns.call=da.call,Ns.empty=da.empty,Ns.node=da.node,Ns.size=da.size,Xo.transition=function(n){return arguments.length?ks?n.transition():n:xa.transition()},Xo.transition.prototype=Ns,Ns.select=function(n){var t,e,r,u=this.id,i=[];n=M(n);for(var o=-1,a=this.length;++oi;i++){u.push(t=[]);for(var e=this[i],a=0,c=e.length;c>a;a++)(r=e[a])&&n.call(r,r.__data__,a,i)&&t.push(r)}return Do(u,this.id)},Ns.tween=function(n,t){var e=this.id;return arguments.length<2?this.node().__transition__[e].tween.get(n):R(this,null==t?function(t){t.__transition__[e].tween.remove(n)}:function(r){r.__transition__[e].tween.set(n,t)})},Ns.attr=function(n,t){function e(){this.removeAttribute(a)}function r(){this.removeAttributeNS(a.space,a.local)}function u(n){return null==n?e:(n+="",function(){var t,e=this.getAttribute(a);return e!==n&&(t=o(e,n),function(n){this.setAttribute(a,t(n))})})}function i(n){return null==n?r:(n+="",function(){var t,e=this.getAttributeNS(a.space,a.local);return e!==n&&(t=o(e,n),function(n){this.setAttributeNS(a.space,a.local,t(n))})})}if(arguments.length<2){for(t in n)this.attr(t,n[t]);return this}var o="transform"==n?Ru:fu,a=Xo.ns.qualify(n);return Po(this,"attr."+n,t,a.local?i:u)},Ns.attrTween=function(n,t){function e(n,e){var r=t.call(this,n,e,this.getAttribute(u));return r&&function(n){this.setAttribute(u,r(n))}}function r(n,e){var r=t.call(this,n,e,this.getAttributeNS(u.space,u.local));return r&&function(n){this.setAttributeNS(u.space,u.local,r(n))}}var u=Xo.ns.qualify(n);return this.tween("attr."+n,u.local?r:e)},Ns.style=function(n,t,e){function r(){this.style.removeProperty(n)}function u(t){return null==t?r:(t+="",function(){var r,u=Go.getComputedStyle(this,null).getPropertyValue(n);return u!==t&&(r=fu(u,t),function(t){this.style.setProperty(n,r(t),e)})})}var i=arguments.length;if(3>i){if("string"!=typeof n){2>i&&(t="");for(e in n)this.style(e,n[e],t);return this}e=""}return Po(this,"style."+n,t,u)},Ns.styleTween=function(n,t,e){function r(r,u){var i=t.call(this,r,u,Go.getComputedStyle(this,null).getPropertyValue(n));return i&&function(t){this.style.setProperty(n,i(t),e)}}return arguments.length<3&&(e=""),this.tween("style."+n,r)},Ns.text=function(n){return Po(this,"text",n,Uo)},Ns.remove=function(){return this.each("end.transition",function(){var n;this.__transition__.count<2&&(n=this.parentNode)&&n.removeChild(this)})},Ns.ease=function(n){var t=this.id;return arguments.length<1?this.node().__transition__[t].ease:("function"!=typeof n&&(n=Xo.ease.apply(Xo,arguments)),R(this,function(e){e.__transition__[t].ease=n}))},Ns.delay=function(n){var t=this.id;return R(this,"function"==typeof n?function(e,r,u){e.__transition__[t].delay=+n.call(e,e.__data__,r,u)}:(n=+n,function(e){e.__transition__[t].delay=n}))},Ns.duration=function(n){var t=this.id;return R(this,"function"==typeof n?function(e,r,u){e.__transition__[t].duration=Math.max(1,n.call(e,e.__data__,r,u))}:(n=Math.max(1,n),function(e){e.__transition__[t].duration=n}))},Ns.each=function(n,t){var e=this.id;if(arguments.length<2){var r=Es,u=ks;ks=e,R(this,function(t,r,u){Es=t.__transition__[e],n.call(t,t.__data__,r,u)}),Es=r,ks=u}else R(this,function(r){var u=r.__transition__[e];(u.event||(u.event=Xo.dispatch("start","end"))).on(n,t)});return this},Ns.transition=function(){for(var n,t,e,r,u=this.id,i=++Ls,o=[],a=0,c=this.length;c>a;a++){o.push(n=[]);for(var t=this[a],s=0,l=t.length;l>s;s++)(e=t[s])&&(r=Object.create(e.__transition__[u]),r.delay+=r.duration,jo(e,s,i,r)),n.push(e)}return Do(o,i)},Xo.svg.axis=function(){function n(n){n.each(function(){var n,s=Xo.select(this),l=this.__chart__||e,f=this.__chart__=e.copy(),h=null==c?f.ticks?f.ticks.apply(f,a):f.domain():c,g=null==t?f.tickFormat?f.tickFormat.apply(f,a):bt:t,p=s.selectAll(".tick").data(h,f),v=p.enter().insert("g",".domain").attr("class","tick").style("opacity",Aa),d=Xo.transition(p.exit()).style("opacity",Aa).remove(),m=Xo.transition(p).style("opacity",1),y=Ri(f),x=s.selectAll(".domain").data([0]),M=(x.enter().append("path").attr("class","domain"),Xo.transition(x));v.append("line"),v.append("text");var _=v.select("line"),b=m.select("line"),w=p.select("text").text(g),S=v.select("text"),k=m.select("text");switch(r){case"bottom":n=Ho,_.attr("y2",u),S.attr("y",Math.max(u,0)+o),b.attr("x2",0).attr("y2",u),k.attr("x",0).attr("y",Math.max(u,0)+o),w.attr("dy",".71em").style("text-anchor","middle"),M.attr("d","M"+y[0]+","+i+"V0H"+y[1]+"V"+i);break;case"top":n=Ho,_.attr("y2",-u),S.attr("y",-(Math.max(u,0)+o)),b.attr("x2",0).attr("y2",-u),k.attr("x",0).attr("y",-(Math.max(u,0)+o)),w.attr("dy","0em").style("text-anchor","middle"),M.attr("d","M"+y[0]+","+-i+"V0H"+y[1]+"V"+-i);break;case"left":n=Fo,_.attr("x2",-u),S.attr("x",-(Math.max(u,0)+o)),b.attr("x2",-u).attr("y2",0),k.attr("x",-(Math.max(u,0)+o)).attr("y",0),w.attr("dy",".32em").style("text-anchor","end"),M.attr("d","M"+-i+","+y[0]+"H0V"+y[1]+"H"+-i);break;case"right":n=Fo,_.attr("x2",u),S.attr("x",Math.max(u,0)+o),b.attr("x2",u).attr("y2",0),k.attr("x",Math.max(u,0)+o).attr("y",0),w.attr("dy",".32em").style("text-anchor","start"),M.attr("d","M"+i+","+y[0]+"H0V"+y[1]+"H"+i)}if(f.rangeBand){var E=f,A=E.rangeBand()/2;l=f=function(n){return E(n)+A}}else l.rangeBand?l=f:d.call(n,f);v.call(n,l),m.call(n,f)})}var t,e=Xo.scale.linear(),r=zs,u=6,i=6,o=3,a=[10],c=null;return n.scale=function(t){return arguments.length?(e=t,n):e},n.orient=function(t){return arguments.length?(r=t in qs?t+"":zs,n):r},n.ticks=function(){return arguments.length?(a=arguments,n):a},n.tickValues=function(t){return arguments.length?(c=t,n):c},n.tickFormat=function(e){return arguments.length?(t=e,n):t},n.tickSize=function(t){var e=arguments.length;return e?(u=+t,i=+arguments[e-1],n):u},n.innerTickSize=function(t){return arguments.length?(u=+t,n):u},n.outerTickSize=function(t){return arguments.length?(i=+t,n):i},n.tickPadding=function(t){return arguments.length?(o=+t,n):o},n.tickSubdivide=function(){return arguments.length&&n},n};var zs="bottom",qs={top:1,right:1,bottom:1,left:1};Xo.svg.brush=function(){function n(i){i.each(function(){var i=Xo.select(this).style("pointer-events","all").style("-webkit-tap-highlight-color","rgba(0,0,0,0)").on("mousedown.brush",u).on("touchstart.brush",u),o=i.selectAll(".background").data([0]);o.enter().append("rect").attr("class","background").style("visibility","hidden").style("cursor","crosshair"),i.selectAll(".extent").data([0]).enter().append("rect").attr("class","extent").style("cursor","move");var a=i.selectAll(".resize").data(p,bt);a.exit().remove(),a.enter().append("g").attr("class",function(n){return"resize "+n}).style("cursor",function(n){return Ts[n]}).append("rect").attr("x",function(n){return/[ew]$/.test(n)?-3:null}).attr("y",function(n){return/^[ns]/.test(n)?-3:null}).attr("width",6).attr("height",6).style("visibility","hidden"),a.style("display",n.empty()?"none":null);var l,f=Xo.transition(i),h=Xo.transition(o);c&&(l=Ri(c),h.attr("x",l[0]).attr("width",l[1]-l[0]),e(f)),s&&(l=Ri(s),h.attr("y",l[0]).attr("height",l[1]-l[0]),r(f)),t(f)})}function t(n){n.selectAll(".resize").attr("transform",function(n){return"translate("+l[+/e$/.test(n)]+","+f[+/^s/.test(n)]+")"})}function e(n){n.select(".extent").attr("x",l[0]),n.selectAll(".extent,.n>rect,.s>rect").attr("width",l[1]-l[0])}function r(n){n.select(".extent").attr("y",f[0]),n.selectAll(".extent,.e>rect,.w>rect").attr("height",f[1]-f[0])}function u(){function u(){32==Xo.event.keyCode&&(C||(x=null,L[0]-=l[1],L[1]-=f[1],C=2),d())}function p(){32==Xo.event.keyCode&&2==C&&(L[0]+=l[1],L[1]+=f[1],C=0,d())}function v(){var n=Xo.mouse(_),u=!1;M&&(n[0]+=M[0],n[1]+=M[1]),C||(Xo.event.altKey?(x||(x=[(l[0]+l[1])/2,(f[0]+f[1])/2]),L[0]=l[+(n[0]p?(u=r,r=p):u=p),v[0]!=r||v[1]!=u?(e?o=null:i=null,v[0]=r,v[1]=u,!0):void 0}function y(){v(),S.style("pointer-events","all").selectAll(".resize").style("display",n.empty()?"none":null),Xo.select("body").style("cursor",null),z.on("mousemove.brush",null).on("mouseup.brush",null).on("touchmove.brush",null).on("touchend.brush",null).on("keydown.brush",null).on("keyup.brush",null),N(),w({type:"brushend"})}var x,M,_=this,b=Xo.select(Xo.event.target),w=a.of(_,arguments),S=Xo.select(_),k=b.datum(),E=!/^(n|s)$/.test(k)&&c,A=!/^(e|w)$/.test(k)&&s,C=b.classed("extent"),N=O(),L=Xo.mouse(_),z=Xo.select(Go).on("keydown.brush",u).on("keyup.brush",p);if(Xo.event.changedTouches?z.on("touchmove.brush",v).on("touchend.brush",y):z.on("mousemove.brush",v).on("mouseup.brush",y),S.interrupt().selectAll("*").interrupt(),C)L[0]=l[0]-L[0],L[1]=f[0]-L[1];else if(k){var q=+/w$/.test(k),T=+/^n/.test(k);M=[l[1-q]-L[0],f[1-T]-L[1]],L[0]=l[q],L[1]=f[T]}else Xo.event.altKey&&(x=L.slice());S.style("pointer-events","none").selectAll(".resize").style("display",null),Xo.select("body").style("cursor",b.style("cursor")),w({type:"brushstart"}),v()}var i,o,a=y(n,"brushstart","brush","brushend"),c=null,s=null,l=[0,0],f=[0,0],h=!0,g=!0,p=Rs[0];return n.event=function(n){n.each(function(){var n=a.of(this,arguments),t={x:l,y:f,i:i,j:o},e=this.__chart__||t;this.__chart__=t,ks?Xo.select(this).transition().each("start.brush",function(){i=e.i,o=e.j,l=e.x,f=e.y,n({type:"brushstart"})}).tween("brush:brush",function(){var e=hu(l,t.x),r=hu(f,t.y);return i=o=null,function(u){l=t.x=e(u),f=t.y=r(u),n({type:"brush",mode:"resize"})}}).each("end.brush",function(){i=t.i,o=t.j,n({type:"brush",mode:"resize"}),n({type:"brushend"})}):(n({type:"brushstart"}),n({type:"brush",mode:"resize"}),n({type:"brushend"}))})},n.x=function(t){return arguments.length?(c=t,p=Rs[!c<<1|!s],n):c},n.y=function(t){return arguments.length?(s=t,p=Rs[!c<<1|!s],n):s},n.clamp=function(t){return arguments.length?(c&&s?(h=!!t[0],g=!!t[1]):c?h=!!t:s&&(g=!!t),n):c&&s?[h,g]:c?h:s?g:null},n.extent=function(t){var e,r,u,a,h;return arguments.length?(c&&(e=t[0],r=t[1],s&&(e=e[0],r=r[0]),i=[e,r],c.invert&&(e=c(e),r=c(r)),e>r&&(h=e,e=r,r=h),(e!=l[0]||r!=l[1])&&(l=[e,r])),s&&(u=t[0],a=t[1],c&&(u=u[1],a=a[1]),o=[u,a],s.invert&&(u=s(u),a=s(a)),u>a&&(h=u,u=a,a=h),(u!=f[0]||a!=f[1])&&(f=[u,a])),n):(c&&(i?(e=i[0],r=i[1]):(e=l[0],r=l[1],c.invert&&(e=c.invert(e),r=c.invert(r)),e>r&&(h=e,e=r,r=h))),s&&(o?(u=o[0],a=o[1]):(u=f[0],a=f[1],s.invert&&(u=s.invert(u),a=s.invert(a)),u>a&&(h=u,u=a,a=h))),c&&s?[[e,u],[r,a]]:c?[e,r]:s&&[u,a])},n.clear=function(){return n.empty()||(l=[0,0],f=[0,0],i=o=null),n},n.empty=function(){return!!c&&l[0]==l[1]||!!s&&f[0]==f[1]},Xo.rebind(n,a,"on")};var Ts={n:"ns-resize",e:"ew-resize",s:"ns-resize",w:"ew-resize",nw:"nwse-resize",ne:"nesw-resize",se:"nwse-resize",sw:"nesw-resize"},Rs=[["n","e","s","w","nw","ne","se","sw"],["e","w"],["n","s"],[]],Ds=tc.format=ac.timeFormat,Ps=Ds.utc,Us=Ps("%Y-%m-%dT%H:%M:%S.%LZ");Ds.iso=Date.prototype.toISOString&&+new Date("2000-01-01T00:00:00.000Z")?Oo:Us,Oo.parse=function(n){var t=new Date(n);return isNaN(t)?null:t},Oo.toString=Us.toString,tc.second=Rt(function(n){return new ec(1e3*Math.floor(n/1e3))},function(n,t){n.setTime(n.getTime()+1e3*Math.floor(t))},function(n){return n.getSeconds()}),tc.seconds=tc.second.range,tc.seconds.utc=tc.second.utc.range,tc.minute=Rt(function(n){return new ec(6e4*Math.floor(n/6e4))},function(n,t){n.setTime(n.getTime()+6e4*Math.floor(t))},function(n){return n.getMinutes()}),tc.minutes=tc.minute.range,tc.minutes.utc=tc.minute.utc.range,tc.hour=Rt(function(n){var t=n.getTimezoneOffset()/60;return new ec(36e5*(Math.floor(n/36e5-t)+t))},function(n,t){n.setTime(n.getTime()+36e5*Math.floor(t))},function(n){return n.getHours()}),tc.hours=tc.hour.range,tc.hours.utc=tc.hour.utc.range,tc.month=Rt(function(n){return n=tc.day(n),n.setDate(1),n},function(n,t){n.setMonth(n.getMonth()+t)},function(n){return n.getMonth()}),tc.months=tc.month.range,tc.months.utc=tc.month.utc.range;var js=[1e3,5e3,15e3,3e4,6e4,3e5,9e5,18e5,36e5,108e5,216e5,432e5,864e5,1728e5,6048e5,2592e6,7776e6,31536e6],Hs=[[tc.second,1],[tc.second,5],[tc.second,15],[tc.second,30],[tc.minute,1],[tc.minute,5],[tc.minute,15],[tc.minute,30],[tc.hour,1],[tc.hour,3],[tc.hour,6],[tc.hour,12],[tc.day,1],[tc.day,2],[tc.week,1],[tc.month,1],[tc.month,3],[tc.year,1]],Fs=Ds.multi([[".%L",function(n){return n.getMilliseconds()}],[":%S",function(n){return n.getSeconds()}],["%I:%M",function(n){return n.getMinutes()}],["%I %p",function(n){return n.getHours()}],["%a %d",function(n){return n.getDay()&&1!=n.getDate()}],["%b %d",function(n){return 1!=n.getDate()}],["%B",function(n){return n.getMonth()}],["%Y",be]]),Os={range:function(n,t,e){return Xo.range(+n,+t,e).map(Io)},floor:bt,ceil:bt};Hs.year=tc.year,tc.scale=function(){return Yo(Xo.scale.linear(),Hs,Fs)};var Ys=Hs.map(function(n){return[n[0].utc,n[1]]}),Is=Ps.multi([[".%L",function(n){return n.getUTCMilliseconds()}],[":%S",function(n){return n.getUTCSeconds()}],["%I:%M",function(n){return n.getUTCMinutes()}],["%I %p",function(n){return n.getUTCHours()}],["%a %d",function(n){return n.getUTCDay()&&1!=n.getUTCDate()}],["%b %d",function(n){return 1!=n.getUTCDate()}],["%B",function(n){return n.getUTCMonth()}],["%Y",be]]);Ys.year=tc.year.utc,tc.scale.utc=function(){return Yo(Xo.scale.linear(),Ys,Is)},Xo.text=wt(function(n){return n.responseText}),Xo.json=function(n,t){return St(n,"application/json",Zo,t)},Xo.html=function(n,t){return St(n,"text/html",Vo,t)},Xo.xml=wt(function(n){return n.responseXML}),"function"==typeof define&&define.amd?define(Xo):"object"==typeof module&&module.exports?module.exports=Xo:this.d3=Xo}(); \ No newline at end of file diff --git a/mne/html/mpld3.v0.2.min.js b/mne/html/mpld3.v0.2.min.js deleted file mode 100644 index adefb15efa7..00000000000 --- a/mne/html/mpld3.v0.2.min.js +++ /dev/null @@ -1,2 +0,0 @@ -!function(t){function s(t){var s={};for(var o in t)s[o]=t[o];return s}function o(t,s){t="undefined"!=typeof t?t:10,s="undefined"!=typeof s?s:"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";for(var o=s.charAt(Math.round(Math.random()*(s.length-11))),e=1;t>e;e++)o+=s.charAt(Math.round(Math.random()*(s.length-1)));return o}function e(s,o){var e=t.interpolate([s[0].valueOf(),s[1].valueOf()],[o[0].valueOf(),o[1].valueOf()]);return function(t){var s=e(t);return[new Date(s[0]),new Date(s[1])]}}function i(t){return"undefined"==typeof t}function r(t){return null==t||i(t)}function n(t,s){return t.length>0?t[s%t.length]:null}function a(){function s(s,n){var a=t.functor(o),p=t.functor(e),h=[],l=[],c=0,d=-1,u=0,f=!1;if(!n){n=["M"];for(var y=1;yc;)i.call(this,s[c],c)?(h.push(a.call(this,s[c],c),p.call(this,s[c],c)),c++):(h=null,c=u);h?f&&h.length>0?(l.push("M",h[0],h[1]),f=!1):(l.push(n[d]),l=l.concat(h)):f=!0}return c!=s.length&&console.warn("Warning: not all vertices used in Path"),l.join(" ")}var o=function(t){return t[0]},e=function(t){return t[1]},i=function(){return!0},r={M:1,m:1,L:1,l:1,Q:2,q:2,T:1,t:1,S:2,s:2,C:3,c:3,Z:0,z:0};return s.x=function(t){return arguments.length?(o=t,s):o},s.y=function(t){return arguments.length?(e=t,s):e},s.defined=function(t){return arguments.length?(i=t,s):i},s.call=s,s}function p(){function t(t){return s.forEach(function(s){t=s(t)}),t}var s=Array.prototype.slice.call(arguments,0),o=s.length;return t.domain=function(o){return arguments.length?(s[0].domain(o),t):s[0].domain()},t.range=function(e){return arguments.length?(s[o-1].range(e),t):s[o-1].range()},t.step=function(t){return s[t]},t}function h(t,s){if(O.call(this,t,s),this.cssclass="mpld3-"+this.props.xy+"grid","x"==this.props.xy)this.transform="translate(0,"+this.ax.height+")",this.position="bottom",this.scale=this.ax.xdom,this.tickSize=-this.ax.height;else{if("y"!=this.props.xy)throw"unrecognized grid xy specifier: should be 'x' or 'y'";this.transform="translate(0,0)",this.position="left",this.scale=this.ax.ydom,this.tickSize=-this.ax.width}}function l(t,s){O.call(this,t,s);var o={bottom:[0,this.ax.height],top:[0,0],left:[0,0],right:[this.ax.width,0]},e={bottom:"x",top:"x",left:"y",right:"y"};this.transform="translate("+o[this.props.position]+")",this.props.xy=e[this.props.position],this.cssclass="mpld3-"+this.props.xy+"axis",this.scale=this.ax[this.props.xy+"dom"]}function c(t,s){if("undefined"==typeof s){if(this.ax=null,this.fig=null,"display"!==this.trans)throw"ax must be defined if transform != 'display'"}else this.ax=s,this.fig=s.fig;if(this.zoomable="data"===t,this.x=this["x_"+t],this.y=this["y_"+t],"undefined"==typeof this.x||"undefined"==typeof this.y)throw"unrecognized coordinate code: "+t}function d(t,s){O.call(this,t,s),this.data=t.fig.get_data(this.props.data),this.pathcodes=this.props.pathcodes,this.pathcoords=new c(this.props.coordinates,this.ax),this.offsetcoords=new c(this.props.offsetcoordinates,this.ax),this.datafunc=a()}function u(t,s){O.call(this,t,s),(null==this.props.facecolors||0==this.props.facecolors.length)&&(this.props.facecolors=["none"]),(null==this.props.edgecolors||0==this.props.edgecolors.length)&&(this.props.edgecolors=["none"]);var o=this.ax.fig.get_data(this.props.offsets);(null===o||0===o.length)&&(o=[null]);var e=Math.max(this.props.paths.length,o.length);if(o.length===e)this.offsets=o;else{this.offsets=[];for(var i=0;e>i;i++)this.offsets.push(n(o,i))}this.pathcoords=new c(this.props.pathcoordinates,this.ax),this.offsetcoords=new c(this.props.offsetcoordinates,this.ax)}function f(s,o){O.call(this,s,o);var e=this.props;e.facecolor="none",e.edgecolor=e.color,delete e.color,e.edgewidth=e.linewidth,delete e.linewidth,this.defaultProps=d.prototype.defaultProps,d.call(this,s,e),this.datafunc=t.svg.line().interpolate("linear")}function y(s,o){O.call(this,s,o),this.marker=null!==this.props.markerpath?0==this.props.markerpath[0].length?null:F.path().call(this.props.markerpath[0],this.props.markerpath[1]):null===this.props.markername?null:t.svg.symbol(this.props.markername).size(Math.pow(this.props.markersize,2))();var e={paths:[this.props.markerpath],offsets:s.fig.get_data(this.props.data),xindex:this.props.xindex,yindex:this.props.yindex,offsetcoordinates:this.props.coordinates,edgecolors:[this.props.edgecolor],edgewidths:[this.props.edgewidth],facecolors:[this.props.facecolor],alphas:[this.props.alpha],zorder:this.props.zorder,id:this.props.id};this.requiredProps=u.prototype.requiredProps,this.defaultProps=u.prototype.defaultProps,u.call(this,s,e)}function g(t,s){O.call(this,t,s),this.coords=new c(this.props.coordinates,this.ax)}function m(t,s){O.call(this,t,s),this.text=this.props.text,this.position=this.props.position,this.coords=new c(this.props.coordinates,this.ax)}function x(s,o){function e(t){return new Date(t[0],t[1],t[2],t[3],t[4],t[5])}function i(t,s){return"date"!==t?s:[e(s[0]),e(s[1])]}function r(s,o,e){var i="date"===s?t.time.scale():"log"===s?t.scale.log():t.scale.linear();return i.domain(o).range(e)}O.call(this,s,o),this.axnum=this.fig.axes.length,this.axid=this.fig.figid+"_ax"+(this.axnum+1),this.clipid=this.axid+"_clip",this.props.xdomain=this.props.xdomain||this.props.xlim,this.props.ydomain=this.props.ydomain||this.props.ylim,this.sharex=[],this.sharey=[],this.elements=[];var n=this.props.bbox;this.position=[n[0]*this.fig.width,(1-n[1]-n[3])*this.fig.height],this.width=n[2]*this.fig.width,this.height=n[3]*this.fig.height,this.props.xdomain=i(this.props.xscale,this.props.xdomain),this.props.ydomain=i(this.props.yscale,this.props.ydomain),this.x=this.xdom=r(this.props.xscale,this.props.xdomain,[0,this.width]),this.y=this.ydom=r(this.props.yscale,this.props.ydomain,[this.height,0]),"date"===this.props.xscale&&(this.x=F.multiscale(t.scale.linear().domain(this.props.xlim).range(this.props.xdomain.map(Number)),this.xdom)),"date"===this.props.yscale&&(this.x=F.multiscale(t.scale.linear().domain(this.props.ylim).range(this.props.ydomain.map(Number)),this.ydom));for(var a=this.props.axes,p=0;p0&&this.buttons.forEach(function(t){t.actions.filter(s).length>0&&t.deactivate()})},F.Button=v,v.prototype=Object.create(O.prototype),v.prototype.constructor=v,v.prototype.setState=function(t){t?this.activate():this.deactivate()},v.prototype.click=function(){this.active?this.deactivate():this.activate()},v.prototype.activate=function(){this.toolbar.deactivate_by_action(this.actions),this.onActivate(),this.active=!0,this.toolbar.toolbar.select("."+this.cssclass).classed({pressed:!0}),this.sticky||this.deactivate()},v.prototype.deactivate=function(){this.onDeactivate(),this.active=!1,this.toolbar.toolbar.select("."+this.cssclass).classed({pressed:!1})},v.prototype.sticky=!1,v.prototype.actions=[],v.prototype.icon=function(){return""},v.prototype.onActivate=function(){},v.prototype.onDeactivate=function(){},v.prototype.onDraw=function(){},F.ButtonFactory=function(t){function s(t){v.call(this,t,this.buttonID)}if("string"!=typeof t.buttonID)throw"ButtonFactory: buttonID must be present and be a string";s.prototype=Object.create(v.prototype),s.prototype.constructor=s;for(var o in t)s.prototype[o]=t[o];return s},F.Plugin=A,A.prototype=Object.create(O.prototype),A.prototype.constructor=A,A.prototype.requiredProps=[],A.prototype.defaultProps={},A.prototype.draw=function(){},F.ResetPlugin=z,F.register_plugin("reset",z),z.prototype=Object.create(A.prototype),z.prototype.constructor=z,z.prototype.requiredProps=[],z.prototype.defaultProps={},F.ZoomPlugin=w,F.register_plugin("zoom",w),w.prototype=Object.create(A.prototype),w.prototype.constructor=w,w.prototype.requiredProps=[],w.prototype.defaultProps={button:!0,enabled:null},w.prototype.activate=function(){this.fig.enable_zoom()},w.prototype.deactivate=function(){this.fig.disable_zoom()},w.prototype.draw=function(){this.props.enabled?this.fig.enable_zoom():this.fig.disable_zoom()},F.BoxZoomPlugin=_,F.register_plugin("boxzoom",_),_.prototype=Object.create(A.prototype),_.prototype.constructor=_,_.prototype.requiredProps=[],_.prototype.defaultProps={button:!0,enabled:null},_.prototype.activate=function(){this.enable&&this.enable()},_.prototype.deactivate=function(){this.disable&&this.disable()},_.prototype.draw=function(){function t(t){if(this.enabled){var o=s.extent();s.empty()||t.set_axlim([o[0][0],o[1][0]],[o[0][1],o[1][1]])}t.axes.call(s.clear())}F.insert_css("#"+this.fig.figid+" rect.extent."+this.extentClass,{fill:"#fff","fill-opacity":0,stroke:"#999"});var s=this.fig.getBrush();this.enable=function(){this.fig.showBrush(this.extentClass),s.on("brushend",t.bind(this)),this.enabled=!0},this.disable=function(){this.fig.hideBrush(this.extentClass),this.enabled=!1},this.toggle=function(){this.enabled?this.disable():this.enable()},this.disable()},F.TooltipPlugin=k,F.register_plugin("tooltip",k),k.prototype=Object.create(A.prototype),k.prototype.constructor=k,k.prototype.requiredProps=["id"],k.prototype.defaultProps={labels:null,hoffset:0,voffset:10,location:"mouse"},k.prototype.draw=function(){function s(t,s){this.tooltip.style("visibility","visible").text(null===r?"("+t+")":n(r,s))}function o(){if("mouse"===a){var s=t.mouse(this.fig.canvas.node());this.x=s[0]+this.props.hoffset,this.y=s[1]-this.props.voffset}this.tooltip.attr("x",this.x).attr("y",this.y)}function e(){this.tooltip.style("visibility","hidden")}var i=F.get_element(this.props.id,this.fig),r=this.props.labels,a=this.props.location;this.tooltip=this.fig.canvas.append("text").attr("class","mpld3-tooltip-text").attr("x",0).attr("y",0).text("").style("visibility","hidden"),"bottom left"==a||"top left"==a?(this.x=i.ax.position[0]+5+this.props.hoffset,this.tooltip.style("text-anchor","beginning")):"bottom right"==a||"top right"==a?(this.x=i.ax.position[0]+i.ax.width-5+this.props.hoffset,this.tooltip.style("text-anchor","end")):this.tooltip.style("text-anchor","middle"),"bottom left"==a||"bottom right"==a?this.y=i.ax.position[1]+i.ax.height-5+this.props.voffset:("top left"==a||"top right"==a)&&(this.y=i.ax.position[1]+5+this.props.voffset),i.elements().on("mouseover",s.bind(this)).on("mousemove",o.bind(this)).on("mouseout",e.bind(this))},F.LinkedBrushPlugin=P,F.register_plugin("linkedbrush",P),P.prototype=Object.create(F.Plugin.prototype),P.prototype.constructor=P,P.prototype.requiredProps=["id"],P.prototype.defaultProps={button:!0,enabled:null},P.prototype.activate=function(){this.enable&&this.enable()},P.prototype.deactivate=function(){this.disable&&this.disable()},P.prototype.draw=function(){function s(s){l!=this&&(t.select(l).call(p.clear()),l=this,p.x(s.xdom).y(s.ydom))}function o(t){var s=h[t.axnum];if(s.length>0){var o=s[0].props.xindex,e=s[0].props.yindex,i=p.extent();p.empty()?c.selectAll("path").classed("mpld3-hidden",!1):c.selectAll("path").classed("mpld3-hidden",function(t){return i[0][0]>t[o]||i[1][0]t[e]||i[1][1]1?s[1]:""},"object"==typeof module&&module.exports?module.exports=F:this.mpld3=F,console.log("Loaded mpld3 version "+F.version)}(d3); \ No newline at end of file diff --git a/mne/report/js_and_css/report.css b/mne/report/js_and_css/report.css new file mode 100644 index 00000000000..724a13241a5 --- /dev/null +++ b/mne/report/js_and_css/report.css @@ -0,0 +1,19 @@ +#container { + position: relative; + padding-bottom: 8rem; +} + +#content { + margin-top: 90px; + scroll-behavior: smooth; + position: relative; /* for scrollspy */ +} + +#toc { + margin-top: 90px; + padding-bottom: 8rem; +} + +footer { + margin-top: 8rem; +} diff --git a/mne/report/js_and_css/report.sass b/mne/report/js_and_css/report.sass deleted file mode 100644 index 4d533d07011..00000000000 --- a/mne/report/js_and_css/report.sass +++ /dev/null @@ -1,19 +0,0 @@ -#container { - position: relative - padding-bottom: 5rem -} - -#content { - margin-top: 90px - scroll-behavior: smooth - position: relative // for scrollspy -} - -#toc { - margin-top: 90px - padding-bottom: 5rem -} - -footer { - margin-top: 5rem; -} diff --git a/mne/report/report.py b/mne/report/report.py index 34acb8860e6..534377d62e3 100644 --- a/mne/report/report.py +++ b/mne/report/report.py @@ -143,7 +143,7 @@ html_include_dir = Path(__file__).parent / "js_and_css" template_dir = Path(__file__).parent / "templates" JAVASCRIPT = (html_include_dir / "report.js").read_text(encoding="utf-8") -CSS = (html_include_dir / "report.sass").read_text(encoding="utf-8") +CSS = (html_include_dir / "report.css").read_text(encoding="utf-8") MAX_IMG_RES = 100 # in dots per inch MAX_IMG_WIDTH = 850 # in pixels From 195a2cc8009160fd125e355b0280e903a941c874 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Tue, 30 Jan 2024 15:52:20 -0500 Subject: [PATCH 169/405] MAINT: Test arm64 on GHA (#12400) Co-authored-by: Daniel McCloy --- .circleci/config.yml | 66 ------------------------------------- .github/workflows/tests.yml | 9 ++++- 2 files changed, 8 insertions(+), 67 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 6618188621b..bca927a36d3 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -22,58 +22,6 @@ _check_skip: &check_skip fi jobs: - pytest-macos-arm64: - parameters: - scheduled: - type: string - default: "false" - macos: - xcode: "14.2.0" - resource_class: macos.m1.medium.gen1 - environment: - HOMEBREW_NO_AUTO_UPDATE: 1 - steps: - - checkout - - run: - <<: *check_skip - - run: - name: Install Python and dependencies - command: | - set -eo pipefail - brew install python@3.11 - which python - which pip - pip install --upgrade pip - pip install --upgrade --only-binary "numpy,scipy,dipy,statsmodels" -ve .[full,test_extra] - # 3D too slow on Apple's software renderer, and numba causes us problems - pip uninstall -y vtk pyvista pyvistaqt numba - mkdir -p test-results - echo "set -eo pipefail" >> $BASH_ENV - - run: - command: mne sys_info - - run: - command: ./tools/get_testing_version.sh && cat testing_version.txt - - restore_cache: - keys: - - data-cache-testing-{{ checksum "testing_version.txt" }} - - run: - command: python -c "import mne; mne.datasets.testing.data_path(verbose=True)" - - save_cache: - key: data-cache-testing-{{ checksum "testing_version.txt" }} - paths: - - ~/mne_data/MNE-testing-data # (2.5 G) - - run: - command: pytest -m "not slowtest" --tb=short --cov=mne --cov-report xml -vv mne - - run: - name: Prepare test data upload - command: cp -av junit-results.xml test-results/junit.xml - - store_test_results: - path: ./test-results - # Codecov orb has bugs on macOS (gpg issues) - # - codecov/upload - - run: - command: bash <(curl -s https://codecov.io/bash) - build_docs: parameters: scheduled: @@ -591,20 +539,6 @@ workflows: only: - main - weekly: - jobs: - - pytest-macos-arm64: - name: pytest_macos_arm64_weekly - scheduled: "true" - triggers: - - schedule: - # "At 6:00 AM GMT every Monday" - cron: "0 6 * * 1" - filters: - branches: - only: - - main - monthly: jobs: - linkcheck: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 396f03803f3..42bbaba2a21 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -61,7 +61,11 @@ jobs: - os: ubuntu-latest python: '3.12' kind: conda - - os: macos-latest + # 3.12 needs https://github.com/conda-forge/dipy-feedstock/pull/50 + - os: macos-14 # arm64 + python: '3.11' + kind: mamba + - os: macos-latest # intel python: '3.11' kind: mamba - os: windows-latest @@ -104,6 +108,9 @@ jobs: mamba fmt!=10.2.0 if: ${{ !startswith(matrix.kind, 'pip') }} + # Make sure we have the right Python + - run: python -c "import platform; assert platform.machine() == 'arm64', platform.machine()" + if: matrix.os == 'macos-14' - run: ./tools/github_actions_dependencies.sh # Minimal commands on Linux (macOS stalls) - run: ./tools/get_minimal_commands.sh From 3a42bb913fcbfdfed7ae9e23b5649c51b372eb9c Mon Sep 17 00:00:00 2001 From: Jacob Woessner Date: Fri, 2 Feb 2024 09:13:11 -0600 Subject: [PATCH 170/405] ENH: Add ability to reject epochs using callables (#12195) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Eric Larson --- doc/changes/devel/12195.newfeature.rst | 1 + mne/epochs.py | 102 ++++++++--- mne/tests/test_epochs.py | 165 +++++++++++++++++- mne/utils/docs.py | 48 ++++- mne/utils/mixin.py | 13 +- .../preprocessing/20_rejecting_bad_data.py | 108 +++++++++++- 6 files changed, 399 insertions(+), 38 deletions(-) create mode 100644 doc/changes/devel/12195.newfeature.rst diff --git a/doc/changes/devel/12195.newfeature.rst b/doc/changes/devel/12195.newfeature.rst new file mode 100644 index 00000000000..0c7e044abce --- /dev/null +++ b/doc/changes/devel/12195.newfeature.rst @@ -0,0 +1 @@ +Add ability reject :class:`mne.Epochs` using callables, by `Jacob Woessner`_. \ No newline at end of file diff --git a/mne/epochs.py b/mne/epochs.py index 2b437dca6b3..1e86c6c96b0 100644 --- a/mne/epochs.py +++ b/mne/epochs.py @@ -787,7 +787,7 @@ def apply_baseline(self, baseline=(None, 0), *, verbose=None): self.baseline = baseline return self - def _reject_setup(self, reject, flat): + def _reject_setup(self, reject, flat, *, allow_callable=False): """Set self._reject_time and self._channel_type_idx.""" idx = channel_indices_by_type(self.info) reject = deepcopy(reject) if reject is not None else dict() @@ -814,11 +814,21 @@ def _reject_setup(self, reject, flat): f"{key.upper()}." ) - # check for invalid values - for rej, kind in zip((reject, flat), ("Rejection", "Flat")): - for key, val in rej.items(): - if val is None or val < 0: - raise ValueError(f'{kind} value must be a number >= 0, not "{val}"') + # check for invalid values + for rej, kind in zip((reject, flat), ("Rejection", "Flat")): + for key, val in rej.items(): + name = f"{kind} dict value for {key}" + if callable(val) and allow_callable: + continue + extra_str = "" + if allow_callable: + extra_str = "or callable" + _validate_type(val, "numeric", name, extra=extra_str) + if val is None or val < 0: + raise ValueError( + f"If using numerical {name} criteria, the value " + f"must be >= 0, not {repr(val)}" + ) # now check to see if our rejection and flat are getting more # restrictive @@ -836,6 +846,9 @@ def _reject_setup(self, reject, flat): reject[key] = old_reject[key] # make sure new thresholds are at least as stringent as the old ones for key in reject: + # Skip this check if old_reject and reject are callables + if callable(reject[key]) and allow_callable: + continue if key in old_reject and reject[key] > old_reject[key]: raise ValueError( bad_msg.format( @@ -851,6 +864,8 @@ def _reject_setup(self, reject, flat): for key in set(old_flat) - set(flat): flat[key] = old_flat[key] for key in flat: + if callable(flat[key]) and allow_callable: + continue if key in old_flat and flat[key] < old_flat[key]: raise ValueError( bad_msg.format( @@ -1404,7 +1419,7 @@ def drop_bad(self, reject="existing", flat="existing", verbose=None): flat = self.flat if any(isinstance(rej, str) and rej != "existing" for rej in (reject, flat)): raise ValueError('reject and flat, if strings, must be "existing"') - self._reject_setup(reject, flat) + self._reject_setup(reject, flat, allow_callable=True) self._get_data(out=False, verbose=verbose) return self @@ -1520,8 +1535,9 @@ def drop(self, indices, reason="USER", verbose=None): Set epochs to remove by specifying indices to remove or a boolean mask to apply (where True values get removed). Events are correspondingly modified. - reason : str - Reason for dropping the epochs ('ECG', 'timeout', 'blink' etc). + reason : list | tuple | str + Reason(s) for dropping the epochs ('ECG', 'timeout', 'blink' etc). + Reason(s) are applied to all indices specified. Default: 'USER'. %(verbose)s @@ -1533,7 +1549,9 @@ def drop(self, indices, reason="USER", verbose=None): indices = np.atleast_1d(indices) if indices.ndim > 1: - raise ValueError("indices must be a scalar or a 1-d array") + raise TypeError("indices must be a scalar or a 1-d array") + # Check if indices and reasons are of the same length + # if using collection to drop epochs if indices.dtype == np.dtype(bool): indices = np.where(indices)[0] @@ -3199,6 +3217,10 @@ class Epochs(BaseEpochs): See :meth:`~mne.Epochs.equalize_event_counts` - 'USER' For user-defined reasons (see :meth:`~mne.Epochs.drop`). + + When dropping based on flat or reject parameters the tuple of + reasons contains a tuple of channels that satisfied the rejection + criteria. filename : str The filename of the object. times : ndarray @@ -3667,6 +3689,8 @@ def _is_good( ): """Test if data segment e is good according to reject and flat. + The reject and flat parameters can accept functions as values. + If full_report=True, it will give True/False as well as a list of all offending channels. """ @@ -3674,30 +3698,60 @@ def _is_good( has_printed = False checkable = np.ones(len(ch_names), dtype=bool) checkable[np.array([c in ignore_chs for c in ch_names], dtype=bool)] = False + for refl, f, t in zip([reject, flat], [np.greater, np.less], ["", "flat"]): if refl is not None: - for key, thresh in refl.items(): + for key, refl in refl.items(): + criterion = refl idx = channel_type_idx[key] name = key.upper() if len(idx) > 0: e_idx = e[idx] - deltas = np.max(e_idx, axis=1) - np.min(e_idx, axis=1) checkable_idx = checkable[idx] - idx_deltas = np.where( - np.logical_and(f(deltas, thresh), checkable_idx) - )[0] + # Check if criterion is a function and apply it + if callable(criterion): + result = criterion(e_idx) + _validate_type(result, tuple, "reject/flat output") + if len(result) != 2: + raise TypeError( + "Function criterion must return a tuple of length 2" + ) + cri_truth, reasons = result + _validate_type(cri_truth, (bool, np.bool_), cri_truth, "bool") + _validate_type( + reasons, (str, list, tuple), reasons, "str, list, or tuple" + ) + idx_deltas = np.where(np.logical_and(cri_truth, checkable_idx))[ + 0 + ] + else: + deltas = np.max(e_idx, axis=1) - np.min(e_idx, axis=1) + idx_deltas = np.where( + np.logical_and(f(deltas, criterion), checkable_idx) + )[0] if len(idx_deltas) > 0: - bad_names = [ch_names[idx[i]] for i in idx_deltas] - if not has_printed: - logger.info( - f" Rejecting {t} epoch based on {name} : {bad_names}" - ) - has_printed = True - if not full_report: - return False + # Check to verify that refl is a callable that returns + # (bool, reason). Reason must be a str/list/tuple. + # If using tuple + if callable(refl): + if isinstance(reasons, str): + reasons = (reasons,) + for idx, reason in enumerate(reasons): + _validate_type(reason, str, reason) + bad_tuple += tuple(reasons) else: - bad_tuple += tuple(bad_names) + bad_names = [ch_names[idx[i]] for i in idx_deltas] + if not has_printed: + logger.info( + " Rejecting %s epoch based on %s : " + "%s" % (t, name, bad_names) + ) + has_printed = True + if not full_report: + return False + else: + bad_tuple += tuple(bad_names) if not full_report: return True diff --git a/mne/tests/test_epochs.py b/mne/tests/test_epochs.py index 2b67dd9dbd6..96d90414e07 100644 --- a/mne/tests/test_epochs.py +++ b/mne/tests/test_epochs.py @@ -550,7 +550,20 @@ def test_reject(): preload=False, reject=dict(eeg=np.inf), ) - for val in (None, -1): # protect against older MNE-C types + + # Good function + def my_reject_1(epoch_data): + bad_idxs = np.where(np.percentile(epoch_data, 90, axis=1) > 1e-35) + reasons = "a" * len(bad_idxs[0]) + return len(bad_idxs) > 0, reasons + + # Bad function + def my_reject_2(epoch_data): + bad_idxs = np.where(np.percentile(epoch_data, 90, axis=1) > 1e-35) + reasons = "a" * len(bad_idxs[0]) + return len(bad_idxs), reasons + + for val in (-1, -2): # protect against older MNE-C types for kwarg in ("reject", "flat"): pytest.raises( ValueError, @@ -564,6 +577,44 @@ def test_reject(): preload=False, **{kwarg: dict(grad=val)}, ) + + # Check that reject and flat in constructor are not callables + val = my_reject_1 + for kwarg in ("reject", "flat"): + with pytest.raises( + TypeError, + match=r".* must be an instance of numeric, got instead.", + ): + Epochs( + raw, + events, + event_id, + tmin, + tmax, + picks=picks_meg, + preload=False, + **{kwarg: dict(grad=val)}, + ) + + # Check if callable returns a tuple with reasons + bad_types = [my_reject_2, ("Hi" "Hi"), (1, 1), None] + for val in bad_types: # protect against bad types + for kwarg in ("reject", "flat"): + with pytest.raises( + TypeError, + match=r".* must be an instance of .* got instead.", + ): + epochs = Epochs( + raw, + events, + event_id, + tmin, + tmax, + picks=picks_meg, + preload=True, + ) + epochs.drop_bad(**{kwarg: dict(grad=val)}) + pytest.raises( KeyError, Epochs, @@ -2149,6 +2200,93 @@ def test_reject_epochs(tmp_path): assert epochs_cleaned.flat == dict(grad=new_flat["grad"], mag=flat["mag"]) +@testing.requires_testing_data +def test_callable_reject(): + """Test using a callable for rejection.""" + raw = read_raw_fif(fname_raw_testing, preload=True) + raw.crop(0, 5) + raw.del_proj() + chans = raw.info["ch_names"][-6:-1] + raw.pick(chans) + data = raw.get_data() + + # Add some artifacts + new_data = data + new_data[0, 180:200] *= 1e7 + new_data[0, 610:880] += 1e-3 + edit_raw = mne.io.RawArray(new_data, raw.info) + + events = mne.make_fixed_length_events(edit_raw, id=1, duration=1.0, start=0) + epochs = mne.Epochs(edit_raw, events, tmin=0, tmax=1, baseline=None, preload=True) + assert len(epochs) == 5 + + epochs = mne.Epochs( + edit_raw, + events, + tmin=0, + tmax=1, + baseline=None, + preload=True, + ) + epochs.drop_bad( + reject=dict(eeg=lambda x: ((np.median(x, axis=1) > 1e-3).any(), "eeg median")) + ) + + assert epochs.drop_log[2] == ("eeg median",) + + epochs = mne.Epochs( + edit_raw, + events, + tmin=0, + tmax=1, + baseline=None, + preload=True, + ) + epochs.drop_bad( + reject=dict(eeg=lambda x: ((np.max(x, axis=1) > 1).any(), ("eeg max",))) + ) + + assert epochs.drop_log[0] == ("eeg max",) + + def reject_criteria(x): + max_condition = np.max(x, axis=1) > 1e-2 + median_condition = np.median(x, axis=1) > 1e-4 + return (max_condition.any() or median_condition.any()), "eeg max or median" + + epochs = mne.Epochs( + edit_raw, + events, + tmin=0, + tmax=1, + baseline=None, + preload=True, + ) + epochs.drop_bad(reject=dict(eeg=reject_criteria)) + + assert epochs.drop_log[0] == ("eeg max or median",) and epochs.drop_log[2] == ( + "eeg max or median", + ) + + # Test reasons must be str or tuple of str + with pytest.raises( + TypeError, + match=r".* must be an instance of str, got instead.", + ): + epochs = mne.Epochs( + edit_raw, + events, + tmin=0, + tmax=1, + baseline=None, + preload=True, + ) + epochs.drop_bad( + reject=dict( + eeg=lambda x: ((np.median(x, axis=1) > 1e-3).any(), ("eeg median", 2)) + ) + ) + + def test_preload_epochs(): """Test preload of epochs.""" raw, events, picks = _get_data() @@ -3180,9 +3318,16 @@ def test_drop_epochs(): events1 = events[events[:, 2] == event_id] # Bound checks - pytest.raises(IndexError, epochs.drop, [len(epochs.events)]) - pytest.raises(IndexError, epochs.drop, [-len(epochs.events) - 1]) - pytest.raises(ValueError, epochs.drop, [[1, 2], [3, 4]]) + with pytest.raises(IndexError, match=r"Epoch index .* is out of bounds"): + epochs.drop([len(epochs.events)]) + with pytest.raises(IndexError, match=r"Epoch index .* is out of bounds"): + epochs.drop([-len(epochs.events) - 1]) + with pytest.raises(TypeError, match="indices must be a scalar or a 1-d array"): + epochs.drop([[1, 2], [3, 4]]) + with pytest.raises( + TypeError, match=r".* must be an instance of .* got instead." + ): + epochs.drop([1], reason=("a", "b", 2)) # Test selection attribute assert_array_equal(epochs.selection, np.where(events[:, 2] == event_id)[0]) @@ -3202,6 +3347,18 @@ def test_drop_epochs(): assert_array_equal(events[epochs[3:].selection], events1[[5, 6]]) assert_array_equal(events[epochs["1"].selection], events1[[0, 1, 3, 5, 6]]) + # Test using tuple to drop epochs + raw, events, picks = _get_data() + epochs_tuple = Epochs(raw, events, event_id, tmin, tmax, picks=picks, preload=True) + selection_tuple = epochs_tuple.selection.copy() + epochs_tuple.drop((2, 3, 4), reason=("a", "b")) + n_events = len(epochs.events) + assert [epochs_tuple.drop_log[k] for k in selection_tuple[[2, 3, 4]]] == [ + ("a", "b"), + ("a", "b"), + ("a", "b"), + ] + @pytest.mark.parametrize("preload", (True, False)) def test_drop_epochs_mult(preload): diff --git a/mne/utils/docs.py b/mne/utils/docs.py index ec9fe66bae0..c3005427ead 100644 --- a/mne/utils/docs.py +++ b/mne/utils/docs.py @@ -1456,9 +1456,16 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): quality, pass the ``reject_tmin`` and ``reject_tmax`` parameters. """ -docdict["flat_drop_bad"] = f""" +docdict["flat_drop_bad"] = """ flat : dict | str | None -{_flat_common} + Reject epochs based on **minimum** peak-to-peak signal amplitude (PTP) + or a custom function. Valid **keys** can be any channel type present + in the object. If using PTP, **values** are floats that set the minimum + acceptable PTP. If the PTP is smaller than this threshold, the epoch + will be dropped. If ``None`` then no rejection is performed based on + flatness of the signal. If a custom function is used than ``flat`` can be + used to reject epochs based on any criteria (including maxima and + minima). If ``'existing'``, then the flat parameters set during epoch creation are used. """ @@ -3291,12 +3298,43 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): difference will be preserved. """ -docdict["reject_drop_bad"] = f""" +docdict["reject_drop_bad"] = """ reject : dict | str | None -{_reject_common} + Reject epochs based on **maximum** peak-to-peak signal amplitude (PTP) + or custom functions. Peak-to-peak signal amplitude is defined as + the absolute difference between the lowest and the highest signal + value. In each individual epoch, the PTP is calculated for every channel. + If the PTP of any one channel exceeds the rejection threshold, the + respective epoch will be dropped. + + The dictionary keys correspond to the different channel types; valid + **keys** can be any channel type present in the object. + + Example:: + + reject = dict(grad=4000e-13, # unit: T / m (gradiometers) + mag=4e-12, # unit: T (magnetometers) + eeg=40e-6, # unit: V (EEG channels) + eog=250e-6 # unit: V (EOG channels) + ) + + Custom rejection criteria can be also be used by passing a callable, + e.g., to check for 99th percentile of absolute values of any channel + across time being bigger than 1mV. The callable must return a good, reason tuple. + Where good must be bool and reason must be str, list, or tuple where each entry is a str.:: + + reject = dict(eeg=lambda x: ((np.percentile(np.abs(x), 99, axis=1) > 1e-3).any(), "> 1mV somewhere")) + + .. note:: If rejection is based on a signal **difference** + calculated for each channel separately, applying baseline + correction does not affect the rejection procedure, as the + difference will be preserved. + + .. note:: If ``reject`` is a callable, than **any** criteria can be + used to reject epochs (including maxima and minima). If ``reject`` is ``None``, no rejection is performed. If ``'existing'`` (default), then the rejection parameters set at instantiation are used. -""" +""" # noqa: E501 docdict["reject_epochs"] = f""" reject : dict | None diff --git a/mne/utils/mixin.py b/mne/utils/mixin.py index c90121fdfbb..87e86aaa315 100644 --- a/mne/utils/mixin.py +++ b/mne/utils/mixin.py @@ -178,7 +178,7 @@ def _getitem( ---------- item: slice, array-like, str, or list see `__getitem__` for details. - reason: str + reason: str, list/tuple of str entry in `drop_log` for unselected epochs copy: bool return a copy of the current object @@ -209,8 +209,15 @@ def _getitem( key_selection = inst.selection[select] drop_log = list(inst.drop_log) if reason is not None: - for k in np.setdiff1d(inst.selection, key_selection): - drop_log[k] = (reason,) + _validate_type(reason, (list, tuple, str), "reason") + if isinstance(reason, (list, tuple)): + for r in reason: + _validate_type(r, str, r) + if isinstance(reason, str): + reason = (reason,) + reason = tuple(reason) + for idx in np.setdiff1d(inst.selection, key_selection): + drop_log[idx] = reason inst.drop_log = tuple(drop_log) inst.selection = key_selection del drop_log diff --git a/tutorials/preprocessing/20_rejecting_bad_data.py b/tutorials/preprocessing/20_rejecting_bad_data.py index d478255b048..a04005f3532 100644 --- a/tutorials/preprocessing/20_rejecting_bad_data.py +++ b/tutorials/preprocessing/20_rejecting_bad_data.py @@ -23,6 +23,8 @@ import os +import numpy as np + import mne sample_data_folder = mne.datasets.sample.data_path() @@ -205,8 +207,8 @@ # %% # .. _`tut-reject-epochs-section`: # -# Rejecting Epochs based on channel amplitude -# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +# Rejecting Epochs based on peak-to-peak channel amplitude +# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ # # Besides "bad" annotations, the :class:`mne.Epochs` class constructor has # another means of rejecting epochs, based on signal amplitude thresholds for @@ -328,6 +330,108 @@ epochs.drop_bad(reject=stronger_reject_criteria) print(epochs.drop_log) +# %% +# .. _`tut-reject-epochs-func-section`: +# +# Rejecting Epochs using callables (functions) +# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +# Sometimes it is useful to reject epochs based criteria other than +# peak-to-peak amplitudes. For example, we might want to reject epochs +# based on the maximum or minimum amplitude of a channel. +# In this case, the `mne.Epochs.drop_bad` function also accepts +# callables (functions) in the ``reject`` and ``flat`` parameters. This +# allows us to define functions to reject epochs based on our desired criteria. +# +# Let's begin by generating Epoch data with large artifacts in one eeg channel +# in order to demonstrate the versatility of this approach. + +raw.crop(0, 5) +raw.del_proj() +chans = raw.info["ch_names"][-5:-1] +raw.pick(chans) +data = raw.get_data() + +new_data = data +new_data[0, 180:200] *= 1e3 +new_data[0, 460:580] += 1e-3 +edit_raw = mne.io.RawArray(new_data, raw.info) + +# Create fixed length epochs of 1 second +events = mne.make_fixed_length_events(edit_raw, id=1, duration=1.0, start=0) +epochs = mne.Epochs(edit_raw, events, tmin=0, tmax=1, baseline=None) +epochs.plot(scalings=dict(eeg=50e-5)) + +# %% +# As you can see, we have two large artifacts in the first channel. One large +# spike in amplitude and one large increase in amplitude. + +# Let's try to reject the epoch containing the spike in amplitude based on the +# maximum amplitude of the first channel. Please note that the callable in +# ``reject`` must return a (good, reason) tuple. Where the good must be bool +# and reason must be a str, list, or tuple where each entry is a str. + +epochs = mne.Epochs( + edit_raw, + events, + tmin=0, + tmax=1, + baseline=None, + preload=True, +) + +epochs.drop_bad( + reject=dict(eeg=lambda x: ((np.max(x, axis=1) > 1e-2).any(), "max amp")) +) +epochs.plot(scalings=dict(eeg=50e-5)) + +# %% +# Here, the epoch containing the spike in amplitude was rejected for having a +# maximum amplitude greater than 1e-2 Volts. Notice the use of the ``any()`` +# function to check if any of the channels exceeded the threshold. We could +# have also used the ``all()`` function to check if all channels exceeded the +# threshold. + +# Next, let's try to reject the epoch containing the increase in amplitude +# using the median. + +epochs = mne.Epochs( + edit_raw, + events, + tmin=0, + tmax=1, + baseline=None, + preload=True, +) + +epochs.drop_bad( + reject=dict(eeg=lambda x: ((np.median(x, axis=1) > 1e-4).any(), "median amp")) +) +epochs.plot(scalings=dict(eeg=50e-5)) + +# %% +# Finally, let's try to reject both epochs using a combination of the maximum +# and median. We'll define a custom function and use boolean operators to +# combine the two criteria. + + +def reject_criteria(x): + max_condition = np.max(x, axis=1) > 1e-2 + median_condition = np.median(x, axis=1) > 1e-4 + return ((max_condition.any() or median_condition.any()), ["max amp", "median amp"]) + + +epochs = mne.Epochs( + edit_raw, + events, + tmin=0, + tmax=1, + baseline=None, + preload=True, +) + +epochs.drop_bad(reject=dict(eeg=reject_criteria)) +epochs.plot(events=True) + # %% # Note that a complementary Python module, the `autoreject package`_, uses # machine learning to find optimal rejection criteria, and is designed to From d8ea2f5e60174d61301dbefbed9c76c9adc01ec9 Mon Sep 17 00:00:00 2001 From: Daniel McCloy Date: Fri, 2 Feb 2024 09:56:10 -0600 Subject: [PATCH 171/405] actually use GFP for EEG channels in plot_compare_evokeds (#12410) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- doc/changes/devel/12410.bugfix.rst | 1 + mne/time_frequency/spectrum.py | 6 +-- mne/utils/docs.py | 46 +++++++++++++++++++--- mne/utils/misc.py | 4 ++ mne/viz/epochs.py | 14 +------ mne/viz/evoked.py | 61 ++++++++++++++++++------------ mne/viz/tests/test_evoked.py | 33 +++++++++++----- mne/viz/utils.py | 57 ++++++++++++++++++++++------ 8 files changed, 153 insertions(+), 69 deletions(-) create mode 100644 doc/changes/devel/12410.bugfix.rst diff --git a/doc/changes/devel/12410.bugfix.rst b/doc/changes/devel/12410.bugfix.rst new file mode 100644 index 00000000000..c5d939845b0 --- /dev/null +++ b/doc/changes/devel/12410.bugfix.rst @@ -0,0 +1 @@ +In :func:`~mne.viz.plot_compare_evokeds`, actually plot GFP (not RMS amplitude) for EEG channels when global field power is requested by `Daniel McCloy`_. \ No newline at end of file diff --git a/mne/time_frequency/spectrum.py b/mne/time_frequency/spectrum.py index a7a2a753932..e46be389695 100644 --- a/mne/time_frequency/spectrum.py +++ b/mne/time_frequency/spectrum.py @@ -45,7 +45,7 @@ _is_numeric, check_fname, ) -from ..utils.misc import _pl +from ..utils.misc import _identity_function, _pl from ..utils.spectrum import _split_psd_kwargs from ..viz.topo import _plot_timeseries, _plot_timeseries_unified, _plot_topo from ..viz.topomap import _make_head_outlines, _prepare_topomap_plot, plot_psds_topomap @@ -60,10 +60,6 @@ from .psd import _check_nfft, psd_array_welch -def _identity_function(x): - return x - - class SpectrumMixin: """Mixin providing spectral plotting methods to sensor-space containers.""" diff --git a/mne/utils/docs.py b/mne/utils/docs.py index c3005427ead..87f457a982b 100644 --- a/mne/utils/docs.py +++ b/mne/utils/docs.py @@ -718,12 +718,46 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): 0 and 255. """ -docdict["combine"] = """ -combine : None | str | callable - How to combine information across channels. If a :class:`str`, must be - one of 'mean', 'median', 'std' (standard deviation) or 'gfp' (global - field power). -""" +_combine_template = """ +combine : 'mean' | {literals} | callable | None + How to aggregate across channels. If ``None``, {none}. If a string, + ``"mean"`` uses :func:`numpy.mean`, {other_string}. + If :func:`callable`, it must operate on an :class:`array ` + of shape ``({shape})`` and return an array of shape + ``({return_shape})``. {example} + {notes}Defaults to ``None``. +""" +_example = """For example:: + + combine = lambda data: np.median(data, axis=1) +""" +_median_std_gfp = """``"median"`` computes the `marginal median + `__, ``"std"`` + uses :func:`numpy.std`, and ``"gfp"`` computes global field power + for EEG channels and RMS amplitude for MEG channels""" +docdict["combine_plot_compare_evokeds"] = _combine_template.format( + literals="'median' | 'std' | 'gfp'", + none="""channels are combined by + computing GFP/RMS, unless ``picks`` is a single channel (not channel type) + or ``axes="topo"``, in which cases no combining is performed""", + other_string=_median_std_gfp, + shape="n_evokeds, n_channels, n_times", + return_shape="n_evokeds, n_times", + example=_example, + notes="", +) +docdict["combine_plot_epochs_image"] = _combine_template.format( + literals="'median' | 'std' | 'gfp'", + none="""channels are combined by + computing GFP/RMS, unless ``group_by`` is also ``None`` and ``picks`` is a + list of specific channels (not channel types), in which case no combining + is performed and each channel gets its own figure""", + other_string=_median_std_gfp, + shape="n_epochs, n_channels, n_times", + return_shape="n_epochs, n_times", + example=_example, + notes="See Notes for further details. ", +) docdict["compute_proj_ecg"] = """This function will: diff --git a/mne/utils/misc.py b/mne/utils/misc.py index 3f342c80570..2cebf8e5450 100644 --- a/mne/utils/misc.py +++ b/mne/utils/misc.py @@ -28,6 +28,10 @@ from .check import _check_option, _validate_type +def _identity_function(x): + return x + + # TODO: no longer needed when py3.9 is minimum supported version def _empty_hash(kind="md5"): func = getattr(hashlib, kind) diff --git a/mne/viz/epochs.py b/mne/viz/epochs.py index 95989637523..9871a0c2647 100644 --- a/mne/viz/epochs.py +++ b/mne/viz/epochs.py @@ -145,19 +145,7 @@ def plot_epochs_image( ``overlay_times`` should be ordered to correspond with the :class:`~mne.Epochs` object (i.e., ``overlay_times[0]`` corresponds to ``epochs[0]``, etc). - %(combine)s - If callable, the callable must accept one positional input (data of - shape ``(n_epochs, n_channels, n_times)``) and return an - :class:`array ` of shape ``(n_epochs, n_times)``. For - example:: - - combine = lambda data: np.median(data, axis=1) - - If ``combine`` is ``None``, channels are combined by computing GFP, - unless ``group_by`` is also ``None`` and ``picks`` is a list of - specific channels (not channel types), in which case no combining is - performed and each channel gets its own figure. See Notes for further - details. Defaults to ``None``. + %(combine_plot_epochs_image)s group_by : None | dict Specifies which channels are aggregated into a single figure, with aggregation method determined by the ``combine`` parameter. If not diff --git a/mne/viz/evoked.py b/mne/viz/evoked.py index 96976532767..f2a47fbe4d0 100644 --- a/mne/viz/evoked.py +++ b/mne/viz/evoked.py @@ -2484,14 +2484,22 @@ def _draw_axes_pce( ) -def _get_data_and_ci(evoked, combine, combine_func, picks, scaling=1, ci_fun=None): +def _get_data_and_ci( + evoked, combine, combine_func, ch_type, picks, scaling=1, ci_fun=None +): """Compute (sensor-aggregated, scaled) time series and possibly CI.""" picks = np.array(picks).flatten() # apply scalings data = np.array([evk.data[picks] * scaling for evk in evoked]) # combine across sensors if combine is not None: - logger.info(f'combining channels using "{combine}"') + if combine == "gfp" and ch_type == "eeg": + msg = f"GFP ({ch_type} channels)" + elif combine == "gfp" and ch_type in ("mag", "grad"): + msg = f"RMS ({ch_type} channels)" + else: + msg = f'"{combine}"' + logger.info(f"combining channels using {msg}") data = combine_func(data) # get confidence band if ci_fun is not None: @@ -2551,7 +2559,7 @@ def _plot_compare_evokeds( ax.set_title(title) -def _title_helper_pce(title, picked_types, picks, ch_names, combine): +def _title_helper_pce(title, picked_types, picks, ch_names, ch_type, combine): """Format title for plot_compare_evokeds.""" if title is None: title = ( @@ -2562,8 +2570,12 @@ def _title_helper_pce(title, picked_types, picks, ch_names, combine): # add the `combine` modifier do_combine = picked_types or len(ch_names) > 1 if title is not None and len(title) and isinstance(combine, str) and do_combine: - _comb = combine.upper() if combine == "gfp" else combine - _comb = "std. dev." if _comb == "std" else _comb + if combine == "gfp": + _comb = "RMS" if ch_type in ("mag", "grad") else "GFP" + elif combine == "std": + _comb = "std. dev." + else: + _comb = combine title += f" ({_comb})" return title @@ -2744,18 +2756,7 @@ def plot_compare_evokeds( value of the ``combine`` parameter. Defaults to ``None``. show : bool Whether to show the figure. Defaults to ``True``. - %(combine)s - If callable, the callable must accept one positional input (data of - shape ``(n_evokeds, n_channels, n_times)``) and return an - :class:`array ` of shape ``(n_epochs, n_times)``. For - example:: - - combine = lambda data: np.median(data, axis=1) - - If ``combine`` is ``None``, channels are combined by computing GFP, - unless ``picks`` is a single channel (not channel type) or - ``axes='topo'``, in which cases no combining is performed. Defaults to - ``None``. + %(combine_plot_compare_evokeds)s %(sphere_topomap_auto)s %(time_unit)s @@ -2914,11 +2915,19 @@ def plot_compare_evokeds( if combine is None and len(picks) > 1 and not do_topo: combine = "gfp" # convert `combine` into callable (if None or str) - combine_func = _make_combine_callable(combine) + combine_funcs = { + ch_type: _make_combine_callable(combine, ch_type=ch_type) + for ch_type in ch_types + } # title title = _title_helper_pce( - title, picked_types, picks=orig_picks, ch_names=ch_names, combine=combine + title, + picked_types, + picks=orig_picks, + ch_names=ch_names, + ch_type=ch_types[0] if len(ch_types) == 1 else None, + combine=combine, ) topo_disp_title = False # setup axes @@ -2943,9 +2952,7 @@ def plot_compare_evokeds( _validate_if_list_of_axes(axes, obligatory_len=len(ch_types)) if len(ch_types) > 1: - logger.info( - "Multiple channel types selected, returning one figure " "per type." - ) + logger.info("Multiple channel types selected, returning one figure per type.") figs = list() for ch_type, ax in zip(ch_types, axes): _picks = picks_by_type[ch_type] @@ -2954,7 +2961,12 @@ def plot_compare_evokeds( # don't pass `combine` here; title will run through this helper # function a second time & it will get added then _title = _title_helper_pce( - title, picked_types, picks=_picks, ch_names=_ch_names, combine=None + title, + picked_types, + picks=_picks, + ch_names=_ch_names, + ch_type=ch_type, + combine=None, ) figs.extend( plot_compare_evokeds( @@ -3003,7 +3015,7 @@ def plot_compare_evokeds( # some things that depend on ch_type: units = _handle_default("units")[ch_type] scalings = _handle_default("scalings")[ch_type] - + combine_func = combine_funcs[ch_type] # prep for topo pos_picks = picks # need this version of picks for sensor location inset info = pick_info(info, sel=picks, copy=True) @@ -3136,6 +3148,7 @@ def click_func( this_evokeds, combine, c_func, + ch_type=ch_type, picks=_picks, scaling=scalings, ci_fun=ci_fun, diff --git a/mne/viz/tests/test_evoked.py b/mne/viz/tests/test_evoked.py index 66609839df8..e177df6a9b8 100644 --- a/mne/viz/tests/test_evoked.py +++ b/mne/viz/tests/test_evoked.py @@ -402,21 +402,34 @@ def test_plot_white(): evoked_sss.plot_white(cov, time_unit="s") +@pytest.mark.parametrize( + "combine,vlines,title,picks", + ( + pytest.param(None, [0.1, 0.2], "MEG 0113", "MEG 0113", id="singlepick"), + pytest.param("mean", [], "(mean)", "mag", id="mag-mean"), + pytest.param("gfp", "auto", "(GFP)", "eeg", id="eeg-gfp"), + pytest.param(None, "auto", "(RMS)", ["MEG 0113", "MEG 0112"], id="meg-rms"), + pytest.param( + "std", "auto", "(std. dev.)", ["MEG 0113", "MEG 0112"], id="meg-std" + ), + pytest.param( + lambda x: np.min(x, axis=1), "auto", "MEG 0112", [0, 1], id="intpicks" + ), + ), +) +def test_plot_compare_evokeds_title(evoked, picks, vlines, combine, title): + """Test title generation by plot_compare_evokeds().""" + # test picks, combine, and vlines (1-channel pick also shows sensor inset) + fig = plot_compare_evokeds(evoked, picks=picks, vlines=vlines, combine=combine) + assert fig[0].axes[0].get_title().endswith(title) + + @pytest.mark.slowtest # slow on Azure -def test_plot_compare_evokeds(): +def test_plot_compare_evokeds(evoked): """Test plot_compare_evokeds.""" - evoked = _get_epochs().average() # test defaults figs = plot_compare_evokeds(evoked) assert len(figs) == 3 - # test picks, combine, and vlines (1-channel pick also shows sensor inset) - picks = ["MEG 0113", "mag"] + 2 * [["MEG 0113", "MEG 0112"]] + [[0, 1]] - vlines = [[0.1, 0.2], []] + 3 * ["auto"] - combine = [None, "mean", "std", None, lambda x: np.min(x, axis=1)] - title = ["MEG 0113", "(mean)", "(std. dev.)", "(GFP)", "MEG 0112"] - for _p, _v, _c, _t in zip(picks, vlines, combine, title): - fig = plot_compare_evokeds(evoked, picks=_p, vlines=_v, combine=_c) - assert fig[0].axes[0].get_title().endswith(_t) # test passing more than one evoked red, blue = evoked.copy(), evoked.copy() red.comment = red.comment + "*" * 100 diff --git a/mne/viz/utils.py b/mne/viz/utils.py index eeaf3d1098e..d325c474a16 100644 --- a/mne/viz/utils.py +++ b/mne/viz/utils.py @@ -46,6 +46,7 @@ ) from .._fiff.proj import Projection, setup_proj from ..defaults import _handle_default +from ..fixes import _median_complex from ..rank import compute_rank from ..transforms import apply_trans from ..utils import ( @@ -65,6 +66,7 @@ verbose, warn, ) +from ..utils.misc import _identity_function from .ui_events import ColormapRange, publish, subscribe _channel_type_prettyprint = { @@ -2328,30 +2330,63 @@ def _plot_masked_image( @fill_doc -def _make_combine_callable(combine): +def _make_combine_callable( + combine, + *, + axis=1, + valid=("mean", "median", "std", "gfp"), + ch_type=None, + keepdims=False, +): """Convert None or string values of ``combine`` into callables. Params ------ - %(combine)s - If callable, the callable must accept one positional input (data of - shape ``(n_epochs, n_channels, n_times)`` or ``(n_evokeds, n_channels, - n_times)``) and return an :class:`array ` of shape - ``(n_epochs, n_times)`` or ``(n_evokeds, n_times)``. + combine : None | str | callable + If callable, the callable must accept one positional input (a numpy array) and + return an array with one fewer dimensions (the missing dimension's position is + given by ``axis``). + axis : int + Axis of data array across which to combine. May vary depending on data + context; e.g., if data are time-domain sensor traces or TFRs, continuous + or epoched, etc. + valid : tuple + Valid string values for built-in combine methods + (may vary for, e.g., combining TFRs versus time-domain signals). + ch_type : str + Channel type. Affects whether "gfp" is allowed as a synonym for "rms". + keepdims : bool + Whether to retain the singleton dimension after collapsing across it. """ + kwargs = dict(axis=axis, keepdims=keepdims) if combine is None: - combine = partial(np.squeeze, axis=1) + combine = _identity_function if keepdims else partial(np.squeeze, axis=axis) elif isinstance(combine, str): combine_dict = { - key: partial(getattr(np, key), axis=1) for key in ("mean", "median", "std") + key: partial(getattr(np, key), **kwargs) + for key in valid + if getattr(np, key, None) is not None } - combine_dict["gfp"] = lambda data: np.sqrt((data**2).mean(axis=1)) + # marginal median that is safe for complex values: + if "median" in valid: + combine_dict["median"] = partial(_median_complex, axis=axis) + + # RMS and GFP; if GFP requested for MEG channels, will use RMS anyway + def _rms(data): + return np.sqrt((data**2).mean(**kwargs)) + + if "rms" in valid: + combine_dict["rms"] = _rms + if "gfp" in valid and ch_type == "eeg": + combine_dict["gfp"] = lambda data: data.std(axis=axis, ddof=0) + elif "gfp" in valid: + combine_dict["gfp"] = _rms try: combine = combine_dict[combine] except KeyError: raise ValueError( - '"combine" must be None, a callable, or one of "mean", "median", "std",' - f' or "gfp"; got {combine}' + f'"combine" must be None, a callable, or one of "{", ".join(valid)}"; ' + f'got {combine}' ) return combine From 78e840d4de0e1748cf32eab31de75887ec3e8082 Mon Sep 17 00:00:00 2001 From: Daniel McCloy Date: Fri, 2 Feb 2024 13:37:22 -0600 Subject: [PATCH 172/405] disable (mostly broken) "edit on GitHub" link in sidebar (#12412) Co-authored-by: Eric Larson --- doc/conf.py | 2 +- mne/epochs.py | 2 +- mne/utils/docs.py | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index 40222a265fe..7773be834fd 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -839,7 +839,7 @@ def append_attr_meth_examples(app, what, name, obj, options, lines): ), ], "icon_links_label": "External Links", # for screen reader - "use_edit_page_button": True, + "use_edit_page_button": False, "navigation_with_keys": False, "show_toc_level": 1, "article_header_start": [], # disable breadcrumbs diff --git a/mne/epochs.py b/mne/epochs.py index 1e86c6c96b0..952f0b27d96 100644 --- a/mne/epochs.py +++ b/mne/epochs.py @@ -1408,7 +1408,7 @@ def drop_bad(self, reject="existing", flat="existing", verbose=None): Dropping bad epochs can be done multiple times with different ``reject`` and ``flat`` parameters. However, once an epoch is dropped, it is dropped forever, so if more lenient thresholds may - subsequently be applied, `epochs.copy ` should be + subsequently be applied, :meth:`epochs.copy ` should be used. """ if reject == "existing": diff --git a/mne/utils/docs.py b/mne/utils/docs.py index 87f457a982b..17575f3a124 100644 --- a/mne/utils/docs.py +++ b/mne/utils/docs.py @@ -3366,6 +3366,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): .. note:: If ``reject`` is a callable, than **any** criteria can be used to reject epochs (including maxima and minima). + If ``reject`` is ``None``, no rejection is performed. If ``'existing'`` (default), then the rejection parameters set at instantiation are used. """ # noqa: E501 From 57611ae7142c3de407dcad60cffe110cc1c84c86 Mon Sep 17 00:00:00 2001 From: Clemens Brunner Date: Fri, 2 Feb 2024 20:38:25 +0100 Subject: [PATCH 173/405] Remove `jinja2` and `pooch` from `sys_info` (#12411) Co-authored-by: Daniel McCloy --- mne/utils/config.py | 10 ++++++++-- mne/utils/docs.py | 17 ++++++++++++----- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/mne/utils/config.py b/mne/utils/config.py index e74a61ccd9d..37a9f1aafd6 100644 --- a/mne/utils/config.py +++ b/mne/utils/config.py @@ -649,8 +649,6 @@ def sys_info( "numpy", "scipy", "matplotlib", - "pooch", - "jinja2", "", "# Numerical (optional)", "sklearn", @@ -701,6 +699,14 @@ def sys_info( "sphinx-gallery", "pydata-sphinx-theme", "", + "# Infrastructure", + "decorator", + "jinja2", + # "lazy-loader", + "packaging", + "pooch", + "tqdm", + "", ) try: unicode = unicode and (sys.stdout.encoding.lower().startswith("utf")) diff --git a/mne/utils/docs.py b/mne/utils/docs.py index 17575f3a124..746ec350081 100644 --- a/mne/utils/docs.py +++ b/mne/utils/docs.py @@ -3332,7 +3332,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): difference will be preserved. """ -docdict["reject_drop_bad"] = """ +docdict["reject_drop_bad"] = """\ reject : dict | str | None Reject epochs based on **maximum** peak-to-peak signal amplitude (PTP) or custom functions. Peak-to-peak signal amplitude is defined as @@ -3354,10 +3354,17 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): Custom rejection criteria can be also be used by passing a callable, e.g., to check for 99th percentile of absolute values of any channel - across time being bigger than 1mV. The callable must return a good, reason tuple. - Where good must be bool and reason must be str, list, or tuple where each entry is a str.:: - - reject = dict(eeg=lambda x: ((np.percentile(np.abs(x), 99, axis=1) > 1e-3).any(), "> 1mV somewhere")) + across time being bigger than :unit:`1 mV`. The callable must return a + ``(good, reason)`` tuple: ``good`` must be :class:`bool` and ``reason`` + must be :class:`str`, :class:`list`, or :class:`tuple` where each entry + is a :class:`str`:: + + reject = dict( + eeg=lambda x: ( + (np.percentile(np.abs(x), 99, axis=1) > 1e-3).any(), + "signal > 1 mV somewhere", + ) + ) .. note:: If rejection is based on a signal **difference** calculated for each channel separately, applying baseline From 0f5c91bbdfa0258c44c71ef8274cad22a90c13eb Mon Sep 17 00:00:00 2001 From: Kristijan Armeni Date: Fri, 2 Feb 2024 16:26:07 -0500 Subject: [PATCH 174/405] [FIX] remove AnalogSignalGap (#12417) --- mne/io/neuralynx/neuralynx.py | 59 +++++------------------------------ 1 file changed, 8 insertions(+), 51 deletions(-) diff --git a/mne/io/neuralynx/neuralynx.py b/mne/io/neuralynx/neuralynx.py index ab768d57b13..46bca5be27d 100644 --- a/mne/io/neuralynx/neuralynx.py +++ b/mne/io/neuralynx/neuralynx.py @@ -12,53 +12,6 @@ from ..base import BaseRaw -class AnalogSignalGap: - """Dummy object to represent gaps in Neuralynx data. - - Creates a AnalogSignalProxy-like object. - Propagate `signal`, `units`, and `sampling_rate` attributes - to the `AnalogSignal` init returned by `load()`. - - Parameters - ---------- - signal : array-like - Array of shape (n_samples, n_chans) containing the data. - units : str - Units of the data. (e.g., 'uV') - sampling_rate : quantity - Sampling rate of the data. (e.g., 4000 * pq.Hz) - - Returns - ------- - sig : instance of AnalogSignal - A AnalogSignal object representing a gap in Neuralynx data. - """ - - def __init__(self, signal, units, sampling_rate): - self.signal = signal - self.units = units - self.sampling_rate = sampling_rate - - def load(self, **kwargs): - """Return AnalogSignal object.""" - _soft_import("neo", "Reading NeuralynxIO files", strict=True) - from neo import AnalogSignal - - # `kwargs` is a dummy argument to mirror the - # AnalogSignalProxy.load() call signature which - # accepts `channel_indexes`` argument; but here we don't need - # any extra data selection arguments since - # self.signal array is already in the correct shape - # (channel dimension is based on `idx` variable) - - sig = AnalogSignal( - signal=self.signal, - units=self.units, - sampling_rate=self.sampling_rate, - ) - return sig - - @fill_doc def read_raw_neuralynx( fname, *, preload=False, exclude_fname_patterns=None, verbose=None @@ -258,8 +211,9 @@ def __init__( def _read_segment_file(self, data, idx, fi, start, stop, cals, mult): """Read a chunk of raw data.""" - from neo import Segment + from neo import AnalogSignal, Segment from neo.io import NeuralynxIO + from neo.io.proxyobjects import AnalogSignalProxy # quantities is a dependency of neo so we are guaranteed it exists from quantities import Hz @@ -338,7 +292,7 @@ def _read_segment_file(self, data, idx, fi, start, stop, cals, mult): ) for seg, n in zip(gap_segments, gap_samples): - asig = AnalogSignalGap( + asig = AnalogSignal( signal=np.zeros((n, n_chans)), units="uV", sampling_rate=sfreq * Hz ) seg.analogsignals.append(asig) @@ -351,13 +305,16 @@ def _read_segment_file(self, data, idx, fi, start, stop, cals, mult): segments_arr[~isgap] = neo_block[0].segments segments_arr[isgap] = gap_segments - # now load data from selected segments/channels via - # neo.Segment.AnalogSignal.load() or AnalogSignalGap.load() + # now load data for selected segments/channels via + # neo.Segment.AnalogSignalProxy.load() or + # pad directly as AnalogSignal.magnitude for any gap data all_data = np.concatenate( [ signal.load(channel_indexes=idx).magnitude[ samples[0] : samples[-1] + 1, : ] + if isinstance(signal, AnalogSignalProxy) + else signal.magnitude[samples[0] : samples[-1] + 1, :] for seg, samples in zip( segments_arr[first_seg : last_seg + 1], sel_samples_local ) From 4c84b3e1f94a2772deb363d4298a078e64f55139 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Feb 2024 14:49:20 +0000 Subject: [PATCH 175/405] Bump codecov/codecov-action from 3 to 4 (#12419) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 42bbaba2a21..e0cfb84ff76 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -129,5 +129,5 @@ jobs: path: ~/mne_data - run: ./tools/github_actions_download.sh - run: ./tools/github_actions_test.sh - - uses: codecov/codecov-action@v3 + - uses: codecov/codecov-action@v4 if: success() From 4cea4a9237867f3cfcdfdf9f7a67d7ee6b790f27 Mon Sep 17 00:00:00 2001 From: Jacob Woessner Date: Mon, 5 Feb 2024 09:09:46 -0600 Subject: [PATCH 176/405] BUG: Fix bug related to how Neuroscan .cnt n_samples is read (#12393) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Eric Larson --- doc/changes/devel/12393.bugfix.rst | 1 + mne/datasets/config.py | 4 ++-- mne/io/cnt/cnt.py | 25 +++++++++++++++++++++++-- mne/io/cnt/tests/test_cnt.py | 18 +++++++++++++++++- 4 files changed, 43 insertions(+), 5 deletions(-) create mode 100644 doc/changes/devel/12393.bugfix.rst diff --git a/doc/changes/devel/12393.bugfix.rst b/doc/changes/devel/12393.bugfix.rst new file mode 100644 index 00000000000..017f81b398b --- /dev/null +++ b/doc/changes/devel/12393.bugfix.rst @@ -0,0 +1 @@ +Change how samples are read when using ``data_format='auto'`` in :func:`mne.io.read_raw_cnt`, by `Jacob Woessner`_. \ No newline at end of file diff --git a/mne/datasets/config.py b/mne/datasets/config.py index 238b61998d6..fb9a04e1e40 100644 --- a/mne/datasets/config.py +++ b/mne/datasets/config.py @@ -89,7 +89,7 @@ # update the checksum in the MNE_DATASETS dict below, and change version # here: ↓↓↓↓↓↓↓↓ RELEASES = dict( - testing="0.151", + testing="0.152", misc="0.27", phantom_kit="0.2", ) @@ -116,7 +116,7 @@ # Testing and misc are at the top as they're updated most often MNE_DATASETS["testing"] = dict( archive_name=f"{TESTING_VERSIONED}.tar.gz", - hash="md5:5832b4d44f0423d22305fa61cb75bc25", + hash="md5:df48cdabcf13ebeaafc617cb8e55b6fc", url=( "https://codeload.github.com/mne-tools/mne-testing-data/" f'tar.gz/{RELEASES["testing"]}' diff --git a/mne/io/cnt/cnt.py b/mne/io/cnt/cnt.py index e217324f437..c695dfb0e86 100644 --- a/mne/io/cnt/cnt.py +++ b/mne/io/cnt/cnt.py @@ -309,7 +309,8 @@ def _get_cnt_info(input_fname, eog, ecg, emg, misc, data_format, date_format, he # Header has a field for number of samples, but it does not seem to be # too reliable. That's why we have option for setting n_bytes manually. fid.seek(864) - n_samples = np.fromfile(fid, dtype=" n_samples: + n_bytes = 4 + n_samples = n_samples_header + warn( + "Annotations are outside data range. " + "Changing data format to 'int32'." + ) else: n_bytes = data_size // (n_samples * n_channels) else: n_bytes = 2 if data_format == "int16" else 4 n_samples = data_size // (n_bytes * n_channels) + # See PR #12393 + if n_samples_header != 0: + n_samples = n_samples_header # Channel offset refers to the size of blocks per channel in the file. cnt_info["channel_offset"] = np.fromfile(fid, dtype=" 1: @@ -548,6 +561,7 @@ def _read_segment_file(self, data, idx, fi, start, stop, cals, mult): channel_offset = self._raw_extras[fi]["channel_offset"] baselines = self._raw_extras[fi]["baselines"] n_bytes = self._raw_extras[fi]["n_bytes"] + n_samples = self._raw_extras[fi]["n_samples"] dtype = " Date: Mon, 5 Feb 2024 21:23:01 +0000 Subject: [PATCH 177/405] [pre-commit.ci] pre-commit autoupdate (#12421) 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 cfc33cc5ceb..9c8bae040f4 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.1.14 + rev: v0.2.0 hooks: - id: ruff name: ruff lint mne From 87df00d63991a51835992e054d690aa67ccca7da Mon Sep 17 00:00:00 2001 From: Dominik Welke Date: Tue, 6 Feb 2024 18:06:41 +0100 Subject: [PATCH 178/405] ENH: make apply_function aware of channel index (#12206) Co-authored-by: Mathieu Scheltienne --- doc/changes/devel/12206.bugfix.rst | 1 + doc/changes/devel/12206.newfeature.rst | 3 ++ mne/epochs.py | 45 ++++++++++++++--- mne/evoked.py | 70 +++++++++++++++++++++----- mne/io/base.py | 42 ++++++++++++++-- mne/io/tests/test_apply_function.py | 29 +++++++++++ mne/tests/test_epochs.py | 33 ++++++++++++ mne/tests/test_evoked.py | 30 +++++++++++ mne/utils/docs.py | 7 +++ 9 files changed, 236 insertions(+), 24 deletions(-) create mode 100644 doc/changes/devel/12206.bugfix.rst create mode 100644 doc/changes/devel/12206.newfeature.rst diff --git a/doc/changes/devel/12206.bugfix.rst b/doc/changes/devel/12206.bugfix.rst new file mode 100644 index 00000000000..6cf72e266b9 --- /dev/null +++ b/doc/changes/devel/12206.bugfix.rst @@ -0,0 +1 @@ +Fix bug in :meth:`mne.Epochs.apply_function` where data was handed down incorrectly in parallel processing, by `Dominik Welke`_. \ No newline at end of file diff --git a/doc/changes/devel/12206.newfeature.rst b/doc/changes/devel/12206.newfeature.rst new file mode 100644 index 00000000000..9ef966ed208 --- /dev/null +++ b/doc/changes/devel/12206.newfeature.rst @@ -0,0 +1,3 @@ +Custom functions applied via :meth:`mne.io.Raw.apply_function`, :meth:`mne.Epochs.apply_function` or :meth:`mne.Evoked.apply_function` can now use ``ch_idx`` or ``ch_name`` to get access to the currently processed channel during channel wise processing. + +:meth:`mne.Evoked.apply_function` can now also work on full data array instead of just channel wise, analogous to :meth:`mne.io.Raw.apply_function` and :meth:`mne.Epochs.apply_function`, by `Dominik Welke`_. \ No newline at end of file diff --git a/mne/epochs.py b/mne/epochs.py index 952f0b27d96..2f4bea9cec9 100644 --- a/mne/epochs.py +++ b/mne/epochs.py @@ -16,6 +16,7 @@ from collections import Counter from copy import deepcopy from functools import partial +from inspect import getfullargspec import numpy as np from scipy.interpolate import interp1d @@ -1972,22 +1973,52 @@ def apply_function( if dtype is not None and dtype != self._data.dtype: self._data = self._data.astype(dtype) + args = getfullargspec(fun).args + getfullargspec(fun).kwonlyargs + if channel_wise is False: + if ("ch_idx" in args) or ("ch_name" in args): + raise ValueError( + "apply_function cannot access ch_idx or ch_name " + "when channel_wise=False" + ) + if "ch_idx" in args: + logger.info("apply_function requested to access ch_idx") + if "ch_name" in args: + logger.info("apply_function requested to access ch_name") + if channel_wise: parallel, p_fun, n_jobs = parallel_func(_check_fun, n_jobs) if n_jobs == 1: - _fun = partial(_check_fun, fun, **kwargs) + _fun = partial(_check_fun, fun) # modify data inplace to save memory - for idx in picks: - self._data[:, idx, :] = np.apply_along_axis( - _fun, -1, data_in[:, idx, :] + for ch_idx in picks: + if "ch_idx" in args: + kwargs.update(ch_idx=ch_idx) + if "ch_name" in args: + kwargs.update(ch_name=self.info["ch_names"][ch_idx]) + self._data[:, ch_idx, :] = np.apply_along_axis( + _fun, -1, data_in[:, ch_idx, :], **kwargs ) else: # use parallel function + _fun = partial(np.apply_along_axis, fun, -1) data_picks_new = parallel( - p_fun(fun, data_in[:, p, :], **kwargs) for p in picks + p_fun( + _fun, + data_in[:, ch_idx, :], + **kwargs, + **{ + k: v + for k, v in [ + ("ch_name", self.info["ch_names"][ch_idx]), + ("ch_idx", ch_idx), + ] + if k in args + }, + ) + for ch_idx in picks ) - for pp, p in enumerate(picks): - self._data[:, p, :] = data_picks_new[pp] + for run_idx, ch_idx in enumerate(picks): + self._data[:, ch_idx, :] = data_picks_new[run_idx] else: self._data = _check_fun(fun, data_in, **kwargs) diff --git a/mne/evoked.py b/mne/evoked.py index 1f694f7c11b..36831db8ce0 100644 --- a/mne/evoked.py +++ b/mne/evoked.py @@ -9,6 +9,7 @@ # Copyright the MNE-Python contributors. from copy import deepcopy +from inspect import getfullargspec from typing import Union import numpy as np @@ -258,7 +259,15 @@ def get_data(self, picks=None, units=None, tmin=None, tmax=None): @verbose def apply_function( - self, fun, picks=None, dtype=None, n_jobs=None, verbose=None, **kwargs + self, + fun, + picks=None, + dtype=None, + n_jobs=None, + channel_wise=True, + *, + verbose=None, + **kwargs, ): """Apply a function to a subset of channels. @@ -271,6 +280,9 @@ def apply_function( %(dtype_applyfun)s %(n_jobs)s Ignored if ``channel_wise=False`` as the workload is split across channels. + %(channel_wise_applyfun)s + + .. versionadded:: 1.6 %(verbose)s %(kwargs_fun)s @@ -289,21 +301,55 @@ def apply_function( if dtype is not None and dtype != self._data.dtype: self._data = self._data.astype(dtype) + args = getfullargspec(fun).args + getfullargspec(fun).kwonlyargs + if channel_wise is False: + if ("ch_idx" in args) or ("ch_name" in args): + raise ValueError( + "apply_function cannot access ch_idx or ch_name " + "when channel_wise=False" + ) + if "ch_idx" in args: + logger.info("apply_function requested to access ch_idx") + if "ch_name" in args: + logger.info("apply_function requested to access ch_name") + # check the dimension of the incoming evoked data _check_option("evoked.ndim", self._data.ndim, [2]) - parallel, p_fun, n_jobs = parallel_func(_check_fun, n_jobs) - if n_jobs == 1: - # modify data inplace to save memory - for idx in picks: - self._data[idx, :] = _check_fun(fun, data_in[idx, :], **kwargs) + if channel_wise: + parallel, p_fun, n_jobs = parallel_func(_check_fun, n_jobs) + if n_jobs == 1: + # modify data inplace to save memory + for ch_idx in picks: + if "ch_idx" in args: + kwargs.update(ch_idx=ch_idx) + if "ch_name" in args: + kwargs.update(ch_name=self.info["ch_names"][ch_idx]) + self._data[ch_idx, :] = _check_fun( + fun, data_in[ch_idx, :], **kwargs + ) + else: + # use parallel function + data_picks_new = parallel( + p_fun( + fun, + data_in[ch_idx, :], + **kwargs, + **{ + k: v + for k, v in [ + ("ch_name", self.info["ch_names"][ch_idx]), + ("ch_idx", ch_idx), + ] + if k in args + }, + ) + for ch_idx in picks + ) + for run_idx, ch_idx in enumerate(picks): + self._data[ch_idx, :] = data_picks_new[run_idx] else: - # use parallel function - data_picks_new = parallel( - p_fun(fun, data_in[p, :], **kwargs) for p in picks - ) - for pp, p in enumerate(picks): - self._data[p, :] = data_picks_new[pp] + self._data[picks, :] = _check_fun(fun, data_in[picks, :], **kwargs) return self diff --git a/mne/io/base.py b/mne/io/base.py index bb40075335c..4fe7975e1cd 100644 --- a/mne/io/base.py +++ b/mne/io/base.py @@ -18,6 +18,7 @@ from copy import deepcopy from dataclasses import dataclass, field from datetime import timedelta +from inspect import getfullargspec import numpy as np @@ -1087,19 +1088,50 @@ def apply_function( if dtype is not None and dtype != self._data.dtype: self._data = self._data.astype(dtype) + args = getfullargspec(fun).args + getfullargspec(fun).kwonlyargs + if channel_wise is False: + if ("ch_idx" in args) or ("ch_name" in args): + raise ValueError( + "apply_function cannot access ch_idx or ch_name " + "when channel_wise=False" + ) + if "ch_idx" in args: + logger.info("apply_function requested to access ch_idx") + if "ch_name" in args: + logger.info("apply_function requested to access ch_name") + if channel_wise: parallel, p_fun, n_jobs = parallel_func(_check_fun, n_jobs) if n_jobs == 1: # modify data inplace to save memory - for idx in picks: - self._data[idx, :] = _check_fun(fun, data_in[idx, :], **kwargs) + for ch_idx in picks: + if "ch_idx" in args: + kwargs.update(ch_idx=ch_idx) + if "ch_name" in args: + kwargs.update(ch_name=self.info["ch_names"][ch_idx]) + self._data[ch_idx, :] = _check_fun( + fun, data_in[ch_idx, :], **kwargs + ) else: # use parallel function data_picks_new = parallel( - p_fun(fun, data_in[p], **kwargs) for p in picks + p_fun( + fun, + data_in[ch_idx], + **kwargs, + **{ + k: v + for k, v in [ + ("ch_name", self.info["ch_names"][ch_idx]), + ("ch_idx", ch_idx), + ] + if k in args + }, + ) + for ch_idx in picks ) - for pp, p in enumerate(picks): - self._data[p, :] = data_picks_new[pp] + for run_idx, ch_idx in enumerate(picks): + self._data[ch_idx, :] = data_picks_new[run_idx] else: self._data[picks, :] = _check_fun(fun, data_in[picks, :], **kwargs) diff --git a/mne/io/tests/test_apply_function.py b/mne/io/tests/test_apply_function.py index b1869e1dae6..f250e9489b9 100644 --- a/mne/io/tests/test_apply_function.py +++ b/mne/io/tests/test_apply_function.py @@ -63,3 +63,32 @@ def test_apply_function_verbose(): assert out is raw raw.apply_function(printer, verbose=True) assert sio.getvalue().count("\n") == n_chan + + +def test_apply_function_ch_access(): + """Test apply_function is able to access channel idx.""" + + def _bad_ch_idx(x, ch_idx): + assert x[0] == ch_idx + return x + + def _bad_ch_name(x, ch_name): + assert isinstance(ch_name, str) + assert x[0] == float(ch_name) + return x + + data = np.full((2, 10), np.arange(2).reshape(-1, 1)) + raw = RawArray(data, create_info(2, 1.0, "mag")) + + # test ch_idx access in both code paths (parallel / 1 job) + raw.apply_function(_bad_ch_idx) + raw.apply_function(_bad_ch_idx, n_jobs=2) + raw.apply_function(_bad_ch_name) + raw.apply_function(_bad_ch_name, n_jobs=2) + + # test input catches + with pytest.raises( + ValueError, + match="cannot access.*when channel_wise=False", + ): + raw.apply_function(_bad_ch_idx, channel_wise=False) diff --git a/mne/tests/test_epochs.py b/mne/tests/test_epochs.py index 96d90414e07..edb2b4967d2 100644 --- a/mne/tests/test_epochs.py +++ b/mne/tests/test_epochs.py @@ -4764,6 +4764,39 @@ def fun(data): assert_array_equal(out.get_data(non_picks), epochs.get_data(non_picks)) +def test_apply_function_epo_ch_access(): + """Test ch-access within apply function to epoch objects.""" + + def _bad_ch_idx(x, ch_idx): + assert x.shape == (46,) + assert x[0] == ch_idx + return x + + def _bad_ch_name(x, ch_name): + assert x.shape == (46,) + assert isinstance(ch_name, str) + assert x[0] == float(ch_name) + return x + + data = np.full((2, 100), np.arange(2).reshape(-1, 1)) + raw = RawArray(data, create_info(2, 1.0, "mag")) + ev = np.array([[0, 0, 33], [50, 0, 33]]) + ep = Epochs(raw, ev, tmin=0, tmax=45, baseline=None, preload=True) + + # test ch_idx access in both code paths (parallel / 1 job) + ep.apply_function(_bad_ch_idx) + ep.apply_function(_bad_ch_idx, n_jobs=2) + ep.apply_function(_bad_ch_name) + ep.apply_function(_bad_ch_name, n_jobs=2) + + # test input catches + with pytest.raises( + ValueError, + match="cannot access.*when channel_wise=False", + ): + ep.apply_function(_bad_ch_idx, channel_wise=False) + + @testing.requires_testing_data def test_add_channels_picks(): """Check that add_channels properly deals with picks.""" diff --git a/mne/tests/test_evoked.py b/mne/tests/test_evoked.py index b5f686c43c3..fbf4c012334 100644 --- a/mne/tests/test_evoked.py +++ b/mne/tests/test_evoked.py @@ -959,3 +959,33 @@ def fun(data, multiplier): applied = evoked.apply_function(fun, n_jobs=None, multiplier=mult) assert np.shape(applied.data) == np.shape(evoked_data) assert np.equal(applied.data, evoked_data * mult).all() + + +def test_apply_function_evk_ch_access(): + """Check ch-access within the apply_function method for evoked data.""" + + def _bad_ch_idx(x, ch_idx): + assert x[0] == ch_idx + return x + + def _bad_ch_name(x, ch_name): + assert isinstance(ch_name, str) + assert x[0] == float(ch_name) + return x + + # create fake evoked data to use for checking apply_function + data = np.full((2, 100), np.arange(2).reshape(-1, 1)) + evoked = EvokedArray(data, create_info(2, 1000.0, "eeg")) + + # test ch_idx access in both code paths (parallel / 1 job) + evoked.apply_function(_bad_ch_idx) + evoked.apply_function(_bad_ch_idx, n_jobs=2) + evoked.apply_function(_bad_ch_name) + evoked.apply_function(_bad_ch_name, n_jobs=2) + + # test input catches + with pytest.raises( + ValueError, + match="cannot access.*when channel_wise=False", + ): + evoked.apply_function(_bad_ch_idx, channel_wise=False) diff --git a/mne/utils/docs.py b/mne/utils/docs.py index 746ec350081..cb7c027c039 100644 --- a/mne/utils/docs.py +++ b/mne/utils/docs.py @@ -1586,6 +1586,13 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): fun has to be a timeseries (:class:`numpy.ndarray`). The function must operate on an array of shape ``(n_times,)`` {}. The function must return an :class:`~numpy.ndarray` shaped like its input. + + .. note:: + If ``channel_wise=True``, one can optionally access the index and/or the + name of the currently processed channel within the applied function. + This can enable tailored computations for different channels. + To use this feature, add ``ch_idx`` and/or ``ch_name`` as + additional argument(s) to your function definition. """ docdict["fun_applyfun"] = applyfun_fun_base.format( " if ``channel_wise=True`` and ``(len(picks), n_times)`` otherwise" From acab264e3d844cce5dfa9fadb49cc55352a261bc Mon Sep 17 00:00:00 2001 From: Scott Huberty <52462026+scott-huberty@users.noreply.github.com> Date: Tue, 6 Feb 2024 09:35:17 -0800 Subject: [PATCH 179/405] Function to convert eyegaze units to radians (#12237) Co-authored-by: Eric Larson Co-authored-by: Daniel McCloy Co-authored-by: Britta Westner --- .pre-commit-config.yaml | 10 +- doc/api/preprocessing.rst | 2 + doc/changes/devel/12237.newfeature.rst | 2 + doc/conf.py | 14 +- doc/sphinxext/contrib_avatars.py | 2 +- doc/sphinxext/gen_commands.py | 3 +- doc/sphinxext/gen_names.py | 2 +- doc/sphinxext/gh_substitutions.py | 2 +- doc/sphinxext/mne_substitutions.py | 6 +- doc/sphinxext/newcontrib_substitutions.py | 2 +- doc/sphinxext/unit_role.py | 2 +- .../visualization/eyetracking_plot_heatmap.py | 29 ++- mne/conftest.py | 51 ++++++ mne/preprocessing/eyetracking/__init__.py | 3 +- .../eyetracking/_pupillometry.py | 8 + mne/preprocessing/eyetracking/eyetracking.py | 165 ++++++++++++++++++ .../eyetracking/tests/test_eyetracking.py | 78 +++++++++ mne/preprocessing/eyetracking/utils.py | 41 +++++ mne/viz/__init__.pyi | 3 +- mne/viz/eyetracking/heatmap.py | 84 +++++++-- mne/viz/eyetracking/tests/test_heatmap.py | 62 +++++-- 21 files changed, 509 insertions(+), 62 deletions(-) create mode 100644 doc/changes/devel/12237.newfeature.rst create mode 100644 mne/preprocessing/eyetracking/tests/test_eyetracking.py create mode 100644 mne/preprocessing/eyetracking/utils.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9c8bae040f4..9a81e895d4c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,18 +11,14 @@ repos: name: ruff lint mne preview args: ["--fix", "--preview", "--select=NPY201"] files: ^mne/ - - id: ruff-format - name: ruff format mne - files: ^mne/ - id: ruff - name: ruff lint tutorials and examples + name: ruff lint doc, tutorials, and examples # D103: missing docstring in public function # D400: docstring first line must end with period args: ["--ignore=D103,D400", "--fix"] - files: ^tutorials/|^examples/ + files: ^doc/|^tutorials/|^examples/ - id: ruff-format - name: ruff format tutorials and examples - files: ^tutorials/|^examples/ + files: ^mne/|^doc/|^tutorials/|^examples/ # Codespell - repo: https://github.com/codespell-project/codespell diff --git a/doc/api/preprocessing.rst b/doc/api/preprocessing.rst index 54d4bfa2999..f5271a1edee 100644 --- a/doc/api/preprocessing.rst +++ b/doc/api/preprocessing.rst @@ -162,6 +162,8 @@ Projections: Calibration read_eyelink_calibration set_channel_types_eyetrack + convert_units + get_screen_visual_angle interpolate_blinks EEG referencing: diff --git a/doc/changes/devel/12237.newfeature.rst b/doc/changes/devel/12237.newfeature.rst new file mode 100644 index 00000000000..e89822f27ed --- /dev/null +++ b/doc/changes/devel/12237.newfeature.rst @@ -0,0 +1,2 @@ +Added a helper function :func:`mne.preprocessing.eyetracking.convert_units` to convert eyegaze data from pixel-on-screen values to radians of visual angle. Also added a helper function :func:`mne.preprocessing.eyetracking.get_screen_visual_angle` to get the visual angle that the participant screen subtends, by `Scott Huberty`_. + diff --git a/doc/conf.py b/doc/conf.py index 7773be834fd..03d3961151a 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -29,12 +29,13 @@ from mne.tests.test_docstring_parameters import error_ignores from mne.utils import ( _assert_no_instances, - linkcode_resolve, # noqa, analysis:ignore + 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") faulthandler.enable() os.environ["_MNE_BROWSER_NO_BLOCK"] = "true" @@ -62,12 +63,12 @@ # We need to triage which date type we use so that incremental builds work # (Sphinx looks at variable changes and rewrites all files if some change) -copyright = ( +copyright = ( # noqa: A001 f'2012–{td.year}, MNE Developers. Last updated \n' # noqa: E501 '' # noqa: E501 ) if os.getenv("MNE_FULL_DATE", "false").lower() != "true": - copyright = f"2012–{td.year}, MNE Developers. Last updated locally." + copyright = f"2012–{td.year}, MNE Developers. Last updated locally." # noqa: A001 # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -293,6 +294,7 @@ "RawNIRX": "mne.io.Raw", "RawPersyst": "mne.io.Raw", "RawSNIRF": "mne.io.Raw", + "Calibration": "mne.preprocessing.eyetracking.Calibration", # dipy "dipy.align.AffineMap": "dipy.align.imaffine.AffineMap", "dipy.align.DiffeomorphicMap": "dipy.align.imwarp.DiffeomorphicMap", @@ -445,16 +447,18 @@ # -- Sphinx-gallery configuration -------------------------------------------- -class Resetter(object): +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: @@ -1753,7 +1757,7 @@ def reset_warnings(gallery_conf, fname): def check_existing_redirect(path): """Make sure existing HTML files are redirects, before overwriting.""" if path.is_file(): - with open(path, "r") as fid: + with open(path) as fid: for _ in range(8): next(fid) line = fid.readline() diff --git a/doc/sphinxext/contrib_avatars.py b/doc/sphinxext/contrib_avatars.py index 5082618a9be..04583ac4c77 100644 --- a/doc/sphinxext/contrib_avatars.py +++ b/doc/sphinxext/contrib_avatars.py @@ -15,9 +15,9 @@ def generate_contrib_avatars(app, config): MNE_ADD_CONTRIBUTOR_IMAGE=true in your environment to generate it.

""" else: from selenium import webdriver + from selenium.common.exceptions import WebDriverException from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait - from selenium.common.exceptions import WebDriverException try: options = webdriver.ChromeOptions() diff --git a/doc/sphinxext/gen_commands.py b/doc/sphinxext/gen_commands.py index 5fa9cd7418a..e50e243eb48 100644 --- a/doc/sphinxext/gen_commands.py +++ b/doc/sphinxext/gen_commands.py @@ -2,10 +2,9 @@ # Copyright the MNE-Python contributors. import glob from importlib import import_module -import os from pathlib import Path -from mne.utils import _replace_md5, ArgvSetter +from mne.utils import ArgvSetter, _replace_md5 def setup(app): diff --git a/doc/sphinxext/gen_names.py b/doc/sphinxext/gen_names.py index 1871ae0068c..fd667ec0951 100644 --- a/doc/sphinxext/gen_names.py +++ b/doc/sphinxext/gen_names.py @@ -25,7 +25,7 @@ def generate_name_links_rst(app=None): ) with open(out_fname, "w", encoding="utf8") as fout: fout.write(":orphan:\n\n") - with open(names_path, "r") as fin: + with open(names_path) as fin: for line in fin: if line.startswith(".. _"): fout.write(f"- {line[4:]}") diff --git a/doc/sphinxext/gh_substitutions.py b/doc/sphinxext/gh_substitutions.py index bccc16d13d0..890a71f1c47 100644 --- a/doc/sphinxext/gh_substitutions.py +++ b/doc/sphinxext/gh_substitutions.py @@ -4,7 +4,7 @@ from docutils.parsers.rst.roles import set_classes -def gh_role(name, rawtext, text, lineno, inliner, options={}, content=[]): +def gh_role(name, rawtext, text, lineno, inliner, options={}, content=[]): # noqa: B006 """Link to a GitHub issue. adapted from diff --git a/doc/sphinxext/mne_substitutions.py b/doc/sphinxext/mne_substitutions.py index 6a5cdbb6797..bd415fc67f9 100644 --- a/doc/sphinxext/mne_substitutions.py +++ b/doc/sphinxext/mne_substitutions.py @@ -4,12 +4,12 @@ from docutils.parsers.rst import Directive from docutils.statemachine import StringList -from mne.defaults import DEFAULTS from mne._fiff.pick import ( - _PICK_TYPES_DATA_DICT, - _DATA_CH_TYPES_SPLIT, _DATA_CH_TYPES_ORDER_DEFAULT, + _DATA_CH_TYPES_SPLIT, + _PICK_TYPES_DATA_DICT, ) +from mne.defaults import DEFAULTS class MNESubstitution(Directive): # noqa: D101 diff --git a/doc/sphinxext/newcontrib_substitutions.py b/doc/sphinxext/newcontrib_substitutions.py index 41cf348c7c4..c38aeb86219 100644 --- a/doc/sphinxext/newcontrib_substitutions.py +++ b/doc/sphinxext/newcontrib_substitutions.py @@ -3,7 +3,7 @@ from docutils.nodes import reference, strong, target -def newcontrib_role(name, rawtext, text, lineno, inliner, options={}, content=[]): +def newcontrib_role(name, rawtext, text, lineno, inliner, options={}, content=[]): # noqa: B006 """Create a role to highlight new contributors in changelog entries.""" newcontrib = f"new contributor {text}" alias_text = f" <{text}_>" diff --git a/doc/sphinxext/unit_role.py b/doc/sphinxext/unit_role.py index b882aedc6b1..89b7543548c 100644 --- a/doc/sphinxext/unit_role.py +++ b/doc/sphinxext/unit_role.py @@ -3,7 +3,7 @@ from docutils import nodes -def unit_role(name, rawtext, text, lineno, inliner, options={}, content=[]): +def unit_role(name, rawtext, text, lineno, inliner, options={}, content=[]): # noqa: B006 parts = text.split() def pass_error_to_sphinx(rawtext, text, lineno, inliner): diff --git a/examples/visualization/eyetracking_plot_heatmap.py b/examples/visualization/eyetracking_plot_heatmap.py index e1826efb6f7..bbfb9b13739 100644 --- a/examples/visualization/eyetracking_plot_heatmap.py +++ b/examples/visualization/eyetracking_plot_heatmap.py @@ -35,6 +35,12 @@ stim_fpath = task_fpath / "stim" / "naturalistic.png" raw = mne.io.read_raw_eyelink(et_fpath) +calibration = mne.preprocessing.eyetracking.read_eyelink_calibration( + et_fpath, + screen_resolution=(1920, 1080), + screen_size=(0.53, 0.3), + screen_distance=0.9, +)[0] # %% # Process and epoch the data @@ -58,9 +64,8 @@ # screen resolution of the participant screen (1920x1080) as the width and height. We # can also use the sigma parameter to smooth the plot. -px_width, px_height = 1920, 1080 cmap = plt.get_cmap("viridis") -plot_gaze(epochs["natural"], width=px_width, height=px_height, cmap=cmap, sigma=50) +plot_gaze(epochs["natural"], calibration=calibration, cmap=cmap, sigma=50) # %% # Overlaying plots with images @@ -77,10 +82,26 @@ ax.imshow(plt.imread(stim_fpath)) plot_gaze( epochs["natural"], - width=px_width, - height=px_height, + calibration=calibration, vlim=(0.0003, None), sigma=50, cmap=cmap, axes=ax, ) + +# %% +# Displaying the heatmap in units of visual angle +# ----------------------------------------------- +# +# In scientific publications it is common to report gaze data as the visual angle +# from the participants eye to the screen. We can convert the units of our gaze data to +# radians of visual angle before plotting the heatmap: + +# %% +epochs.load_data() +mne.preprocessing.eyetracking.convert_units(epochs, calibration, to="radians") +plot_gaze( + epochs["natural"], + calibration=calibration, + sigma=50, +) diff --git a/mne/conftest.py b/mne/conftest.py index 4bab9dc1186..80380d1a387 100644 --- a/mne/conftest.py +++ b/mne/conftest.py @@ -199,6 +199,7 @@ def pytest_configure(config): ignore:Python 3\.14 will, by default, filter extracted tar archives.*:DeprecationWarning # pandas ignore:\n*Pyarrow will become a required dependency of pandas.*:DeprecationWarning + ignore:np\.find_common_type is deprecated.*:DeprecationWarning # pyvista <-> NumPy 2.0 ignore:__array_wrap__ must accept context and return_scalar arguments.*:DeprecationWarning """ # noqa: E501 @@ -1179,3 +1180,53 @@ def pytest_runtest_makereport(item, call): outcome = yield rep = outcome.get_result() item.stash.setdefault(_phase_report_key, {})[rep.when] = rep + + +@pytest.fixture(scope="function") +def eyetrack_cal(): + """Create a toy calibration instance.""" + screen_size = (0.4, 0.225) # width, height in meters + screen_resolution = (1920, 1080) + screen_distance = 0.7 # meters + onset = 0 + model = "HV9" + eye = "R" + avg_error = 0.5 + max_error = 1.0 + positions = np.zeros((9, 2)) + offsets = np.zeros((9,)) + gaze = np.zeros((9, 2)) + cal = mne.preprocessing.eyetracking.Calibration( + screen_size=screen_size, + screen_distance=screen_distance, + screen_resolution=screen_resolution, + eye=eye, + model=model, + positions=positions, + offsets=offsets, + gaze=gaze, + onset=onset, + avg_error=avg_error, + max_error=max_error, + ) + return cal + + +@pytest.fixture(scope="function") +def eyetrack_raw(): + """Create a toy raw instance with eyetracking channels.""" + # simulate a steady fixation at the center pixel of a 1920x1080 resolution screen + shape = (1, 100) # x or y, time + data = np.vstack([np.full(shape, 960), np.full(shape, 540), np.full(shape, 0)]) + + info = info = mne.create_info( + ch_names=["xpos", "ypos", "pupil"], sfreq=100, ch_types="eyegaze" + ) + more_info = dict( + xpos=("eyegaze", "px", "right", "x"), + ypos=("eyegaze", "px", "right", "y"), + pupil=("pupil", "au", "right"), + ) + raw = mne.io.RawArray(data, info) + raw = mne.preprocessing.eyetracking.set_channel_types_eyetrack(raw, more_info) + return raw diff --git a/mne/preprocessing/eyetracking/__init__.py b/mne/preprocessing/eyetracking/__init__.py index 01a30bf4436..efab0fb079d 100644 --- a/mne/preprocessing/eyetracking/__init__.py +++ b/mne/preprocessing/eyetracking/__init__.py @@ -5,6 +5,7 @@ # License: BSD-3-Clause # Copyright the MNE-Python contributors. -from .eyetracking import set_channel_types_eyetrack +from .eyetracking import set_channel_types_eyetrack, convert_units from .calibration import Calibration, read_eyelink_calibration from ._pupillometry import interpolate_blinks +from .utils import get_screen_visual_angle diff --git a/mne/preprocessing/eyetracking/_pupillometry.py b/mne/preprocessing/eyetracking/_pupillometry.py index 956c37cb114..8da124b2e1f 100644 --- a/mne/preprocessing/eyetracking/_pupillometry.py +++ b/mne/preprocessing/eyetracking/_pupillometry.py @@ -77,6 +77,7 @@ def _interpolate_blinks(raw, buffer, blink_annots, interpolate_gaze): logger.info("Interpolating missing data during blinks...") pre_buffer, post_buffer = buffer # iterate over each eyetrack channel and interpolate the blinks + interpolated_chs = [] for ci, ch_info in enumerate(raw.info["chs"]): if interpolate_gaze: # interpolate over all eyetrack channels if ch_info["kind"] != FIFF.FIFFV_EYETRACK_CH: @@ -107,3 +108,10 @@ def _interpolate_blinks(raw, buffer, blink_annots, interpolate_gaze): ) # Replace the samples at the blink_indices with the interpolated values raw._data[ci, blink_indices] = interpolated_samples + interpolated_chs.append(ch_info["ch_name"]) + if interpolated_chs: + logger.info( + f"Interpolated {len(interpolated_chs)} channels: {interpolated_chs}" + ) + else: + warn("No channels were interpolated.") diff --git a/mne/preprocessing/eyetracking/eyetracking.py b/mne/preprocessing/eyetracking/eyetracking.py index f6b1b0fd0d4..883cf1934c6 100644 --- a/mne/preprocessing/eyetracking/eyetracking.py +++ b/mne/preprocessing/eyetracking/eyetracking.py @@ -8,6 +8,12 @@ import numpy as np from ..._fiff.constants import FIFF +from ...epochs import BaseEpochs +from ...evoked import Evoked +from ...io import BaseRaw +from ...utils import _check_option, _validate_type, logger, warn +from .calibration import Calibration +from .utils import _check_calibration # specific function to set eyetrack channels @@ -164,3 +170,162 @@ def _convert_mm_to_m(array): def _convert_deg_to_rad(array): return array * np.pi / 180.0 + + +def convert_units(inst, calibration, to="radians"): + """Convert Eyegaze data from pixels to radians of visual angle or vice versa. + + .. warning:: + Currently, depending on the units (pixels or radians), eyegaze channels may not + be reported correctly in visualization functions like :meth:`mne.io.Raw.plot`. + They will be shown correctly in :func:`mne.viz.eyetracking.plot_gaze`. + See :gh:`11879` for more information. + + .. Important:: + There are important considerations to keep in mind when using this function, + see the Notes section below. + + Parameters + ---------- + inst : instance of Raw, Epochs, or Evoked + The Raw, Epochs, or Evoked instance with eyegaze channels. + calibration : Calibration + Instance of Calibration, containing information about the screen size + (in meters), viewing distance (in meters), and the screen resolution + (in pixels). + to : str + Must be either ``"radians"`` or ``"pixels"``, indicating the desired unit. + + Returns + ------- + inst : instance of Raw | Epochs | Evoked + The Raw, Epochs, or Evoked instance, modified in place. + + Notes + ----- + There are at least two important considerations to keep in mind when using this + function: + + 1. Converting between on-screen pixels and visual angle is not a linear + transformation. If the visual angle subtends less than approximately ``.44`` + radians (``25`` degrees), the conversion could be considered to be approximately + linear. However, as the visual angle increases, the conversion becomes + increasingly non-linear. This may lead to unexpected results after converting + between pixels and visual angle. + + * This function assumes that the head is fixed in place and aligned with the center + of the screen, such that gaze to the center of the screen results in a visual + angle of ``0`` radians. + + .. versionadded:: 1.7 + """ + _validate_type(inst, (BaseRaw, BaseEpochs, Evoked), "inst") + _validate_type(calibration, Calibration, "calibration") + _check_option("to", to, ("radians", "pixels")) + _check_calibration(calibration) + + # get screen parameters + screen_size = calibration["screen_size"] + screen_resolution = calibration["screen_resolution"] + dist = calibration["screen_distance"] + + # loop through channels and convert units + converted_chs = [] + for ch_dict in inst.info["chs"]: + if ch_dict["coil_type"] != FIFF.FIFFV_COIL_EYETRACK_POS: + continue + unit = ch_dict["unit"] + name = ch_dict["ch_name"] + + if ch_dict["loc"][4] == -1: # x-coordinate + size = screen_size[0] + res = screen_resolution[0] + elif ch_dict["loc"][4] == 1: # y-coordinate + size = screen_size[1] + res = screen_resolution[1] + else: + raise ValueError( + f"loc array not set properly for channel '{name}'. Index 4 should" + f" be -1 or 1, but got {ch_dict['loc'][4]}" + ) + # check unit, convert, and set new unit + if to == "radians": + if unit != FIFF.FIFF_UNIT_PX: + raise ValueError( + f"Data must be in pixels in order to convert to radians." + f" Got {unit} for {name}" + ) + inst.apply_function(_pix_to_rad, picks=name, size=size, res=res, dist=dist) + ch_dict["unit"] = FIFF.FIFF_UNIT_RAD + elif to == "pixels": + if unit != FIFF.FIFF_UNIT_RAD: + raise ValueError( + f"Data must be in radians in order to convert to pixels." + f" Got {unit} for {name}" + ) + inst.apply_function(_rad_to_pix, picks=name, size=size, res=res, dist=dist) + ch_dict["unit"] = FIFF.FIFF_UNIT_PX + converted_chs.append(name) + if converted_chs: + logger.info(f"Converted {converted_chs} to {to}.") + if to == "radians": + # check if any values are greaater than .44 radians + # (25 degrees) and warn user + data = inst.get_data(picks=converted_chs) + if np.any(np.abs(data) > 0.52): + warn( + "Some visual angle values subtend greater than .52 radians " + "(30 degrees), meaning that the conversion between pixels " + "and visual angle may be very non-linear. Take caution when " + "interpreting these values. Max visual angle value in data:" + f" {np.nanmax(data):0.2f} radians.", + UserWarning, + ) + else: + warn("Could not find any eyegaze channels. Doing nothing.", UserWarning) + return inst + + +def _pix_to_rad(data, size, res, dist): + """Convert pixel coordinates to radians of visual angle. + + Parameters + ---------- + data : array-like, shape (n_samples,) + A vector of pixel coordinates. + size : float + The width or height of the screen, in meters. + res : int + The screen resolution in pixels, along the x or y axis. + dist : float + The viewing distance from the screen, in meters. + + Returns + ------- + rad : ndarray, shape (n_samples) + the data in radians. + """ + # Center the data so that 0 radians will be the center of the screen + data -= res / 2 + # How many meters is the pixel width or height + px_size = size / res + # Convert to radians + return np.arctan((data * px_size) / dist) + + +def _rad_to_pix(data, size, res, dist): + """Convert radians of visual angle to pixel coordinates. + + See the parameters section of _pix_to_rad for more information. + + Returns + ------- + pix : ndarray, shape (n_samples) + the data in pixels. + """ + # How many meters is the pixel width or height + px_size = size / res + # 1. calculate length of opposite side of triangle (in meters) + # 2. convert meters to pixel coordinates + # 3. add half of screen resolution to uncenter the pixel data (0,0 is top left) + return np.tan(data) * dist / px_size + res / 2 diff --git a/mne/preprocessing/eyetracking/tests/test_eyetracking.py b/mne/preprocessing/eyetracking/tests/test_eyetracking.py new file mode 100644 index 00000000000..8bea006d9fd --- /dev/null +++ b/mne/preprocessing/eyetracking/tests/test_eyetracking.py @@ -0,0 +1,78 @@ +import numpy as np +import pytest +from numpy.testing import assert_allclose + +import mne +from mne._fiff.constants import FIFF +from mne.utils import _record_warnings + + +def test_set_channel_types_eyetrack(eyetrack_raw): + """Test that set_channel_types_eyetrack worked on the fixture.""" + assert eyetrack_raw.info["chs"][0]["kind"] == FIFF.FIFFV_EYETRACK_CH + assert eyetrack_raw.info["chs"][1]["coil_type"] == FIFF.FIFFV_COIL_EYETRACK_POS + assert eyetrack_raw.info["chs"][0]["unit"] == FIFF.FIFF_UNIT_PX + assert eyetrack_raw.info["chs"][2]["unit"] == FIFF.FIFF_UNIT_NONE + + +def test_convert_units(eyetrack_raw, eyetrack_cal): + """Test unit conversion.""" + raw, cal = eyetrack_raw, eyetrack_cal # shorter names + + # roundtrip conversion should be identical to original data + data_orig = raw.get_data(picks=[0]) # take the first x-coord channel + mne.preprocessing.eyetracking.convert_units(raw, cal, "radians") + assert raw.info["chs"][0]["unit"] == FIFF.FIFF_UNIT_RAD + # Gaze was to center of screen, so x-coord and y-coord should now be 0 radians + assert_allclose(raw.get_data(picks=[0, 1]), 0) + + # Should raise an error if we try to convert to radians again + with pytest.raises(ValueError, match="Data must be in"): + mne.preprocessing.eyetracking.convert_units(raw, cal, "radians") + + # Convert back to pixels + mne.preprocessing.eyetracking.convert_units(raw, cal, "pixels") + assert raw.info["chs"][1]["unit"] == FIFF.FIFF_UNIT_PX + data_new = raw.get_data(picks=[0]) + assert_allclose(data_orig, data_new) + + # Should raise an error if we try to convert to pixels again + with pytest.raises(ValueError, match="Data must be in"): + mne.preprocessing.eyetracking.convert_units(raw, cal, "pixels") + + # Finally, check that we raise other errors or warnings when we should + # warn if no eyegaze channels found + raw_misc = raw.copy() + with _record_warnings(): # channel units change warning + raw_misc.set_channel_types({ch: "misc" for ch in raw_misc.ch_names}) + with pytest.warns(UserWarning, match="Could not"): + mne.preprocessing.eyetracking.convert_units(raw_misc, cal, "radians") + + # raise an error if the calibration is missing a key + bad_cal = cal.copy() + bad_cal.pop("screen_size") + bad_cal["screen_distance"] = None + with pytest.raises(KeyError, match="Calibration object must have the following"): + mne.preprocessing.eyetracking.convert_units(raw, bad_cal, "radians") + + # warn if visual angle is too large + cal_tmp = cal.copy() + cal_tmp["screen_distance"] = 0.1 + raw_tmp = raw.copy() + raw_tmp._data[0, :10] = 1900 # gaze to extremity of screen + with pytest.warns(UserWarning, match="Some visual angle values"): + mne.preprocessing.eyetracking.convert_units(raw_tmp, cal_tmp, "radians") + + # raise an error if channel locations not set + raw_missing = raw.copy() + raw_missing.info["chs"][0]["loc"] = np.zeros(12) + with pytest.raises(ValueError, match="loc array not set"): + mne.preprocessing.eyetracking.convert_units(raw_missing, cal, "radians") + + +def test_get_screen_visual_angle(eyetrack_cal): + """Test calculating the radians of visual angle for a screen.""" + # Our toy calibration should subtend .56 x .32 radians i.e 31.5 x 18.26 degrees + viz_angle = mne.preprocessing.eyetracking.get_screen_visual_angle(eyetrack_cal) + assert viz_angle.shape == (2,) + np.testing.assert_allclose(np.round(viz_angle, 2), (0.56, 0.32)) diff --git a/mne/preprocessing/eyetracking/utils.py b/mne/preprocessing/eyetracking/utils.py new file mode 100644 index 00000000000..89c379c9760 --- /dev/null +++ b/mne/preprocessing/eyetracking/utils.py @@ -0,0 +1,41 @@ +import numpy as np + +from ...utils import _validate_type +from .calibration import Calibration + + +def _check_calibration( + calibration, want_keys=("screen_size", "screen_resolution", "screen_distance") +): + missing_keys = [] + for key in want_keys: + if calibration.get(key, None) is None: + missing_keys.append(key) + + if missing_keys: + raise KeyError( + "Calibration object must have the following keys with valid values:" + f" {', '.join(missing_keys)}" + ) + else: + return True + + +def get_screen_visual_angle(calibration): + """Calculate the radians of visual angle that the participant screen subtends. + + Parameters + ---------- + calibration : Calibration + An instance of Calibration. Must have valid values for ``"screen_size"`` and + ``"screen_distance"`` keys. + + Returns + ------- + visual angle in radians : ndarray, shape (2,) + The visual angle of the monitor width and height, respectively. + """ + _validate_type(calibration, Calibration, "calibration") + _check_calibration(calibration, want_keys=("screen_size", "screen_distance")) + size = np.array(calibration["screen_size"]) + return 2 * np.arctan(size / (2 * calibration["screen_distance"])) diff --git a/mne/viz/__init__.pyi b/mne/viz/__init__.pyi index dfebec1f5dc..c58ad7d0e54 100644 --- a/mne/viz/__init__.pyi +++ b/mne/viz/__init__.pyi @@ -18,6 +18,7 @@ __all__ = [ "compare_fiff", "concatenate_images", "create_3d_figure", + "eyetracking", "get_3d_backend", "get_brain_class", "get_browser_backend", @@ -86,7 +87,7 @@ __all__ = [ "use_3d_backend", "use_browser_backend", ] -from . import _scraper, backends, ui_events +from . import _scraper, backends, eyetracking, ui_events from ._3d import ( link_brains, plot_alignment, diff --git a/mne/viz/eyetracking/heatmap.py b/mne/viz/eyetracking/heatmap.py index 8cb44ac4931..e6e6832084e 100644 --- a/mne/viz/eyetracking/heatmap.py +++ b/mne/viz/eyetracking/heatmap.py @@ -6,16 +6,18 @@ import numpy as np from scipy.ndimage import gaussian_filter -from ...utils import _ensure_int, _validate_type, fill_doc, logger +from ..._fiff.constants import FIFF +from ...utils import _validate_type, fill_doc, logger from ..utils import plt_show @fill_doc def plot_gaze( epochs, - width, - height, *, + calibration=None, + width=None, + height=None, sigma=25, cmap=None, alpha=1.0, @@ -29,14 +31,17 @@ def plot_gaze( ---------- epochs : instance of Epochs The :class:`~mne.Epochs` object containing eyegaze channels. + calibration : instance of Calibration | None + An instance of Calibration with information about the screen size, distance, + and resolution. If ``None``, you must provide a width and height. width : int - The width dimension of the plot canvas. For example, if the eyegaze data units - are pixels, and the participant screen resolution was 1920x1080, then the width - should be 1920. + The width dimension of the plot canvas, only valid if eyegaze data are in + pixels. For example, if the participant screen resolution was 1920x1080, then + the width should be 1920. height : int - The height dimension of the plot canvas. For example, if the eyegaze data units - are pixels, and the participant screen resolution was 1920x1080, then the height - should be 1080. + The height dimension of the plot canvas, only valid if eyegaze data are in + pixels. For example, if the participant screen resolution was 1920x1080, then + the height should be 1080. sigma : float | None The amount of Gaussian smoothing applied to the heatmap data (standard deviation in pixels). If ``None``, no smoothing is applied. Default is 25. @@ -59,17 +64,22 @@ def plot_gaze( from mne import BaseEpochs from mne._fiff.pick import _picks_to_idx + from ...preprocessing.eyetracking.utils import ( + _check_calibration, + get_screen_visual_angle, + ) + _validate_type(epochs, BaseEpochs, "epochs") _validate_type(alpha, "numeric", "alpha") _validate_type(sigma, ("numeric", None), "sigma") - width = _ensure_int(width, "width") - height = _ensure_int(height, "height") + # Get the gaze data pos_picks = _picks_to_idx(epochs.info, "eyegaze") gaze_data = epochs.get_data(picks=pos_picks) gaze_ch_loc = np.array([epochs.info["chs"][idx]["loc"] for idx in pos_picks]) x_data = gaze_data[:, np.where(gaze_ch_loc[:, 4] == -1)[0], :] y_data = gaze_data[:, np.where(gaze_ch_loc[:, 4] == 1)[0], :] + unit = epochs.info["chs"][pos_picks[0]]["unit"] # assumes all units are the same if x_data.shape[1] > 1: # binocular recording. Average across eyes logger.info("Detected binocular recording. Averaging positions across eyes.") @@ -77,13 +87,53 @@ def plot_gaze( y_data = np.nanmean(y_data, axis=1) canvas = np.vstack((x_data.flatten(), y_data.flatten())) # shape (2, n_samples) + # Check that we have the right inputs + if calibration is not None: + if width is not None or height is not None: + raise ValueError( + "If a calibration is provided, you cannot provide a width or height" + " to plot heatmaps. Please provide only the calibration object." + ) + _check_calibration(calibration) + if unit == FIFF.FIFF_UNIT_PX: + width, height = calibration["screen_resolution"] + elif unit == FIFF.FIFF_UNIT_RAD: + width, height = calibration["screen_size"] + else: + raise ValueError( + f"Invalid unit type: {unit}. gaze data Must be pixels or radians." + ) + else: + if width is None or height is None: + raise ValueError( + "If no calibration is provided, you must provide a width and height" + " to plot heatmaps." + ) + # Create 2D histogram - # Bin into image-like format + # We need to set the histogram bins & bounds, and imshow extent, based on the units + if unit == FIFF.FIFF_UNIT_PX: # pixel on screen + _range = [[0, height], [0, width]] + bins_x, bins_y = width, height + extent = [0, width, height, 0] + elif unit == FIFF.FIFF_UNIT_RAD: # radians of visual angle + if not calibration: + raise ValueError( + "If gaze data are in Radians, you must provide a" + " calibration instance to plot heatmaps." + ) + width, height = get_screen_visual_angle(calibration) + x_range = [-width / 2, width / 2] + y_range = [-height / 2, height / 2] + _range = [y_range, x_range] + extent = (x_range[0], x_range[1], y_range[0], y_range[1]) + bins_x, bins_y = calibration["screen_resolution"] + hist, _, _ = np.histogram2d( canvas[1, :], canvas[0, :], - bins=(height, width), - range=[[0, height], [0, width]], + bins=(bins_y, bins_x), + range=_range, ) # Convert density from samples to seconds hist /= epochs.info["sfreq"] @@ -99,6 +149,7 @@ def plot_gaze( alpha=alpha, vmin=vlim[0], vmax=vlim[1], + extent=extent, axes=axes, show=show, ) @@ -108,10 +159,12 @@ def _plot_heatmap_array( data, width, height, + *, cmap=None, alpha=None, vmin=None, vmax=None, + extent=None, axes=None, show=True, ): @@ -136,7 +189,8 @@ def _plot_heatmap_array( alphas = 1 if alpha is None else alpha vmin = np.nanmin(data) if vmin is None else vmin vmax = np.nanmax(data) if vmax is None else vmax - extent = [0, width, height, 0] # origin is the top left of the screen + if extent is None: + extent = [0, width, height, 0] # Plot heatmap im = ax.imshow( diff --git a/mne/viz/eyetracking/tests/test_heatmap.py b/mne/viz/eyetracking/tests/test_heatmap.py index a088c1dc7fe..0f0b0bfc4d5 100644 --- a/mne/viz/eyetracking/tests/test_heatmap.py +++ b/mne/viz/eyetracking/tests/test_heatmap.py @@ -4,33 +4,57 @@ # Copyright the MNE-Python contributors. import matplotlib.pyplot as plt -import numpy as np import pytest import mne +from mne._fiff.constants import FIFF -@pytest.mark.parametrize("axes", [None, True]) -def test_plot_heatmap(axes): +@pytest.mark.parametrize("axes, unit", [(None, "px"), (True, "rad")]) +def test_plot_heatmap(eyetrack_raw, eyetrack_cal, axes, unit): """Test plot_gaze.""" - # Create a toy epochs instance - info = info = mne.create_info( - ch_names=["xpos", "ypos"], sfreq=100, ch_types="eyegaze" - ) - # simulate a steady fixation at the center of the screen - width, height = (1920, 1080) - shape = (1, 100) # x or y, time - data = np.vstack([np.full(shape, width / 2), np.full(shape, height / 2)]) - epochs = mne.EpochsArray(data[None, ...], info) - epochs.info["chs"][0]["loc"][4] = -1 - epochs.info["chs"][1]["loc"][4] = 1 + epochs = mne.make_fixed_length_epochs(eyetrack_raw, duration=1.0) + epochs.load_data() + width, height = eyetrack_cal["screen_resolution"] # 1920, 1080 + if unit == "rad": + mne.preprocessing.eyetracking.convert_units(epochs, eyetrack_cal, to="radians") if axes: axes = plt.subplot() - fig = mne.viz.eyetracking.plot_gaze( - epochs, width=width, height=height, axes=axes, cmap="Greys", sigma=None - ) + + # First check that we raise errors when we should + with pytest.raises(ValueError, match="If no calibration is provided"): + mne.viz.eyetracking.plot_gaze(epochs) + + with pytest.raises(ValueError, match="If a calibration is provided"): + mne.viz.eyetracking.plot_gaze( + epochs, width=width, height=height, calibration=eyetrack_cal + ) + + with pytest.raises(ValueError, match="Invalid unit"): + ep_bad = epochs.copy() + ep_bad.info["chs"][0]["unit"] = FIFF.FIFF_UNIT_NONE + mne.viz.eyetracking.plot_gaze(ep_bad, calibration=eyetrack_cal) + + # raise an error if no calibration object is provided for radian data + if unit == "rad": + with pytest.raises(ValueError, match="If gaze data are in Radians"): + mne.viz.eyetracking.plot_gaze(epochs, axes=axes, width=1, height=1) + + # Now check that we get the expected output + if unit == "px": + fig = mne.viz.eyetracking.plot_gaze( + epochs, width=width, height=height, axes=axes, cmap="Greys", sigma=None + ) + elif unit == "rad": + fig = mne.viz.eyetracking.plot_gaze( + epochs, + calibration=eyetrack_cal, + axes=axes, + cmap="Greys", + sigma=None, + ) img = fig.axes[0].images[0].get_array() # We simulated a 2D histogram where only the central pixel (960, 540) was active - assert img.T[width // 2, height // 2] == 1 # central pixel is active - assert np.sum(img) == 1 # only the central pixel should be active + # so regardless of the unit, we should have a heatmap with the central bin active + assert img.T[width // 2, height // 2] == 1 From 9f0dfefe612776226d22714958dec2f3e2c05e4b Mon Sep 17 00:00:00 2001 From: Nabil Alibou <63203348+nabilalibou@users.noreply.github.com> Date: Tue, 6 Feb 2024 18:46:55 +0100 Subject: [PATCH 180/405] DOC: Point out that inverse modeling needs an average reference projector ready to not raise an error (#12420) Co-authored-by: Daniel McCloy Co-authored-by: Eric Larson --- doc/changes/devel/12420.other.rst | 1 + doc/changes/names.inc | 2 ++ tutorials/preprocessing/55_setting_eeg_reference.py | 12 ++++++++---- 3 files changed, 11 insertions(+), 4 deletions(-) create mode 100644 doc/changes/devel/12420.other.rst diff --git a/doc/changes/devel/12420.other.rst b/doc/changes/devel/12420.other.rst new file mode 100644 index 00000000000..8b949d25dc7 --- /dev/null +++ b/doc/changes/devel/12420.other.rst @@ -0,0 +1 @@ +Clarify in the :ref:`EEG referencing tutorial ` that an average reference projector ready is required for inverse modeling, by :newcontrib:`Nabil Alibou` diff --git a/doc/changes/names.inc b/doc/changes/names.inc index 0389f75e83e..a2def55af97 100644 --- a/doc/changes/names.inc +++ b/doc/changes/names.inc @@ -390,6 +390,8 @@ .. _Motofumi Fushimi: https://github.com/motofumi-fushimi/motofumi-fushimi.github.io +.. _Nabil Alibou: https://github.com/nabilalibou + .. _Natalie Klein: https://github.com/natalieklein .. _Nathalie Gayraud: https://github.com/ngayraud diff --git a/tutorials/preprocessing/55_setting_eeg_reference.py b/tutorials/preprocessing/55_setting_eeg_reference.py index dbc817dc2d7..22e247469ee 100644 --- a/tutorials/preprocessing/55_setting_eeg_reference.py +++ b/tutorials/preprocessing/55_setting_eeg_reference.py @@ -131,7 +131,7 @@ # :meth:`~mne.io.Raw.set_eeg_reference` with ``ref_channels='average'``. Just # as above, this will not affect any channels marked as "bad", nor will it # include bad channels when computing the average. However, it does modify the -# :class:`~mne.io.Raw` object in-place, so we'll make a copy first so we can +# :class:`~mne.io.Raw` object in-place, so we'll make a copy first, so we can # still go back to the unmodified :class:`~mne.io.Raw` object later: # sphinx_gallery_thumbnail_number = 4 @@ -241,9 +241,13 @@ # the source modeling is performed. In contrast, applying an average reference # by the traditional subtraction method offers no such guarantee. # -# For these reasons, when performing inverse imaging, *MNE-Python will raise -# a ``ValueError`` if there are EEG channels present and something other than -# an average reference strategy has been specified*. +# .. important:: For these reasons, when performing inverse imaging, MNE-Python +# will raise a ``ValueError`` if there are EEG channels present +# and something other than an average reference projector strategy +# has been specified. To ensure correct functioning consider +# calling :meth:`set_eeg_reference(projection=True) +# ` to add an average +# reference as a projector. # # .. LINKS # From 78fbfea6363555feb873ef01762f20a4f6c53f72 Mon Sep 17 00:00:00 2001 From: Daniel McCloy Date: Wed, 7 Feb 2024 10:25:43 -0600 Subject: [PATCH 181/405] ruff TOML updates (#12428) --- pyproject.toml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 58a5915cd2d..998ceffc5e2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -205,8 +205,10 @@ builtin = "clear,rare,informal,names,usage" skip = "doc/references.bib" [tool.ruff] -select = ["A", "B006", "D", "E", "F", "I", "W", "UP"] exclude = ["__init__.py", "constants.py", "resources.py"] + +[tool.ruff.lint] +select = ["A", "B006", "D", "E", "F", "I", "W", "UP"] ignore = [ "D100", # Missing docstring in public module "D104", # Missing docstring in public package @@ -214,7 +216,7 @@ ignore = [ "UP031", # Use format specifiers instead of percent format ] -[tool.ruff.pydocstyle] +[tool.ruff.lint.pydocstyle] convention = "numpy" ignore-decorators = [ "property", @@ -224,7 +226,7 @@ ignore-decorators = [ "mne.utils.deprecated", ] -[tool.ruff.per-file-ignores] +[tool.ruff.lint.per-file-ignores] "tutorials/time-freq/10_spectrum_class.py" = [ "E501", # line too long ] From e05e77c92480871f0be38a184894d2ef48694844 Mon Sep 17 00:00:00 2001 From: Nabil Alibou <63203348+nabilalibou@users.noreply.github.com> Date: Wed, 7 Feb 2024 19:22:06 +0100 Subject: [PATCH 182/405] DOC: Specifies that a custom reference is anything but an average projector (#12426) --- mne/_fiff/meas_info.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mne/_fiff/meas_info.py b/mne/_fiff/meas_info.py index 462a34cb6d6..797e3d4bbaa 100644 --- a/mne/_fiff/meas_info.py +++ b/mne/_fiff/meas_info.py @@ -1104,9 +1104,9 @@ class Info(dict, SetChannelsMixin, MontageMixin, ContainsMixin): The transformation from 4D/CTF head coordinates to Neuromag head coordinates. This is only present in 4D/CTF data. custom_ref_applied : int - Whether a custom (=other than average) reference has been applied to - the EEG data. This flag is checked by some algorithms that require an - average reference to be set. + Whether a custom (=other than an average projector) reference has been + applied to the EEG data. This flag is checked by some algorithms that + require an average reference to be set. description : str | None String description of the recording. dev_ctf_t : Transform | None From 6857f10bd6470495635ffea190e673e91dc81ac6 Mon Sep 17 00:00:00 2001 From: rcmdnk Date: Fri, 9 Feb 2024 00:08:28 +0900 Subject: [PATCH 183/405] [FIX] Add tol parameter to events_from_annotations (#12324) --- doc/changes/devel/12324.bugfix.rst | 1 + mne/annotations.py | 8 +++++- mne/tests/test_annotations.py | 43 ++++++++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 doc/changes/devel/12324.bugfix.rst diff --git a/doc/changes/devel/12324.bugfix.rst b/doc/changes/devel/12324.bugfix.rst new file mode 100644 index 00000000000..ec7f2c5849d --- /dev/null +++ b/doc/changes/devel/12324.bugfix.rst @@ -0,0 +1 @@ +Add ``tol`` parameter to :meth:`mne.events_from_annotations` so that the user can specify the tolerance to ignore rounding errors of event onsets when using ``chunk_duration`` is not None (default is 1e-8), by `Michiru Kaneda`_ diff --git a/mne/annotations.py b/mne/annotations.py index f0f88783b68..a6be1f7a62d 100644 --- a/mne/annotations.py +++ b/mne/annotations.py @@ -1529,6 +1529,7 @@ def events_from_annotations( regexp=r"^(?![Bb][Aa][Dd]|[Ee][Dd][Gg][Ee]).*$", use_rounding=True, chunk_duration=None, + tol=1e-8, verbose=None, ): """Get :term:`events` and ``event_id`` from an Annotations object. @@ -1572,6 +1573,11 @@ def events_from_annotations( they fit within the annotation duration spaced according to ``chunk_duration``. As a consequence annotations with duration shorter than ``chunk_duration`` will not contribute events. + tol : float + The tolerance used to check if a chunk fits within an annotation when + ``chunk_duration`` is not ``None``. If the duration from a computed + chunk onset to the end of the annotation is smaller than + ``chunk_duration`` minus ``tol``, the onset will be discarded. %(verbose)s Returns @@ -1617,7 +1623,7 @@ def events_from_annotations( for annot in annotations[event_sel]: annot_offset = annot["onset"] + annot["duration"] _onsets = np.arange(annot["onset"], annot_offset, chunk_duration) - good_events = annot_offset - _onsets >= chunk_duration + good_events = annot_offset - _onsets >= chunk_duration - tol if good_events.any(): _onsets = _onsets[good_events] _inds = raw.time_as_index( diff --git a/mne/tests/test_annotations.py b/mne/tests/test_annotations.py index 8be52b60a9d..4868f5dc5df 100644 --- a/mne/tests/test_annotations.py +++ b/mne/tests/test_annotations.py @@ -819,6 +819,49 @@ def test_events_from_annot_onset_alingment(): assert raw.first_samp == event_latencies[0, 0] +@pytest.mark.parametrize( + "use_rounding,tol,shape,onsets,descriptions", + [ + pytest.param(True, 0, (2, 3), [202, 402], [0, 2], id="rounding-notol"), + pytest.param(True, 1e-8, (3, 3), [202, 302, 402], [0, 1, 2], id="rounding-tol"), + pytest.param(False, 0, (2, 3), [202, 401], [0, 2], id="norounding-notol"), + pytest.param( + False, 1e-8, (3, 3), [202, 302, 401], [0, 1, 2], id="norounding-tol" + ), + pytest.param(None, None, (3, 3), [202, 302, 402], [0, 1, 2], id="default"), + ], +) +def test_events_from_annot_with_tolerance( + use_rounding, tol, shape, onsets, descriptions +): + """Test events_from_annotations w/ and w/o tolerance.""" + info = create_info(ch_names=1, sfreq=100) + raw = RawArray(data=np.empty((1, 1000)), info=info, first_samp=0) + meas_date = _handle_meas_date(0) + with raw.info._unlock(check_after=True): + raw.info["meas_date"] = meas_date + chunk_duration = 1 + annot = Annotations([2.02, 3.02, 4.02], chunk_duration, ["0", "1", "2"], 0) + raw.set_annotations(annot) + event_id = {"0": 0, "1": 1, "2": 2} + + if use_rounding is None: + events, _ = events_from_annotations( + raw, event_id=event_id, chunk_duration=chunk_duration + ) + else: + events, _ = events_from_annotations( + raw, + event_id=event_id, + chunk_duration=chunk_duration, + use_rounding=use_rounding, + tol=tol, + ) + assert events.shape == shape + assert (events[:, 0] == onsets).all() + assert (events[:, 2] == descriptions).all() + + def _create_annotation_based_on_descr( description, annotation_start_sampl=0, duration=0, orig_time=0 ): From 6305bd16975e2fe31b1eaf63408c9f93bfe1b535 Mon Sep 17 00:00:00 2001 From: Alex Rockhill Date: Thu, 8 Feb 2024 09:43:02 -0800 Subject: [PATCH 184/405] [BUG] Fix bad channels error, better default (#12382) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- doc/changes/devel/12382.apichange.rst | 1 + doc/changes/devel/12382.bugfix.rst | 1 + mne/source_estimate.py | 23 +++++++++++++++++++---- mne/tests/test_source_estimate.py | 3 ++- 4 files changed, 23 insertions(+), 5 deletions(-) create mode 100644 doc/changes/devel/12382.apichange.rst create mode 100644 doc/changes/devel/12382.bugfix.rst diff --git a/doc/changes/devel/12382.apichange.rst b/doc/changes/devel/12382.apichange.rst new file mode 100644 index 00000000000..aa38b436cf0 --- /dev/null +++ b/doc/changes/devel/12382.apichange.rst @@ -0,0 +1 @@ +Change :func:`mne.stc_near_sensors` ``surface`` default from the ``'pial'`` surface to the surface in ``src`` if ``src`` is not ``None`` in version 1.8, by `Alex Rockhill`_. diff --git a/doc/changes/devel/12382.bugfix.rst b/doc/changes/devel/12382.bugfix.rst new file mode 100644 index 00000000000..8409f016206 --- /dev/null +++ b/doc/changes/devel/12382.bugfix.rst @@ -0,0 +1 @@ +Fix bad channels not handled properly in :func:`mne.stc_near_sensors` by `Alex Rockhill`_. diff --git a/mne/source_estimate.py b/mne/source_estimate.py index 66897bcaedc..7994aab519b 100644 --- a/mne/source_estimate.py +++ b/mne/source_estimate.py @@ -3781,7 +3781,7 @@ def stc_near_sensors( subjects_dir=None, src=None, picks=None, - surface="pial", + surface="auto", verbose=None, ): """Create a STC from ECoG, sEEG and DBS sensor data. @@ -3821,8 +3821,8 @@ def stc_near_sensors( .. versionadded:: 0.24 surface : str | None - The surface to use if ``src=None``. Default is the pial surface. - If None, the source space surface will be used. + The surface to use. If ``src=None``, defaults to the pial surface. + Otherwise, the source space surface will be used. .. versionadded:: 0.24.1 %(verbose)s @@ -3876,12 +3876,27 @@ def stc_near_sensors( _validate_type(mode, str, "mode") _validate_type(src, (None, SourceSpaces), "src") _check_option("mode", mode, ("sum", "single", "nearest", "weighted")) + if surface == "auto": + if src is not None: + pial_fname = op.join(subjects_dir, subject, "surf", "lh.pial") + src_surf_is_pial = op.isfile(pial_fname) and np.allclose( + src[0]["rr"], read_surface(pial_fname)[0] + ) + if not src_surf_is_pial: + warn( + "In version 1.8, ``surface='auto'`` will be the default " + "which will use the surface in ``src`` instead of the " + "pial surface when ``src != None``. Pass ``surface='pial'`` " + "or ``surface=None`` to suppress this warning", + DeprecationWarning, + ) + surface = "pial" # create a copy of Evoked using ecog, seeg and dbs if picks is None: picks = pick_types(evoked.info, ecog=True, seeg=True, dbs=True) evoked = evoked.copy().pick(picks) - frames = set(evoked.info["chs"][pick]["coord_frame"] for pick in picks) + 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)}" diff --git a/mne/tests/test_source_estimate.py b/mne/tests/test_source_estimate.py index dff220d9752..77f0eaec28c 100644 --- a/mne/tests/test_source_estimate.py +++ b/mne/tests/test_source_estimate.py @@ -1713,7 +1713,8 @@ def test_stc_near_sensors(tmp_path): for s in src: transform_surface_to(s, "head", trans, copy=False) assert src[0]["coord_frame"] == FIFF.FIFFV_COORD_HEAD - stc_src = stc_near_sensors(evoked, src=src, **kwargs) + with pytest.warns(DeprecationWarning, match="instead of the pial"): + stc_src = stc_near_sensors(evoked, src=src, **kwargs) assert len(stc_src.data) == 7928 with pytest.warns(RuntimeWarning, match="not included"): # some removed stc_src_full = compute_source_morph( From cef847997255716045e43bf11967af6d5206c8ba Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Thu, 8 Feb 2024 13:33:10 -0500 Subject: [PATCH 185/405] MAINT: Fix for pip-pre (#12433) --- mne/tests/test_epochs.py | 2 +- mne/tests/test_source_estimate.py | 2 +- mne/time_frequency/tests/test_tfr.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/mne/tests/test_epochs.py b/mne/tests/test_epochs.py index edb2b4967d2..1b4b65dc7e8 100644 --- a/mne/tests/test_epochs.py +++ b/mne/tests/test_epochs.py @@ -3086,7 +3086,7 @@ def test_to_data_frame_index(index): # test index order/hierarchy preservation if not isinstance(index, list): index = [index] - assert df.index.names == index + assert list(df.index.names) == index # test that non-indexed data were present as columns non_index = list(set(["condition", "time", "epoch"]) - set(index)) if len(non_index): diff --git a/mne/tests/test_source_estimate.py b/mne/tests/test_source_estimate.py index 77f0eaec28c..1ed5c90623c 100644 --- a/mne/tests/test_source_estimate.py +++ b/mne/tests/test_source_estimate.py @@ -1240,7 +1240,7 @@ def test_to_data_frame_index(index): # test index setting if not isinstance(index, list): index = [index] - assert df.index.names == index + assert list(df.index.names) == index # test that non-indexed data were present as columns non_index = list(set(["time", "subject"]) - set(index)) if len(non_index): diff --git a/mne/time_frequency/tests/test_tfr.py b/mne/time_frequency/tests/test_tfr.py index 35c132ad3f1..abd7d0786be 100644 --- a/mne/time_frequency/tests/test_tfr.py +++ b/mne/time_frequency/tests/test_tfr.py @@ -1453,7 +1453,7 @@ def test_to_data_frame_index(index): # test index order/hierarchy preservation if not isinstance(index, list): index = [index] - assert df.index.names == index + assert list(df.index.names) == index # test that non-indexed data were present as columns non_index = list(set(["condition", "time", "freq", "epoch"]) - set(index)) if len(non_index): From ccf679457e0ccddc6a2aabeda6968da647f10c6e Mon Sep 17 00:00:00 2001 From: Alex Rockhill Date: Mon, 12 Feb 2024 08:50:18 -0800 Subject: [PATCH 186/405] Fix stc_near_sensors error (#12436) --- doc/changes/devel/12436.bugfix.rst | 1 + mne/source_estimate.py | 9 ++++++--- tutorials/clinical/20_seeg.py | 10 ++++++++-- 3 files changed, 15 insertions(+), 5 deletions(-) create mode 100644 doc/changes/devel/12436.bugfix.rst diff --git a/doc/changes/devel/12436.bugfix.rst b/doc/changes/devel/12436.bugfix.rst new file mode 100644 index 00000000000..7ddbd9f5d21 --- /dev/null +++ b/doc/changes/devel/12436.bugfix.rst @@ -0,0 +1 @@ +Fix :ref:`tut-working-with-seeg` use of :func:`mne.stc_near_sensors` to use the :class:`mne.VolSourceEstimate` positions and not the pial surface, by `Alex Rockhill`_ diff --git a/mne/source_estimate.py b/mne/source_estimate.py index 7994aab519b..2bd1ef48dee 100644 --- a/mne/source_estimate.py +++ b/mne/source_estimate.py @@ -3879,8 +3879,11 @@ def stc_near_sensors( if surface == "auto": if src is not None: pial_fname = op.join(subjects_dir, subject, "surf", "lh.pial") - src_surf_is_pial = op.isfile(pial_fname) and np.allclose( - src[0]["rr"], read_surface(pial_fname)[0] + pial_rr = read_surface(pial_fname)[0] + src_surf_is_pial = ( + op.isfile(pial_fname) + and src[0]["rr"].shape == pial_rr.shape + and np.allclose(src[0]["rr"], pial_rr) ) if not src_surf_is_pial: warn( @@ -3890,7 +3893,7 @@ def stc_near_sensors( "or ``surface=None`` to suppress this warning", DeprecationWarning, ) - surface = "pial" + surface = "pial" if src is None or src.kind == "surface" else None # create a copy of Evoked using ecog, seeg and dbs if picks is None: diff --git a/tutorials/clinical/20_seeg.py b/tutorials/clinical/20_seeg.py index dac5739110d..6166001c075 100644 --- a/tutorials/clinical/20_seeg.py +++ b/tutorials/clinical/20_seeg.py @@ -212,8 +212,14 @@ evoked = epochs.average() stc = mne.stc_near_sensors( - evoked, trans, "fsaverage", subjects_dir=subjects_dir, src=vol_src, verbose="error" -) # ignore missing electrode warnings + evoked, + trans, + "fsaverage", + subjects_dir=subjects_dir, + src=vol_src, + surface=None, + verbose="error", +) stc = abs(stc) # just look at magnitude clim = dict(kind="value", lims=np.percentile(abs(evoked.data), [10, 50, 75])) From 7436c9efc30c0d2679161a83e9255770f41ec21b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 Feb 2024 19:58:03 +0000 Subject: [PATCH 187/405] Bump pre-commit/action from 3.0.0 to 3.0.1 (#12437) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Eric Larson --- .github/workflows/tests.yml | 2 +- tools/azure_dependencies.sh | 3 ++- tools/github_actions_dependencies.sh | 4 +++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e0cfb84ff76..ed7a0a7b412 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -23,7 +23,7 @@ jobs: - uses: actions/setup-python@v5 with: python-version: '3.11' - - uses: pre-commit/action@v3.0.0 + - uses: pre-commit/action@v3.0.1 bandit: name: Bandit diff --git a/tools/azure_dependencies.sh b/tools/azure_dependencies.sh index 9ee566f3c30..bfaa96a19e6 100755 --- a/tools/azure_dependencies.sh +++ b/tools/azure_dependencies.sh @@ -6,7 +6,8 @@ if [ "${TEST_MODE}" == "pip" ]; then python -m pip install --only-binary="numba,llvmlite,numpy,scipy,vtk" -e .[test,full] elif [ "${TEST_MODE}" == "pip-pre" ]; then STD_ARGS="$STD_ARGS --pre" - python -m pip install $STD_ARGS --only-binary ":all:" --extra-index-url "https://www.riverbankcomputing.com/pypi/simple" "PyQt6!=6.6.1" PyQt6-sip PyQt6-Qt6 "PyQt6-Qt6!=6.6.1" + # python -m pip install $STD_ARGS --only-binary ":all:" --extra-index-url "https://www.riverbankcomputing.com/pypi/simple" "PyQt6!=6.6.1" PyQt6-sip PyQt6-Qt6 "PyQt6-Qt6!=6.6.1" + python -m pip install $STD_ARGS --only-binary ":all:" "PyQt6!=6.6.1" PyQt6-sip PyQt6-Qt6 "PyQt6-Qt6!=6.6.1" echo "Numpy etc." python -m pip install $STD_ARGS --only-binary ":all:" --default-timeout=60 --extra-index-url "https://pypi.anaconda.org/scientific-python-nightly-wheels/simple" "numpy>=2.0.0.dev0" "scipy>=1.12.0.dev0" "scikit-learn>=1.5.dev0" matplotlib pillow statsmodels pyarrow # echo "dipy" diff --git a/tools/github_actions_dependencies.sh b/tools/github_actions_dependencies.sh index b801b458dc8..cc5d9fffc48 100755 --- a/tools/github_actions_dependencies.sh +++ b/tools/github_actions_dependencies.sh @@ -26,7 +26,9 @@ else echo "Numpy" pip uninstall -yq numpy echo "PyQt6" - pip install $STD_ARGS --only-binary ":all:" --default-timeout=60 --extra-index-url https://www.riverbankcomputing.com/pypi/simple "PyQt6!=6.6.1" "PyQt6-Qt6!=6.6.1" + # Now broken in latest release and in the pre release: + # pip install $STD_ARGS --only-binary ":all:" --default-timeout=60 --extra-index-url https://www.riverbankcomputing.com/pypi/simple "PyQt6!=6.6.1" "PyQt6-Qt6!=6.6.1" + pip install $STD_ARGS --only-binary ":all:" --default-timeout=60 "PyQt6!=6.6.1" "PyQt6-Qt6!=6.6.1" echo "NumPy/SciPy/pandas etc." pip install $STD_ARGS --only-binary ":all:" --default-timeout=60 --extra-index-url "https://pypi.anaconda.org/scientific-python-nightly-wheels/simple" "numpy>=2.0.0.dev0" "scipy>=1.12.0.dev0" "scikit-learn>=1.5.dev0" matplotlib pillow statsmodels pyarrow # No pandas, dipy, h5py, openmeeg, python-picard (needs numexpr) until they update to NumPy 2.0 compat From 2851fb5f8d2a780ff2ef5dd181678fe8f10f04c6 Mon Sep 17 00:00:00 2001 From: Mathieu Scheltienne Date: Mon, 12 Feb 2024 22:16:39 +0100 Subject: [PATCH 188/405] Specify CODECOV_TOKEN in codecov/codecov-action@v4 (#12431) --- .github/workflows/tests.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ed7a0a7b412..5abfae43a3b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -130,4 +130,6 @@ jobs: - run: ./tools/github_actions_download.sh - run: ./tools/github_actions_test.sh - uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} if: success() From 1db901c2ab7dd428f43e3ad8a79f04edff790edc Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 13 Feb 2024 00:52:54 +0000 Subject: [PATCH 189/405] [pre-commit.ci] pre-commit autoupdate (#12438) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9a81e895d4c..7558ace0222 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.2.0 + rev: v0.2.1 hooks: - id: ruff name: ruff lint mne @@ -32,7 +32,7 @@ repos: # yamllint - repo: https://github.com/adrienverge/yamllint.git - rev: v1.33.0 + rev: v1.34.0 hooks: - id: yamllint args: [--strict, -c, .yamllint.yml] From 73ca06b4056bcb63d55bbcbc68c816c5c8b8f6f2 Mon Sep 17 00:00:00 2001 From: Carina Date: Tue, 13 Feb 2024 14:58:48 +1000 Subject: [PATCH 190/405] equalize epoch counts for EpochsTFR (#12207) Co-authored-by: Daniel McCloy --- doc/changes/devel/12207.newfeature.rst | 1 + doc/changes/names.inc | 2 +- mne/epochs.py | 38 +++++++++++------------ mne/tests/test_epochs.py | 43 +++++++++++++++++++++++--- mne/time_frequency/tests/test_tfr.py | 31 +++++++++++++++++++ mne/time_frequency/tfr.py | 37 ++++++++++++++++++++++ 6 files changed, 127 insertions(+), 25 deletions(-) create mode 100644 doc/changes/devel/12207.newfeature.rst diff --git a/doc/changes/devel/12207.newfeature.rst b/doc/changes/devel/12207.newfeature.rst new file mode 100644 index 00000000000..7d741a06bf5 --- /dev/null +++ b/doc/changes/devel/12207.newfeature.rst @@ -0,0 +1 @@ +Allow :class:`mne.time_frequency.EpochsTFR` as input to :func:`mne.epochs.equalize_epoch_counts`, by `Carina Forster`_. \ No newline at end of file diff --git a/doc/changes/names.inc b/doc/changes/names.inc index a2def55af97..f090b463cd4 100644 --- a/doc/changes/names.inc +++ b/doc/changes/names.inc @@ -74,7 +74,7 @@ .. _Carlos de la Torre-Ortiz: https://ctorre.me -.. _Carina Forster: https://github.com/carinafo +.. _Carina Forster: https://github.com/CarinaFo .. _Cathy Nangini: https://github.com/KatiRG diff --git a/mne/epochs.py b/mne/epochs.py index 2f4bea9cec9..7da05dbd045 100644 --- a/mne/epochs.py +++ b/mne/epochs.py @@ -75,6 +75,7 @@ from .html_templates import _get_html_template from .parallel import parallel_func from .time_frequency.spectrum import EpochsSpectrum, SpectrumMixin, _validate_method +from .time_frequency.tfr import EpochsTFR from .utils import ( ExtendedTimeMixin, GetEpochsMixin, @@ -2479,8 +2480,8 @@ def equalize_event_counts(self, event_ids=None, method="mintime"): for eq in event_ids: eq_inds.append(self._keys_to_idx(eq)) - event_times = [self.events[e, 0] for e in eq_inds] - indices = _get_drop_indices(event_times, method) + sample_nums = [self.events[e, 0] for e in eq_inds] + indices = _get_drop_indices(sample_nums, method) # need to re-index indices indices = np.concatenate([e[idx] for e, idx in zip(eq_inds, indices)]) self.drop(indices, reason="EQUALIZED_COUNT") @@ -3616,7 +3617,7 @@ def combine_event_ids(epochs, old_event_ids, new_event_id, copy=True): def equalize_epoch_counts(epochs_list, method="mintime"): - """Equalize the number of trials in multiple Epoch instances. + """Equalize the number of trials in multiple Epochs or EpochsTFR instances. Parameters ---------- @@ -3643,33 +3644,32 @@ def equalize_epoch_counts(epochs_list, method="mintime"): -------- >>> equalize_epoch_counts([epochs1, epochs2]) # doctest: +SKIP """ - if not all(isinstance(e, BaseEpochs) for e in epochs_list): + if not all(isinstance(epoch, (BaseEpochs, EpochsTFR)) for epoch in epochs_list): raise ValueError("All inputs must be Epochs instances") # make sure bad epochs are dropped - for e in epochs_list: - if not e._bad_dropped: - e.drop_bad() - event_times = [e.events[:, 0] for e in epochs_list] - indices = _get_drop_indices(event_times, method) - for e, inds in zip(epochs_list, indices): - e.drop(inds, reason="EQUALIZED_COUNT") + for epoch in epochs_list: + if not epoch._bad_dropped: + epoch.drop_bad() + sample_nums = [epoch.events[:, 0] for epoch in epochs_list] + indices = _get_drop_indices(sample_nums, method) + for epoch, inds in zip(epochs_list, indices): + epoch.drop(inds, reason="EQUALIZED_COUNT") -def _get_drop_indices(event_times, method): +def _get_drop_indices(sample_nums, method): """Get indices to drop from multiple event timing lists.""" - small_idx = np.argmin([e.shape[0] for e in event_times]) - small_e_times = event_times[small_idx] + small_idx = np.argmin([e.shape[0] for e in sample_nums]) + small_epoch_indices = sample_nums[small_idx] _check_option("method", method, ["mintime", "truncate"]) indices = list() - for e in event_times: + for event in sample_nums: if method == "mintime": - mask = _minimize_time_diff(small_e_times, e) + mask = _minimize_time_diff(small_epoch_indices, event) else: - mask = np.ones(e.shape[0], dtype=bool) - mask[small_e_times.shape[0] :] = False + mask = np.ones(event.shape[0], dtype=bool) + mask[small_epoch_indices.shape[0] :] = False indices.append(np.where(np.logical_not(mask))[0]) - return indices diff --git a/mne/tests/test_epochs.py b/mne/tests/test_epochs.py index 1b4b65dc7e8..015974e89cc 100644 --- a/mne/tests/test_epochs.py +++ b/mne/tests/test_epochs.py @@ -2803,25 +2803,58 @@ def test_subtract_evoked(): def test_epoch_eq(): - """Test epoch count equalization and condition combining.""" + """Test for equalize_epoch_counts and equalize_event_counts functions.""" + # load data raw, events, picks = _get_data() - # equalizing epochs objects + # test equalize epoch counts + # create epochs with unequal counts events_1 = events[events[:, 2] == event_id] epochs_1 = Epochs(raw, events_1, event_id, tmin, tmax, picks=picks) events_2 = events[events[:, 2] == event_id_2] epochs_2 = Epochs(raw, events_2, event_id_2, tmin, tmax, picks=picks) + # events 2 has one more event than events 1 epochs_1.drop_bad() # make sure drops are logged + epochs_2.drop_bad() # make sure drops are logged + # make sure there is a difference in the number of events + assert len(epochs_1) != len(epochs_2) + # make sure bad epochs are dropped before equalizing epoch counts assert_equal( len([log for log in epochs_1.drop_log if not log]), len(epochs_1.events) ) - assert epochs_1.drop_log == ((),) * len(epochs_1.events) - assert_equal(len([lg for lg in epochs_1.drop_log if not lg]), len(epochs_1.events)) - assert epochs_1.events.shape[0] != epochs_2.events.shape[0] + assert epochs_2.drop_log == ((),) * len(epochs_2.events) + # test mintime method + events_1[-1, 0] += 60 # hack: ensure mintime drops something other than last trial + # now run equalize_epoch_counts with mintime method equalize_epoch_counts([epochs_1, epochs_2], method="mintime") + # mintime method should give us the smallest difference between timings of epochs + alleged_mintime = np.sum(np.abs(epochs_1.events[:, 0] - epochs_2.events[:, 0])) + # test that "mintime" works as expected, by systematically dropping each event from + # events_2 and ensuring the latencies are actually smallest in the + # equalize_epoch_counts case. NB: len(events_2) > len(events_1) + for idx in range(events_2.shape[0]): + # delete epoch from events_2 + test_events = np.delete(events_2.copy(), idx, axis=0) + assert test_events.shape == epochs_1.events.shape == epochs_2.events.shape + # difference (in samples) between epochs_1 event times and the event times we + # get from our deletion of row `idx` from events_2 + latencies = epochs_1.events[:, 0] - test_events[:, 0] + got_mintime = np.sum(np.abs(latencies)) + assert got_mintime >= alleged_mintime + # make sure the number of events is equal assert_equal(epochs_1.events.shape[0], epochs_2.events.shape[0]) + # create new epochs with the same event ids as epochs_1 and epochs_2 epochs_3 = Epochs(raw, events, event_id, tmin, tmax, picks=picks) epochs_4 = Epochs(raw, events, event_id_2, tmin, tmax, picks=picks) + epochs_3.drop_bad() # make sure drops are logged + epochs_4.drop_bad() # make sure drops are logged + # make sure there is a difference in the number of events + assert len(epochs_3) != len(epochs_4) + # test truncate method equalize_epoch_counts([epochs_3, epochs_4], method="truncate") + if len(epochs_3.events) > len(epochs_4.events): + assert_equal(epochs_3.events[-2, 0], epochs_3.events.shape[-1, 0]) + elif len(epochs_3.events) < len(epochs_4.events): + assert_equal(epochs_4.events[-2, 0], epochs_4.events[-1, 0]) assert_equal(epochs_1.events.shape[0], epochs_3.events.shape[0]) assert_equal(epochs_3.events.shape[0], epochs_4.events.shape[0]) diff --git a/mne/time_frequency/tests/test_tfr.py b/mne/time_frequency/tests/test_tfr.py index abd7d0786be..4fc6f147377 100644 --- a/mne/time_frequency/tests/test_tfr.py +++ b/mne/time_frequency/tests/test_tfr.py @@ -21,6 +21,7 @@ pick_types, read_events, ) +from mne.epochs import equalize_epoch_counts from mne.io import read_raw_fif from mne.tests.test_epochs import assert_metadata_equal from mne.time_frequency import tfr_array_morlet, tfr_array_multitaper @@ -47,6 +48,28 @@ raw_ctf_fname = data_path / "test_ctf_raw.fif" +def _create_test_epochstfr(): + n_epos = 3 + ch_names = ["EEG 001", "EEG 002", "EEG 003", "EEG 004"] + n_picks = len(ch_names) + ch_types = ["eeg"] * n_picks + n_freqs = 5 + n_times = 6 + data = np.random.rand(n_epos, n_picks, n_freqs, n_times) + times = np.arange(6) + srate = 1000.0 + freqs = np.arange(5) + events = np.zeros((n_epos, 3), dtype=int) + events[:, 0] = np.arange(n_epos) + events[:, 2] = np.arange(5, 5 + n_epos) + event_id = {k: v for v, k in zip(events[:, 2], ["ha", "he", "hu"])} + info = mne.create_info(ch_names, srate, ch_types) + tfr = mne.time_frequency.EpochsTFR( + info, data, times, freqs, events=events, event_id=event_id + ) + return tfr + + def test_tfr_ctf(): """Test that TFRs can be calculated on CTF data.""" raw = read_raw_fif(raw_ctf_fname).crop(0, 1) @@ -723,6 +746,14 @@ def test_init_EpochsTFR(): del tfr +def test_equalize_epochs_tfr_counts(): + """Test equalize_epoch_counts for EpochsTFR.""" + tfr = _create_test_epochstfr() + tfr2 = tfr.copy() + tfr2 = tfr2[:-1] + equalize_epoch_counts([tfr, tfr2]) + + def test_dB_computation(): """Test dB computation in plot methods (gh 11091).""" ampl = 2.0 diff --git a/mne/time_frequency/tfr.py b/mne/time_frequency/tfr.py index 3be20ce28fc..4f8a43b51e3 100644 --- a/mne/time_frequency/tfr.py +++ b/mne/time_frequency/tfr.py @@ -2764,6 +2764,8 @@ def __init__( self.method = method self.preload = True self.metadata = metadata + # we need this to allow equalize_epoch_counts to work with EpochsTFRs + self._bad_dropped = True @property def _detrend_picks(self): @@ -2875,6 +2877,41 @@ def average(self, method="mean", dim="epochs", copy=False): self.freqs = freqs return self + @verbose + def drop(self, indices, reason="USER", verbose=None): + """Drop epochs based on indices or boolean mask. + + .. note:: The indices refer to the current set of undropped epochs + rather than the complete set of dropped and undropped epochs. + They are therefore not necessarily consistent with any + external indices (e.g., behavioral logs). To drop epochs + based on external criteria, do not use the ``preload=True`` + flag when constructing an Epochs object, and call this + method before calling the :meth:`mne.Epochs.drop_bad` or + :meth:`mne.Epochs.load_data` methods. + + Parameters + ---------- + indices : array of int or bool + Set epochs to remove by specifying indices to remove or a boolean + mask to apply (where True values get removed). Events are + correspondingly modified. + reason : str + Reason for dropping the epochs ('ECG', 'timeout', 'blink' etc). + Default: 'USER'. + %(verbose)s + + Returns + ------- + epochs : instance of Epochs or EpochsTFR + The epochs with indices dropped. Operates in-place. + """ + from ..epochs import BaseEpochs + + BaseEpochs.drop(self, indices=indices, reason=reason, verbose=verbose) + + return self + def combine_tfr(all_tfr, weights="nave"): """Merge AverageTFR data by weighted addition. From 7f06cb0096b71d4a6a4cd60c8be666ed046cadd0 Mon Sep 17 00:00:00 2001 From: Daniel McCloy Date: Tue, 13 Feb 2024 14:27:56 -0600 Subject: [PATCH 191/405] don't put pytest marks on test fixtures (#12440) --- mne/conftest.py | 2 -- mne/source_space/tests/test_source_space.py | 1 + 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/mne/conftest.py b/mne/conftest.py index 80380d1a387..17ff64b32f7 100644 --- a/mne/conftest.py +++ b/mne/conftest.py @@ -797,8 +797,6 @@ def mixed_fwd_cov_evoked(_evoked_cov_sphere, _all_src_types_fwd): @pytest.fixture(scope="session") -@pytest.mark.slowtest -@pytest.mark.parametrize(params=[testing._pytest_param()]) def src_volume_labels(): """Create a 7mm source space with labels.""" pytest.importorskip("nibabel") diff --git a/mne/source_space/tests/test_source_space.py b/mne/source_space/tests/test_source_space.py index 4a1e20eef9b..a2648459fa6 100644 --- a/mne/source_space/tests/test_source_space.py +++ b/mne/source_space/tests/test_source_space.py @@ -679,6 +679,7 @@ def test_source_space_from_label(tmp_path, pass_ids): _compare_source_spaces(src, src_from_file, mode="approx") +@pytest.mark.slowtest @testing.requires_testing_data def test_source_space_exclusive_complete(src_volume_labels): """Test that we produce exclusive and complete labels.""" From 70455902ba834bec75ae1fd0f7f5b2fc48455cf8 Mon Sep 17 00:00:00 2001 From: Hamza Abdelhedi Date: Tue, 13 Feb 2024 15:42:08 -0500 Subject: [PATCH 192/405] Apply hilbert stc (#12323) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Eric Larson --- doc/changes/devel/12323.newfeature.rst | 1 + mne/filter.py | 56 +++++++++++++++++------- mne/source_estimate.py | 59 ++++++++++++++++++++++++-- mne/tests/test_evoked.py | 12 +++++- mne/tests/test_source_estimate.py | 30 ++++++++++++- mne/utils/check.py | 3 +- mne/utils/docs.py | 8 +++- 7 files changed, 145 insertions(+), 24 deletions(-) create mode 100644 doc/changes/devel/12323.newfeature.rst diff --git a/doc/changes/devel/12323.newfeature.rst b/doc/changes/devel/12323.newfeature.rst new file mode 100644 index 00000000000..f10fdf5cf23 --- /dev/null +++ b/doc/changes/devel/12323.newfeature.rst @@ -0,0 +1 @@ +Add :meth:`~mne.SourceEstimate.savgol_filter`, :meth:`~mne.SourceEstimate.filter`, :meth:`~mne.SourceEstimate.apply_hilbert`, and :meth:`~mne.SourceEstimate.apply_function` methods to :class:`mne.SourceEstimate` and related classes, by `Hamza Abdelhedi`_. \ No newline at end of file diff --git a/mne/filter.py b/mne/filter.py index 99fdf7f3b00..477434a7ca4 100644 --- a/mne/filter.py +++ b/mne/filter.py @@ -2476,7 +2476,7 @@ def savgol_filter(self, h_freq, verbose=None): Returns ------- - inst : instance of Epochs or Evoked + inst : instance of Epochs, Evoked or SourceEstimate The object with the filtering applied. See Also @@ -2489,6 +2489,8 @@ def savgol_filter(self, h_freq, verbose=None): https://gist.github.com/larsoner/bbac101d50176611136b + When working on SourceEstimates the sample rate of the original data is inferred from tstep. + .. versionadded:: 0.9.0 References @@ -2504,13 +2506,19 @@ def savgol_filter(self, h_freq, verbose=None): >>> evoked.savgol_filter(10.) # low-pass at around 10 Hz # doctest:+SKIP >>> evoked.plot() # doctest:+SKIP """ # noqa: E501 + from .source_estimate import _BaseSourceEstimate + _check_preload(self, "inst.savgol_filter") + if not isinstance(self, _BaseSourceEstimate): + s_freq = self.info["sfreq"] + else: + s_freq = 1 / self.tstep h_freq = float(h_freq) - if h_freq >= self.info["sfreq"] / 2.0: + if h_freq >= s_freq / 2.0: raise ValueError("h_freq must be less than half the sample rate") # savitzky-golay filtering - window_length = (int(np.round(self.info["sfreq"] / h_freq)) // 2) * 2 + 1 + window_length = (int(np.round(s_freq / h_freq)) // 2) * 2 + 1 logger.info("Using savgol length %d" % window_length) self._data[:] = signal.savgol_filter( self._data, axis=-1, polyorder=5, window_length=window_length @@ -2537,7 +2545,7 @@ def filter( *, verbose=None, ): - """Filter a subset of channels. + """Filter a subset of channels/vertices. Parameters ---------- @@ -2561,7 +2569,7 @@ def filter( Returns ------- - inst : instance of Epochs, Evoked, or Raw + inst : instance of Epochs, Evoked, SourceEstimate, or Raw The filtered data. See Also @@ -2598,6 +2606,9 @@ def filter( ``len(picks) * n_times`` additional time points need to be temporarily stored in memory. + When working on SourceEstimates the sample rate of the original + data is inferred from tstep. + For more information, see the tutorials :ref:`disc-filtering` and :ref:`tut-filter-resample` and :func:`mne.filter.create_filter`. @@ -2606,11 +2617,16 @@ def filter( """ from .annotations import _annotations_starts_stops from .io import BaseRaw + from .source_estimate import _BaseSourceEstimate _check_preload(self, "inst.filter") + if not isinstance(self, _BaseSourceEstimate): + update_info, picks = _filt_check_picks(self.info, picks, l_freq, h_freq) + s_freq = self.info["sfreq"] + else: + s_freq = 1.0 / self.tstep if pad is None and method != "iir": pad = "edge" - update_info, picks = _filt_check_picks(self.info, picks, l_freq, h_freq) if isinstance(self, BaseRaw): # Deal with annotations onsets, ends = _annotations_starts_stops( @@ -2629,7 +2645,7 @@ def filter( use_verbose = verbose if si == max_idx else "error" filter_data( self._data[:, start:stop], - self.info["sfreq"], + s_freq, l_freq, h_freq, picks, @@ -2646,9 +2662,10 @@ def filter( pad=pad, verbose=use_verbose, ) - # update info if filter is applied to all data channels, + # update info if filter is applied to all data channels/vertices, # and it's not a band-stop filter - _filt_update_info(self.info, update_info, l_freq, h_freq) + if not isinstance(self, _BaseSourceEstimate): + _filt_update_info(self.info, update_info, l_freq, h_freq) return self @verbose @@ -2703,7 +2720,7 @@ def resample( from .evoked import Evoked # Should be guaranteed by our inheritance, and the fact that - # mne.io.BaseRaw overrides this method + # mne.io.BaseRaw and _BaseSourceEstimate overrides this method assert isinstance(self, (BaseEpochs, Evoked)) sfreq = float(sfreq) @@ -2740,13 +2757,13 @@ def resample( def apply_hilbert( self, picks=None, envelope=False, n_jobs=None, n_fft="auto", *, verbose=None ): - """Compute analytic signal or envelope for a subset of channels. + """Compute analytic signal or envelope for a subset of channels/vertices. Parameters ---------- %(picks_all_data_noref)s envelope : bool - Compute the envelope signal of each channel. Default False. + Compute the envelope signal of each channel/vertex. Default False. See Notes. %(n_jobs)s n_fft : int | None | str @@ -2758,19 +2775,19 @@ def apply_hilbert( Returns ------- - self : instance of Raw, Epochs, or Evoked + self : instance of Raw, Epochs, Evoked or SourceEstimate The raw object with transformed data. Notes ----- **Parameters** - If ``envelope=False``, the analytic signal for the channels defined in + If ``envelope=False``, the analytic signal for the channels/vertices defined in ``picks`` is computed and the data of the Raw object is converted to a complex representation (the analytic signal is complex valued). If ``envelope=True``, the absolute value of the analytic signal for the - channels defined in ``picks`` is computed, resulting in the envelope + channels/vertices defined in ``picks`` is computed, resulting in the envelope signal. .. warning: Do not use ``envelope=True`` if you intend to compute @@ -2803,7 +2820,15 @@ def apply_hilbert( by computing the analytic signal in sensor space, applying the MNE inverse, and computing the envelope in source space. """ + from .source_estimate import _BaseSourceEstimate + + if not isinstance(self, _BaseSourceEstimate): + use_info = self.info + else: + use_info = len(self._data) _check_preload(self, "inst.apply_hilbert") + picks = _picks_to_idx(use_info, picks, exclude=(), with_ref_meg=False) + if n_fft is None: n_fft = len(self.times) elif isinstance(n_fft, str): @@ -2819,7 +2844,6 @@ def apply_hilbert( f"{len(self.times)})" ) dtype = None if envelope else np.complex128 - picks = _picks_to_idx(self.info, picks, exclude=(), with_ref_meg=False) args, kwargs = (), dict(n_fft=n_fft, envelope=envelope) data_in = self._data diff --git a/mne/source_estimate.py b/mne/source_estimate.py index 2bd1ef48dee..e99f260c665 100644 --- a/mne/source_estimate.py +++ b/mne/source_estimate.py @@ -17,13 +17,14 @@ from ._fiff.constants import FIFF from ._fiff.meas_info import Info -from ._fiff.pick import pick_types +from ._fiff.pick import _picks_to_idx, pick_types from ._freesurfer import _get_atlas_values, _get_mri_info_data, read_freesurfer_lut from .baseline import rescale from .cov import Covariance from .evoked import _get_peak -from .filter import resample +from .filter import FilterMixin, _check_fun, resample from .fixes import _safe_svd +from .parallel import parallel_func from .source_space._source_space import ( SourceSpaces, _check_volume_labels, @@ -42,6 +43,7 @@ _check_option, _check_pandas_index_arguments, _check_pandas_installed, + _check_preload, _check_src_normal, _check_stc_units, _check_subject, @@ -494,7 +496,7 @@ def _verify_source_estimate_compat(a, b): ) -class _BaseSourceEstimate(TimeMixin): +class _BaseSourceEstimate(TimeMixin, FilterMixin): _data_ndim = 2 @verbose @@ -642,6 +644,57 @@ def extract_label_time_course( verbose=verbose, ) + @verbose + def apply_function( + self, fun, picks=None, dtype=None, n_jobs=None, verbose=None, **kwargs + ): + """Apply a function to a subset of vertices. + + %(applyfun_summary_stc)s + + Parameters + ---------- + %(fun_applyfun_stc)s + %(picks_all)s + %(dtype_applyfun)s + %(n_jobs)s Ignored if ``vertice_wise=False`` as the workload + is split across vertices. + %(verbose)s + %(kwargs_fun)s + + Returns + ------- + self : instance of _BaseSourceEstimate + The SourceEstimate object with transformed data. + """ + _check_preload(self, "source_estimate.apply_function") + picks = _picks_to_idx(len(self._data), picks, exclude=(), with_ref_meg=False) + + if not callable(fun): + raise ValueError("fun needs to be a function") + + data_in = self._data + if dtype is not None and dtype != self._data.dtype: + self._data = self._data.astype(dtype) + + # check the dimension of the source estimate data + _check_option("source_estimate.ndim", self._data.ndim, [2, 3]) + + parallel, p_fun, n_jobs = parallel_func(_check_fun, n_jobs) + if n_jobs == 1: + # modify data inplace to save memory + for idx in picks: + self._data[idx, :] = _check_fun(fun, data_in[idx, :], **kwargs) + else: + # use parallel function + data_picks_new = parallel( + p_fun(fun, data_in[p, :], **kwargs) for p in picks + ) + for pp, p in enumerate(picks): + self._data[p, :] = data_picks_new[pp] + + return self + @verbose def apply_baseline(self, baseline=(None, 0), *, verbose=None): """Baseline correct source estimate data. diff --git a/mne/tests/test_evoked.py b/mne/tests/test_evoked.py index fbf4c012334..17586b1a465 100644 --- a/mne/tests/test_evoked.py +++ b/mne/tests/test_evoked.py @@ -23,6 +23,7 @@ from mne import ( Epochs, EpochsArray, + SourceEstimate, combine_evoked, create_info, equalize_channels, @@ -917,7 +918,7 @@ def test_evoked_baseline(tmp_path): def test_hilbert(): - """Test hilbert on raw, epochs, and evoked.""" + """Test hilbert on raw, epochs, evoked and SourceEstimate data.""" raw = read_raw_fif(raw_fname).load_data() raw.del_proj() raw.pick(raw.ch_names[:2]) @@ -927,10 +928,17 @@ def test_hilbert(): epochs.apply_hilbert() epochs.load_data() evoked = epochs.average() + # Create SourceEstimate stc data + verts = [np.arange(10), np.arange(90)] + data = np.random.default_rng(0).normal(size=(100, 10)) + stc = SourceEstimate(data, verts, 0, 1e-1, "foo") + raw_hilb = raw.apply_hilbert() epochs_hilb = epochs.apply_hilbert() evoked_hilb = evoked.copy().apply_hilbert() evoked_hilb_2_data = epochs_hilb.get_data(copy=False).mean(0) + stc_hilb = stc.copy().apply_hilbert() + stc_hilb_env = stc.copy().apply_hilbert(envelope=True) assert_allclose(evoked_hilb.data, evoked_hilb_2_data) # This one is only approximate because of edge artifacts evoked_hilb_3 = Epochs(raw_hilb, events).average() @@ -941,6 +949,8 @@ def test_hilbert(): # envelope=True mode evoked_hilb_env = evoked.apply_hilbert(envelope=True) assert_allclose(evoked_hilb_env.data, np.abs(evoked_hilb.data)) + assert len(stc_hilb.data) == len(stc.data) + assert_allclose(stc_hilb_env.data, np.abs(stc_hilb.data)) def test_apply_function_evk(): diff --git a/mne/tests/test_source_estimate.py b/mne/tests/test_source_estimate.py index 1ed5c90623c..08e08761ced 100644 --- a/mne/tests/test_source_estimate.py +++ b/mne/tests/test_source_estimate.py @@ -72,7 +72,7 @@ read_inverse_operator, ) from mne.morph_map import _make_morph_map_hemi -from mne.source_estimate import _get_vol_mask, grade_to_tris +from mne.source_estimate import _get_vol_mask, _make_stc, grade_to_tris from mne.source_space._source_space import _get_src_nn from mne.transforms import apply_trans, invert_transform, transform_surface_to from mne.utils import ( @@ -2055,3 +2055,31 @@ def test_label_extraction_subject(kind): stc.subject = None with pytest.raises(ValueError, match=r"label\.sub.*not match.* sour"): extract_label_time_course(stc, labels_fs, src) + + +def test_apply_function_stc(): + """Check the apply_function method for source estimate data.""" + # Create a sample _BaseSourceEstimate object + n_vertices = 100 + n_times = 200 + vertices = [np.array(np.arange(50)), np.array(np.arange(50, 100))] + tmin = 0.0 + tstep = 0.001 + data = np.random.default_rng(0).normal(size=(n_vertices, n_times)) + + stc = _make_stc(data, vertices, tmin=tmin, tstep=tstep, src_type="surface") + + # A sample function to apply to the data + def fun(data_row, **kwargs): + return 2 * data_row + + # Test applying the function to all vertices without parallelization + stc_copy = stc.copy() + stc.apply_function(fun) + for idx in range(n_vertices): + assert_allclose(stc.data[idx, :], 2 * stc_copy.data[idx, :]) + + # Test applying the function with parallelization + stc.apply_function(fun, n_jobs=2) + for idx in range(n_vertices): + assert_allclose(stc.data[idx, :], 4 * stc_copy.data[idx, :]) diff --git a/mne/utils/check.py b/mne/utils/check.py index 461d7e526a7..13eca1e0ba0 100644 --- a/mne/utils/check.py +++ b/mne/utils/check.py @@ -294,10 +294,11 @@ def _check_preload(inst, msg): """Ensure data are preloaded.""" from ..epochs import BaseEpochs from ..evoked import Evoked + from ..source_estimate import _BaseSourceEstimate from ..time_frequency import _BaseTFR from ..time_frequency.spectrum import BaseSpectrum - if isinstance(inst, (_BaseTFR, Evoked, BaseSpectrum)): + if isinstance(inst, (_BaseTFR, Evoked, BaseSpectrum, _BaseSourceEstimate)): pass else: name = "epochs" if isinstance(inst, BaseEpochs) else "raw" diff --git a/mne/utils/docs.py b/mne/utils/docs.py index cb7c027c039..02bb6825b6e 100644 --- a/mne/utils/docs.py +++ b/mne/utils/docs.py @@ -216,11 +216,11 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): # raw/epochs/evoked apply_function method # apply_function method summary applyfun_summary = """\ -The function ``fun`` is applied to the channels defined in ``picks``. +The function ``fun`` is applied to the channels or vertices defined in ``picks``. The {} object's data is modified in-place. If the function returns a different data type (e.g. :py:obj:`numpy.complex128`) it must be specified using the ``dtype`` parameter, which causes the data type of **all** the data -to change (even if the function is only applied to channels in ``picks``).{} +to change (even if the function is only applied to channels/vertices in ``picks``).{} .. note:: If ``n_jobs`` > 1, more memory is required as ``len(picks) * n_times`` additional time points need to @@ -236,6 +236,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): docdict["applyfun_summary_epochs"] = applyfun_summary.format("epochs", applyfun_preload) docdict["applyfun_summary_evoked"] = applyfun_summary.format("evoked", "") docdict["applyfun_summary_raw"] = applyfun_summary.format("raw", applyfun_preload) +docdict["applyfun_summary_stc"] = applyfun_summary.format("source estimate", "") docdict["area_alpha_plot_psd"] = """\ area_alpha : float @@ -1600,6 +1601,9 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): docdict["fun_applyfun_evoked"] = applyfun_fun_base.format( " because it will apply channel-wise" ) +docdict["fun_applyfun_stc"] = applyfun_fun_base.format( + " because it will apply vertex-wise" +) docdict["fwd"] = """ fwd : instance of Forward From fc771130d7e7bd0a06984848c84eee209af7f4b8 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Wed, 14 Feb 2024 10:10:28 -0500 Subject: [PATCH 193/405] FIX: Ref (#12442) --- mne/source_estimate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mne/source_estimate.py b/mne/source_estimate.py index e99f260c665..91b56289062 100644 --- a/mne/source_estimate.py +++ b/mne/source_estimate.py @@ -664,7 +664,7 @@ def apply_function( Returns ------- - self : instance of _BaseSourceEstimate + self : instance of SourceEstimate The SourceEstimate object with transformed data. """ _check_preload(self, "source_estimate.apply_function") From 8e55fef3a354e2c3dbe086256dfa3a73f698e0c5 Mon Sep 17 00:00:00 2001 From: Alexander Kiefer <56556451+alexk101@users.noreply.github.com> Date: Wed, 14 Feb 2024 09:36:20 -0600 Subject: [PATCH 194/405] Remove incorrect assertions in snirf parsing (#12430) --- doc/changes/devel/12430.bugfix.rst | 1 + doc/changes/names.inc | 2 ++ mne/io/snirf/_snirf.py | 13 +++++-------- 3 files changed, 8 insertions(+), 8 deletions(-) create mode 100644 doc/changes/devel/12430.bugfix.rst diff --git a/doc/changes/devel/12430.bugfix.rst b/doc/changes/devel/12430.bugfix.rst new file mode 100644 index 00000000000..688e7066fa8 --- /dev/null +++ b/doc/changes/devel/12430.bugfix.rst @@ -0,0 +1 @@ +Reformats channel and detector lookup in :func:`mne.io.read_raw_snirf` from array based to dictionary based. Removes incorrect assertions that every detector and source must have data associated with every registered optode position, by :newcontrib:`Alex Kiefer`. \ No newline at end of file diff --git a/doc/changes/names.inc b/doc/changes/names.inc index f090b463cd4..02898d1ff66 100644 --- a/doc/changes/names.inc +++ b/doc/changes/names.inc @@ -20,6 +20,8 @@ .. _Alex Gramfort: https://alexandre.gramfort.net +.. _Alex Kiefer: https://home.alex101.dev + .. _Alex Rockhill: https://github.com/alexrockhill/ .. _Alexander Rudiuk: https://github.com/ARudiuk diff --git a/mne/io/snirf/_snirf.py b/mne/io/snirf/_snirf.py index a7d081983af..0974394a700 100644 --- a/mne/io/snirf/_snirf.py +++ b/mne/io/snirf/_snirf.py @@ -168,7 +168,7 @@ def natural_keys(text): for c in channels ] ) - sources = [f"S{int(s)}" for s in sources] + sources = {int(s): f"S{int(s)}" for s in sources} if "detectorLabels_disabled" in dat["nirs/probe"]: # This is disabled as @@ -185,7 +185,7 @@ def natural_keys(text): for c in channels ] ) - detectors = [f"D{int(d)}" for d in detectors] + detectors = {int(d): f"D{int(d)}" for d in detectors} # Extract source and detector locations # 3D positions are optional in SNIRF, @@ -224,9 +224,6 @@ def natural_keys(text): "location information" ) - assert len(sources) == srcPos3D.shape[0] - assert len(detectors) == detPos3D.shape[0] - chnames = [] ch_types = [] for chan in channels: @@ -248,9 +245,9 @@ def natural_keys(text): )[0] ) ch_name = ( - sources[src_idx - 1] + sources[src_idx] + "_" - + detectors[det_idx - 1] + + detectors[det_idx] + " " + str(fnirs_wavelengths[wve_idx - 1]) ) @@ -265,7 +262,7 @@ def natural_keys(text): # Convert between SNIRF processed names and MNE type names dt_id = dt_id.lower().replace("dod", "fnirs_od") - ch_name = sources[src_idx - 1] + "_" + detectors[det_idx - 1] + ch_name = sources[src_idx] + "_" + detectors[det_idx] if dt_id == "fnirs_od": wve_idx = int( From 85ca0ed7df3ae689ecf7a0109cc95982c5be2895 Mon Sep 17 00:00:00 2001 From: Mathieu Scheltienne Date: Thu, 15 Feb 2024 21:08:43 +0100 Subject: [PATCH 195/405] Fix validation of ch_tpye in annotate_muscle_zscore (#12444) --- doc/changes/devel/12444.bugfix.rst | 1 + mne/preprocessing/artifact_detection.py | 22 +++++++++++++--------- 2 files changed, 14 insertions(+), 9 deletions(-) create mode 100644 doc/changes/devel/12444.bugfix.rst diff --git a/doc/changes/devel/12444.bugfix.rst b/doc/changes/devel/12444.bugfix.rst new file mode 100644 index 00000000000..c27fb5e8425 --- /dev/null +++ b/doc/changes/devel/12444.bugfix.rst @@ -0,0 +1 @@ +Fix validation of ``ch_type`` in :func:`mne.preprocessing.annotate_muscle_zscore`, by `Mathieu Scheltienne`_. diff --git a/mne/preprocessing/artifact_detection.py b/mne/preprocessing/artifact_detection.py index d2bed58fd78..1f3ee7b4946 100644 --- a/mne/preprocessing/artifact_detection.py +++ b/mne/preprocessing/artifact_detection.py @@ -25,7 +25,14 @@ apply_trans, quat_to_rot, ) -from ..utils import _mask_to_onsets_offsets, _pl, _validate_type, logger, verbose +from ..utils import ( + _check_option, + _mask_to_onsets_offsets, + _pl, + _validate_type, + logger, + verbose, +) @verbose @@ -94,16 +101,13 @@ def annotate_muscle_zscore( ch_type = "eeg" else: raise ValueError( - "No M/EEG channel types found, please specify a" - " ch_type or provide M/EEG sensor data" + "No M/EEG channel types found, please specify a 'ch_type' or provide " + "M/EEG sensor data." ) - logger.info("Using %s sensors for muscle artifact detection" % (ch_type)) - - if ch_type in ("mag", "grad"): - raw_copy.pick(ch_type) + logger.info("Using %s sensors for muscle artifact detection", ch_type) else: - ch_type = {"meg": False, ch_type: True} - raw_copy.pick(**ch_type) + _check_option("ch_type", ch_type, ["mag", "grad", "eeg"]) + raw_copy.pick(ch_type) raw_copy.filter( filter_freq[0], From 5e23fe00bf0f7ec9cf75708269bbf2bd95a2dad6 Mon Sep 17 00:00:00 2001 From: Sophie Herbst Date: Fri, 16 Feb 2024 17:18:57 +0100 Subject: [PATCH 196/405] ENH: Add image_kwargs to report.add_epochs (#12443) 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 --- doc/changes/devel/12443.newfeature.rst | 1 + mne/report/report.py | 19 +++++++++++++++++-- mne/report/tests/test_report.py | 18 ++++++++++++++---- 3 files changed, 32 insertions(+), 6 deletions(-) create mode 100644 doc/changes/devel/12443.newfeature.rst diff --git a/doc/changes/devel/12443.newfeature.rst b/doc/changes/devel/12443.newfeature.rst new file mode 100644 index 00000000000..f704e45b4a5 --- /dev/null +++ b/doc/changes/devel/12443.newfeature.rst @@ -0,0 +1 @@ +Add option to pass ``image_kwargs`` to :class:`mne.Report.add_epochs` to allow adjusting e.g. ``vmin`` and ``vmax`` of the epochs image in the report, by `Sophie Herbst`_. \ No newline at end of file diff --git a/mne/report/report.py b/mne/report/report.py index 534377d62e3..4f7cbed04eb 100644 --- a/mne/report/report.py +++ b/mne/report/report.py @@ -1092,6 +1092,7 @@ def add_epochs( *, psd=True, projs=None, + image_kwargs=None, topomap_kwargs=None, drop_log_ignore=("IGNORED",), tags=("epochs",), @@ -1120,6 +1121,11 @@ def add_epochs( If ``True``, add PSD plots based on all ``epochs``. If ``False``, do not add PSD plots. %(projs_report)s + image_kwargs : dict | None + Keyword arguments to pass to the "epochs image"-generating + function (:meth:`mne.Epochs.plot_image`). + + .. versionadded:: 1.7 %(topomap_kwargs)s drop_log_ignore : array-like of str The drop reasons to ignore when creating the drop log bar plot. @@ -1130,7 +1136,7 @@ def add_epochs( Notes ----- - .. versionadded:: 0.24.0 + .. versionadded:: 0.24 """ tags = _check_tags(tags) add_projs = self.projs if projs is None else projs @@ -1138,6 +1144,7 @@ def add_epochs( epochs=epochs, psd=psd, add_projs=add_projs, + image_kwargs=image_kwargs, topomap_kwargs=topomap_kwargs, drop_log_ignore=drop_log_ignore, section=title, @@ -3900,6 +3907,7 @@ def _add_epochs( epochs, psd, add_projs, + image_kwargs, topomap_kwargs, drop_log_ignore, image_format, @@ -3934,9 +3942,16 @@ def _add_epochs( ch_types = _get_data_ch_types(epochs) epochs.load_data() + if image_kwargs is None: + image_kwargs = dict() + for ch_type in ch_types: with use_log_level(_verbose_safe_false(level="error")): - figs = epochs.copy().pick(ch_type, verbose=False).plot_image(show=False) + figs = ( + epochs.copy() + .pick(ch_type, verbose=False) + .plot_image(show=False, **image_kwargs) + ) assert len(figs) == 1 fig = figs[0] diff --git a/mne/report/tests/test_report.py b/mne/report/tests/test_report.py index 5f549093581..65d3ceb697a 100644 --- a/mne/report/tests/test_report.py +++ b/mne/report/tests/test_report.py @@ -884,6 +884,8 @@ def test_manual_report_2d(tmp_path, invisible_fig): raw = read_raw_fif(raw_fname) raw.pick(raw.ch_names[:6]).crop(10, None) raw.info.normalize_proj() + raw_non_preloaded = raw.copy() + raw.load_data() cov = read_cov(cov_fname) cov = pick_channels_cov(cov, raw.ch_names) events = read_events(events_fname) @@ -899,7 +901,12 @@ def test_manual_report_2d(tmp_path, invisible_fig): events=events, event_id=event_id, tmin=-0.2, tmax=0.5, sfreq=raw.info["sfreq"] ) epochs_without_metadata = Epochs( - raw=raw, events=events, event_id=event_id, baseline=None + raw=raw, + events=events, + event_id=event_id, + baseline=None, + decim=10, + verbose="error", ) epochs_with_metadata = Epochs( raw=raw, @@ -907,9 +914,11 @@ def test_manual_report_2d(tmp_path, invisible_fig): event_id=metadata_event_id, baseline=None, metadata=metadata, + decim=10, + verbose="error", ) evokeds = read_evokeds(evoked_fname) - evoked = evokeds[0].pick("eeg") + evoked = evokeds[0].pick("eeg").decimate(10, verbose="error") with pytest.warns(ConvergenceWarning, match="did not converge"): ica = ICA(n_components=3, max_iter=1, random_state=42).fit( @@ -927,6 +936,7 @@ def test_manual_report_2d(tmp_path, invisible_fig): tags=("epochs",), psd=False, projs=False, + image_kwargs=dict(colorbar=False), ) r.add_epochs( epochs=epochs_without_metadata, title="my epochs 2", psd=1, projs=False @@ -963,11 +973,11 @@ def test_manual_report_2d(tmp_path, invisible_fig): ) r.add_ica(ica=ica, title="my ica", inst=None) with pytest.raises(RuntimeError, match="not preloaded"): - r.add_ica(ica=ica, title="ica", inst=raw) + r.add_ica(ica=ica, title="ica", inst=raw_non_preloaded) r.add_ica( ica=ica, title="my ica with raw inst", - inst=raw.copy().load_data(), + inst=raw, picks=[2], ecg_evoked=ica_ecg_evoked, eog_evoked=ica_eog_evoked, From 49e895fd9c5ab44b92c73aaa210b223d4ad633f7 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Fri, 16 Feb 2024 15:24:19 -0500 Subject: [PATCH 197/405] ENH: Multiple raw instance support to head pos average (#12445) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- doc/changes/devel/12445.newfeature.rst | 1 + mne/preprocessing/artifact_detection.py | 68 ++++++++++++----- .../tests/test_artifact_detection.py | 76 ++++++++++++++++++- mne/preprocessing/tests/test_fine_cal.py | 23 +++--- mne/transforms.py | 27 ++++++- 5 files changed, 159 insertions(+), 36 deletions(-) create mode 100644 doc/changes/devel/12445.newfeature.rst diff --git a/doc/changes/devel/12445.newfeature.rst b/doc/changes/devel/12445.newfeature.rst new file mode 100644 index 00000000000..ccaef2c2c07 --- /dev/null +++ b/doc/changes/devel/12445.newfeature.rst @@ -0,0 +1 @@ +Add support for multiple raw instances in :func:`mne.preprocessing.compute_average_dev_head_t` by `Eric Larson`_. diff --git a/mne/preprocessing/artifact_detection.py b/mne/preprocessing/artifact_detection.py index 1f3ee7b4946..514eadb00a9 100644 --- a/mne/preprocessing/artifact_detection.py +++ b/mne/preprocessing/artifact_detection.py @@ -32,6 +32,7 @@ _validate_type, logger, verbose, + warn, ) @@ -293,7 +294,8 @@ def annotate_movement( return annot, disp -def compute_average_dev_head_t(raw, pos): +@verbose +def compute_average_dev_head_t(raw, pos, *, verbose=None): """Get new device to head transform based on good segments. Segments starting with "BAD" annotations are not included for calculating @@ -301,19 +303,59 @@ def compute_average_dev_head_t(raw, pos): Parameters ---------- - raw : instance of Raw - Data to compute head position. - pos : array, shape (N, 10) - The position and quaternion parameters from cHPI fitting. + raw : instance of Raw | list of Raw + Data to compute head position. Can be a list containing multiple raw + instances. + pos : array, shape (N, 10) | list of ndarray + The position and quaternion parameters from cHPI fitting. Can be + a list containing multiple position arrays, one per raw instance passed. + %(verbose)s Returns ------- dev_head_t : instance of Transform New ``dev_head_t`` transformation using the averaged good head positions. + + Notes + ----- + .. versionchanged:: 1.7 + Support for multiple raw instances and position arrays was added. """ + # Get weighted head pos trans and rot + if not isinstance(raw, (list, tuple)): + raw = [raw] + if not isinstance(pos, (list, tuple)): + pos = [pos] + if len(pos) != len(raw): + raise ValueError( + f"Number of head positions ({len(pos)}) must match the number of raw " + f"instances ({len(raw)})" + ) + hp = list() + dt = list() + for ri, (r, p) in enumerate(zip(raw, pos)): + _validate_type(r, BaseRaw, f"raw[{ri}]") + _validate_type(p, np.ndarray, f"pos[{ri}]") + hp_, dt_ = _raw_hp_weights(r, p) + hp.append(hp_) + dt.append(dt_) + hp = np.concatenate(hp, axis=0) + dt = np.concatenate(dt, axis=0) + dt /= dt.sum() + best_q = _average_quats(hp[:, 1:4], weights=dt) + trans = np.eye(4) + trans[:3, :3] = quat_to_rot(best_q) + trans[:3, 3] = dt @ hp[:, 4:7] + dist = np.linalg.norm(trans[:3, 3]) + if dist > 1: # less than 1 meter is sane + warn(f"Implausible head position detected: {dist} meters from device origin") + dev_head_t = Transform("meg", "head", trans) + return dev_head_t + + +def _raw_hp_weights(raw, pos): sfreq = raw.info["sfreq"] seg_good = np.ones(len(raw.times)) - trans_pos = np.zeros(3) hp = pos.copy() hp_ts = hp[:, 0] - raw._first_time @@ -353,19 +395,7 @@ def compute_average_dev_head_t(raw, pos): assert (dt >= 0).all() dt = dt / sfreq del seg_good, idx - - # Get weighted head pos trans and rot - trans_pos += np.dot(dt, hp[:, 4:7]) - - rot_qs = hp[:, 1:4] - best_q = _average_quats(rot_qs, weights=dt) - - trans = np.eye(4) - trans[:3, :3] = quat_to_rot(best_q) - trans[:3, 3] = trans_pos / dt.sum() - assert np.linalg.norm(trans[:3, 3]) < 1 # less than 1 meter is sane - dev_head_t = Transform("meg", "head", trans) - return dev_head_t + return hp, dt def _annotations_from_mask(times, mask, annot_name, orig_time=None): diff --git a/mne/preprocessing/tests/test_artifact_detection.py b/mne/preprocessing/tests/test_artifact_detection.py index af01fa4416d..6aa386d0b05 100644 --- a/mne/preprocessing/tests/test_artifact_detection.py +++ b/mne/preprocessing/tests/test_artifact_detection.py @@ -18,6 +18,7 @@ compute_average_dev_head_t, ) from mne.tests.test_annotations import _assert_annotations_equal +from mne.transforms import _angle_dist_between_rigid, quat_to_rot, rot_to_quat data_path = testing.data_path(download=False) sss_path = data_path / "SSS" @@ -35,6 +36,7 @@ def test_movement_annotation_head_correction(meas_date): raw.set_meas_date(None) else: assert meas_date == "orig" + raw_unannot = raw.copy() # Check 5 rotation segments are detected annot_rot, [] = annotate_movement(raw, pos, rotation_velocity_limit=5) @@ -67,7 +69,7 @@ def test_movement_annotation_head_correction(meas_date): _assert_annotations_equal(annot_all_2, annot_all) assert annot_all.orig_time == raw.info["meas_date"] raw.set_annotations(annot_all) - dev_head_t = compute_average_dev_head_t(raw, pos) + dev_head_t = compute_average_dev_head_t(raw, pos)["trans"] dev_head_t_ori = np.array( [ @@ -78,13 +80,83 @@ def test_movement_annotation_head_correction(meas_date): ] ) - assert_allclose(dev_head_t_ori, dev_head_t["trans"], rtol=1e-5, atol=0) + assert_allclose(dev_head_t_ori, dev_head_t, rtol=1e-5, atol=0) + + with pytest.raises(ValueError, match="Number of .* must match .*"): + compute_average_dev_head_t([raw], [pos] * 2) + # Using two identical ones should be identical ... + dev_head_t_double = compute_average_dev_head_t([raw] * 2, [pos] * 2)["trans"] + assert_allclose(dev_head_t, dev_head_t_double) + # ... unannotated and annotated versions differ ... + dev_head_t_unannot = compute_average_dev_head_t(raw_unannot, pos)["trans"] + rot_tol = 1.5e-3 + mov_tol = 1e-3 + assert not np.allclose( + dev_head_t_unannot[:3, :3], + dev_head_t[:3, :3], + atol=rot_tol, + rtol=0, + ) + assert not np.allclose( + dev_head_t_unannot[:3, 3], + dev_head_t[:3, 3], + atol=mov_tol, + rtol=0, + ) + # ... and Averaging the two is close to (but not identical!) to operating on the two + # files. Note they shouldn't be identical because there are more time points + # included in the unannotated version! + dev_head_t_naive = np.eye(4) + dev_head_t_naive[:3, :3] = quat_to_rot( + np.mean( + rot_to_quat(np.array([dev_head_t[:3, :3], dev_head_t_unannot[:3, :3]])), + axis=0, + ) + ) + dev_head_t_naive[:3, 3] = np.mean( + [dev_head_t[:3, 3], dev_head_t_unannot[:3, 3]], axis=0 + ) + dev_head_t_combo = compute_average_dev_head_t([raw, raw_unannot], [pos] * 2)[ + "trans" + ] + unit_kw = dict(distance_units="mm", angle_units="deg") + deg_annot_combo, mm_annot_combo = _angle_dist_between_rigid( + dev_head_t, + dev_head_t_combo, + **unit_kw, + ) + deg_unannot_combo, mm_unannot_combo = _angle_dist_between_rigid( + dev_head_t_unannot, + dev_head_t_combo, + **unit_kw, + ) + deg_annot_unannot, mm_annot_unannot = _angle_dist_between_rigid( + dev_head_t, + dev_head_t_unannot, + **unit_kw, + ) + deg_combo_naive, mm_combo_naive = _angle_dist_between_rigid( + dev_head_t_combo, + dev_head_t_naive, + **unit_kw, + ) + # combo<->naive closer than combo<->annotated closer than annotated<->unannotated + assert 0.05 < deg_combo_naive < deg_annot_combo < deg_annot_unannot < 1.5 + assert 0.1 < mm_combo_naive < mm_annot_combo < mm_annot_unannot < 2 + # combo<->naive closer than combo<->unannotated closer than annotated<->unannotated + assert 0.05 < deg_combo_naive < deg_unannot_combo < deg_annot_unannot < 1.5 + assert 0.12 < mm_combo_naive < mm_unannot_combo < mm_annot_unannot < 2.0 # Smoke test skipping time due to previous annotations. raw.set_annotations(Annotations([raw.times[0]], 0.1, "bad")) annot_dis, _ = annotate_movement(raw, pos, mean_distance_limit=0.02) assert annot_dis.duration.size == 1 + # really far should warn + pos[:, 4] += 5 + with pytest.warns(RuntimeWarning, match="Implausible head position"): + compute_average_dev_head_t(raw, pos) + @testing.requires_testing_data @pytest.mark.parametrize("meas_date", (None, "orig")) diff --git a/mne/preprocessing/tests/test_fine_cal.py b/mne/preprocessing/tests/test_fine_cal.py index 95c9e7d63ba..2b3d4df0e3f 100644 --- a/mne/preprocessing/tests/test_fine_cal.py +++ b/mne/preprocessing/tests/test_fine_cal.py @@ -18,7 +18,7 @@ write_fine_calibration, ) from mne.preprocessing.tests.test_maxwell import _assert_shielding -from mne.transforms import _angle_between_quats, rot_to_quat +from mne.transforms import _angle_dist_between_rigid from mne.utils import object_diff # Define fine calibration filepaths @@ -75,16 +75,17 @@ def test_compute_fine_cal(): orig_trans = _loc_to_coil_trans(orig_locs) want_trans = _loc_to_coil_trans(want_locs) got_trans = _loc_to_coil_trans(got_locs) - dist = np.linalg.norm(got_trans[:, :3, 3] - want_trans[:, :3, 3], axis=1) - assert_allclose(dist, 0.0, atol=1e-6) - dist = np.linalg.norm(got_trans[:, :3, 3] - orig_trans[:, :3, 3], axis=1) - assert_allclose(dist, 0.0, atol=1e-6) - orig_quat = rot_to_quat(orig_trans[:, :3, :3]) - want_quat = rot_to_quat(want_trans[:, :3, :3]) - got_quat = rot_to_quat(got_trans[:, :3, :3]) - want_orig_angles = np.rad2deg(_angle_between_quats(want_quat, orig_quat)) - got_want_angles = np.rad2deg(_angle_between_quats(got_quat, want_quat)) - got_orig_angles = np.rad2deg(_angle_between_quats(got_quat, orig_quat)) + want_orig_angles, want_orig_dist = _angle_dist_between_rigid( + want_trans, orig_trans, angle_units="deg" + ) + got_want_angles, got_want_dist = _angle_dist_between_rigid( + got_trans, want_trans, angle_units="deg" + ) + got_orig_angles, got_orig_dist = _angle_dist_between_rigid( + got_trans, orig_trans, angle_units="deg" + ) + assert_allclose(got_want_dist, 0.0, atol=1e-6) + assert_allclose(got_orig_dist, 0.0, atol=1e-6) for key in ("mag", "grad"): # imb_cals value p = pick_types(raw.info, meg=key, exclude=()) diff --git a/mne/transforms.py b/mne/transforms.py index cb387582ef8..975a4818910 100644 --- a/mne/transforms.py +++ b/mne/transforms.py @@ -1351,6 +1351,28 @@ def _quat_to_affine(quat): return affine +def _affine_to_quat(affine): + assert affine.shape[-2:] == (4, 4) + return np.concatenate( + [rot_to_quat(affine[..., :3, :3]), affine[..., :3, 3]], + axis=-1, + ) + + +def _angle_dist_between_rigid(a, b=None, *, angle_units="rad", distance_units="m"): + a = _affine_to_quat(a) + b = np.zeros(6) if b is None else _affine_to_quat(b) + ang = _angle_between_quats(a[..., :3], b[..., :3]) + dist = np.linalg.norm(a[..., 3:] - b[..., 3:], axis=-1) + assert isinstance(angle_units, str) and angle_units in ("rad", "deg") + if angle_units == "deg": + ang = np.rad2deg(ang) + assert isinstance(distance_units, str) and distance_units in ("m", "mm") + if distance_units == "mm": + dist *= 1e3 + return ang, dist + + def _angle_between_quats(x, y=None): """Compute the ang between two quaternions w/3-element representations.""" # z = conj(x) * y @@ -1839,10 +1861,7 @@ def _compute_volume_registration( # report some useful information if step in ("translation", "rigid"): - dist = np.linalg.norm(reg_affine[:3, 3]) - angle = np.rad2deg( - _angle_between_quats(np.zeros(3), rot_to_quat(reg_affine[:3, :3])) - ) + angle, dist = _angle_dist_between_rigid(reg_affine, angle_units="deg") logger.info(f" Translation: {dist:6.1f} mm") if step == "rigid": logger.info(f" Rotation: {angle:6.1f}°") From c410b6ddeb1f04562f83eed7d5418ff21254f7fd Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Fri, 16 Feb 2024 17:23:08 -0500 Subject: [PATCH 198/405] FIX: na_rep for reports (#12447) --- mne/report/report.py | 114 ++++++++++++++++++++++--------------------- 1 file changed, 58 insertions(+), 56 deletions(-) diff --git a/mne/report/report.py b/mne/report/report.py index 4f7cbed04eb..7e80047a32b 100644 --- a/mne/report/report.py +++ b/mne/report/report.py @@ -3835,63 +3835,9 @@ def _add_epochs_metadata(self, *, epochs, section, tags, replace): metadata.index.name = "Epoch #" assert metadata.index.is_unique - index_name = metadata.index.name # store for later use + data_id = metadata.index.name # store for later use metadata = metadata.reset_index() # We want "proper" columns only - html = metadata.to_html( - border=0, - index=False, - show_dimensions=True, - justify="unset", - float_format=lambda x: f"{round(x, 3):.3f}", - classes="table table-hover table-striped " - "table-sm table-responsive small", - ) - del metadata - - # Massage the table such that it woks nicely with bootstrap-table - htmls = html.split("\n") - header_pattern = "(.*)" - - for idx, html in enumerate(htmls): - if "' - ) - continue - - col_headers = re.findall(pattern=header_pattern, string=html) - if col_headers: - # Make columns sortable - assert len(col_headers) == 1 - col_header = col_headers[0] - htmls[idx] = html.replace( - "", - f'', - ) - - html = "\n".join(htmls) + html = _df_bootstrap_table(df=metadata, data_id=data_id) self._add_html_element( div_klass="epochs", tags=tags, @@ -4388,3 +4334,59 @@ def __call__(self, block, block_vars, gallery_conf): def copyfiles(self, *args, **kwargs): for key, value in self.files.items(): copyfile(key, value) + + +def _df_bootstrap_table(*, df, data_id): + html = df.to_html( + border=0, + index=False, + show_dimensions=True, + justify="unset", + float_format=lambda x: f"{x:.3f}", + classes="table table-hover table-striped table-sm table-responsive small", + na_rep="", + ) + htmls = html.split("\n") + header_pattern = "(.*)" + + for idx, html in enumerate(htmls): + if "' + ) + continue + + col_headers = re.findall(pattern=header_pattern, string=html) + if col_headers: + # Make columns sortable + assert len(col_headers) == 1 + col_header = col_headers[0] + htmls[idx] = html.replace( + "", + f'', + ) + + html = "\n".join(htmls) + return html From 614424718fa911cc0cfacc0df85652b3efaee358 Mon Sep 17 00:00:00 2001 From: Alex Rockhill Date: Fri, 16 Feb 2024 15:38:55 -0800 Subject: [PATCH 199/405] DOC: add missing info to interpolate bads docstring (#12448) --- mne/channels/channels.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mne/channels/channels.py b/mne/channels/channels.py index aee085891c4..0d0af8279cb 100644 --- a/mne/channels/channels.py +++ b/mne/channels/channels.py @@ -839,6 +839,8 @@ def interpolate_bads( - ``"meg"`` channels support ``"MNE"`` (default) and ``"nan"`` - ``"eeg"`` channels support ``"spline"`` (default), ``"MNE"`` and ``"nan"`` - ``"fnirs"`` channels support ``"nearest"`` (default) and ``"nan"`` + - ``"ecog"`` channels support ``"spline"`` (default) and ``"nan"`` + - ``"seeg"`` channels support ``"spline"`` (default) and ``"nan"`` None is an alias for:: From b210678357b1dae7f880d11392863be5fda4983e Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Mon, 19 Feb 2024 14:15:45 -0500 Subject: [PATCH 200/405] MAINT: Fix CIs for PyQt6 (#12452) --- azure-pipelines.yml | 4 ++-- pyproject.toml | 4 ++-- tools/azure_dependencies.sh | 4 ++-- tools/github_actions_dependencies.sh | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 6cac2d5990f..43cdb1db960 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -104,7 +104,7 @@ stages: - bash: | set -e python -m pip install --progress-bar off --upgrade pip - python -m pip install --progress-bar off "mne-qt-browser[opengl] @ git+https://github.com/mne-tools/mne-qt-browser.git@main" pyvista scikit-learn pytest-error-for-skips python-picard "PyQt6!=6.5.1,!=6.6.1" "PyQt6-Qt6!=6.6.1" qtpy nibabel sphinx-gallery + python -m pip install --progress-bar off "mne-qt-browser[opengl] @ git+https://github.com/mne-tools/mne-qt-browser.git@main" pyvista scikit-learn pytest-error-for-skips python-picard "PyQt6!=6.5.1,!=6.6.1,!=6.6.2" "PyQt6-Qt6!=6.6.1,!=6.6.2" qtpy nibabel sphinx-gallery python -m pip uninstall -yq mne python -m pip install --progress-bar off --upgrade -e .[test] displayName: 'Install dependencies with pip' @@ -183,7 +183,7 @@ stages: displayName: 'Get test data' - bash: | set -e - python -m pip install "PyQt6!=6.6.1" "PyQt6-Qt6!=6.6.1" + python -m pip install "PyQt6!=6.6.1,!=6.6.2" "PyQt6-Qt6!=6.6.1,!=6.6.2" LD_DEBUG=libs python -c "from PyQt6.QtWidgets import QApplication, QWidget; app = QApplication([]); import matplotlib; matplotlib.use('QtAgg'); import matplotlib.pyplot as plt; plt.figure()" - bash: | mne sys_info -pd diff --git a/pyproject.toml b/pyproject.toml index 998ceffc5e2..39f2d2ee32b 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", + "PyQt6!=6.6.1,!=6.6.2", + "PyQt6-Qt6!=6.6.1,!=6.6.2", "pyobjc-framework-Cocoa>=5.2.0; platform_system=='Darwin'", "sip", "scikit-learn", diff --git a/tools/azure_dependencies.sh b/tools/azure_dependencies.sh index bfaa96a19e6..53629f256a8 100755 --- a/tools/azure_dependencies.sh +++ b/tools/azure_dependencies.sh @@ -6,8 +6,8 @@ if [ "${TEST_MODE}" == "pip" ]; then python -m pip install --only-binary="numba,llvmlite,numpy,scipy,vtk" -e .[test,full] elif [ "${TEST_MODE}" == "pip-pre" ]; then STD_ARGS="$STD_ARGS --pre" - # python -m pip install $STD_ARGS --only-binary ":all:" --extra-index-url "https://www.riverbankcomputing.com/pypi/simple" "PyQt6!=6.6.1" PyQt6-sip PyQt6-Qt6 "PyQt6-Qt6!=6.6.1" - python -m pip install $STD_ARGS --only-binary ":all:" "PyQt6!=6.6.1" PyQt6-sip PyQt6-Qt6 "PyQt6-Qt6!=6.6.1" + # python -m pip install $STD_ARGS --only-binary ":all:" --extra-index-url "https://www.riverbankcomputing.com/pypi/simple" "PyQt6!=6.6.1,!=6.6.2" PyQt6-sip PyQt6-Qt6 "PyQt6-Qt6!=6.6.1,!=6.6.2" + python -m pip install $STD_ARGS --only-binary ":all:" "PyQt6!=6.6.1,!=6.6.2" PyQt6-sip PyQt6-Qt6 "PyQt6-Qt6!=6.6.1,!=6.6.2" echo "Numpy etc." python -m pip install $STD_ARGS --only-binary ":all:" --default-timeout=60 --extra-index-url "https://pypi.anaconda.org/scientific-python-nightly-wheels/simple" "numpy>=2.0.0.dev0" "scipy>=1.12.0.dev0" "scikit-learn>=1.5.dev0" matplotlib pillow statsmodels pyarrow # echo "dipy" diff --git a/tools/github_actions_dependencies.sh b/tools/github_actions_dependencies.sh index cc5d9fffc48..11529447b77 100755 --- a/tools/github_actions_dependencies.sh +++ b/tools/github_actions_dependencies.sh @@ -27,8 +27,8 @@ else pip uninstall -yq numpy echo "PyQt6" # Now broken in latest release and in the pre release: - # pip install $STD_ARGS --only-binary ":all:" --default-timeout=60 --extra-index-url https://www.riverbankcomputing.com/pypi/simple "PyQt6!=6.6.1" "PyQt6-Qt6!=6.6.1" - pip install $STD_ARGS --only-binary ":all:" --default-timeout=60 "PyQt6!=6.6.1" "PyQt6-Qt6!=6.6.1" + # pip install $STD_ARGS --only-binary ":all:" --default-timeout=60 --extra-index-url https://www.riverbankcomputing.com/pypi/simple "PyQt6!=6.6.1,!=6.6.2" "PyQt6-Qt6!=6.6.1,!=6.6.2" + pip install $STD_ARGS --only-binary ":all:" --default-timeout=60 "PyQt6!=6.6.1,!=6.6.2" "PyQt6-Qt6!=6.6.1,!=6.6.2" echo "NumPy/SciPy/pandas etc." pip install $STD_ARGS --only-binary ":all:" --default-timeout=60 --extra-index-url "https://pypi.anaconda.org/scientific-python-nightly-wheels/simple" "numpy>=2.0.0.dev0" "scipy>=1.12.0.dev0" "scikit-learn>=1.5.dev0" matplotlib pillow statsmodels pyarrow # No pandas, dipy, h5py, openmeeg, python-picard (needs numexpr) until they update to NumPy 2.0 compat From 7d5329ee5aa1f14b5450abebaf9caeecd3ccdb8f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 19 Feb 2024 20:02:01 -0500 Subject: [PATCH 201/405] [pre-commit.ci] pre-commit autoupdate (#12453) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7558ace0222..b564f516d2b 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.2.1 + rev: v0.2.2 hooks: - id: ruff name: ruff lint mne @@ -32,7 +32,7 @@ repos: # yamllint - repo: https://github.com/adrienverge/yamllint.git - rev: v1.34.0 + rev: v1.35.1 hooks: - id: yamllint args: [--strict, -c, .yamllint.yml] From abfa0a6256b8f7f938ea91b1a7146c1a522f01dc Mon Sep 17 00:00:00 2001 From: Richard Scholz <33288574+scholzri@users.noreply.github.com> Date: Wed, 21 Feb 2024 13:56:56 +0000 Subject: [PATCH 202/405] ENH: Support partial pathlength factors for each wavelength in Beer-Lambert law (#12446) Co-authored-by: Alexandre Gramfort Co-authored-by: Eric Larson --- doc/changes/devel/12446.newfeature.rst | 1 + doc/changes/names.inc | 2 ++ mne/preprocessing/nirs/_beer_lambert_law.py | 18 ++++++++++++++---- .../nirs/tests/test_beer_lambert_law.py | 2 +- 4 files changed, 18 insertions(+), 5 deletions(-) create mode 100644 doc/changes/devel/12446.newfeature.rst diff --git a/doc/changes/devel/12446.newfeature.rst b/doc/changes/devel/12446.newfeature.rst new file mode 100644 index 00000000000..734721ce628 --- /dev/null +++ b/doc/changes/devel/12446.newfeature.rst @@ -0,0 +1 @@ +Support partial pathlength factors for each wavelength in :func:`mne.preprocessing.nirs.beer_lambert_law`, by :newcontrib:`Richard Scholz`. diff --git a/doc/changes/names.inc b/doc/changes/names.inc index 02898d1ff66..b3c74318975 100644 --- a/doc/changes/names.inc +++ b/doc/changes/names.inc @@ -474,6 +474,8 @@ .. _Richard Koehler: https://github.com/richardkoehler +.. _Richard Scholz: https://github.com/scholzri + .. _Riessarius Stargardsky: https://github.com/Riessarius .. _Roan LaPlante: https://github.com/aestrivex diff --git a/mne/preprocessing/nirs/_beer_lambert_law.py b/mne/preprocessing/nirs/_beer_lambert_law.py index f6f17a1ae04..9a39a342e50 100644 --- a/mne/preprocessing/nirs/_beer_lambert_law.py +++ b/mne/preprocessing/nirs/_beer_lambert_law.py @@ -25,8 +25,11 @@ def beer_lambert_law(raw, ppf=6.0): ---------- raw : instance of Raw The optical density data. - ppf : float - The partial pathlength factor. + ppf : tuple | float + The partial pathlength factors for each wavelength. + + .. versionchanged:: 1.7 + Support for different factors for the two wavelengths. Returns ------- @@ -35,8 +38,15 @@ def beer_lambert_law(raw, ppf=6.0): """ raw = raw.copy().load_data() _validate_type(raw, BaseRaw, "raw") - _validate_type(ppf, "numeric", "ppf") - ppf = float(ppf) + _validate_type(ppf, ("numeric", "array-like"), "ppf") + ppf = np.array(ppf, float) + if ppf.ndim == 0: # upcast single float to shape (2,) + ppf = np.array([ppf, ppf]) + if ppf.shape != (2,): + raise ValueError( + f"ppf must be float or array-like of shape (2,), got shape {ppf.shape}" + ) + ppf = ppf[:, np.newaxis] # shape (2, 1) picks = _validate_nirs_info(raw.info, fnirs="od", which="Beer-lambert") # This is the one place we *really* need the actual/accurate frequencies freqs = np.array([raw.info["chs"][pick]["loc"][9] for pick in picks], float) diff --git a/mne/preprocessing/nirs/tests/test_beer_lambert_law.py b/mne/preprocessing/nirs/tests/test_beer_lambert_law.py index 29dd6b3bd4d..da5341b17d5 100644 --- a/mne/preprocessing/nirs/tests/test_beer_lambert_law.py +++ b/mne/preprocessing/nirs/tests/test_beer_lambert_law.py @@ -78,7 +78,7 @@ def test_beer_lambert_v_matlab(): pymatreader = pytest.importorskip("pymatreader") raw = read_raw_nirx(fname_nirx_15_0) raw = optical_density(raw) - raw = beer_lambert_law(raw, ppf=0.121) + raw = beer_lambert_law(raw, ppf=(0.121, 0.121)) raw._data *= 1e6 # Scale to uM for comparison to MATLAB matlab_fname = ( From b8ff49750fd8381ad4ef6615aa941270d1e3c877 Mon Sep 17 00:00:00 2001 From: Clemens Brunner Date: Wed, 21 Feb 2024 15:23:35 +0100 Subject: [PATCH 203/405] Disable interpolation in BrainVision config parser (#12456) --- doc/changes/devel/12456.bugfix.rst | 1 + mne/io/brainvision/brainvision.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 doc/changes/devel/12456.bugfix.rst diff --git a/doc/changes/devel/12456.bugfix.rst b/doc/changes/devel/12456.bugfix.rst new file mode 100644 index 00000000000..01e15b3c22e --- /dev/null +++ b/doc/changes/devel/12456.bugfix.rst @@ -0,0 +1 @@ +Disable config parser interpolation when reading BrainVision files, which allows using the percent sign as a regular character in channel units, by `Clemens Brunner`_. \ No newline at end of file diff --git a/mne/io/brainvision/brainvision.py b/mne/io/brainvision/brainvision.py index 5aabdbb626c..9a8531a22d1 100644 --- a/mne/io/brainvision/brainvision.py +++ b/mne/io/brainvision/brainvision.py @@ -447,7 +447,7 @@ def _aux_hdr_info(hdr_fname): params, settings = settings.split("[Comment]") else: params, settings = settings, "" - cfg = configparser.ConfigParser() + cfg = configparser.ConfigParser(interpolation=None) with StringIO(params) as fid: cfg.read_file(fid) From a867e5cd372ab8ee980efffd14c7d157c5b73bf3 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Wed, 21 Feb 2024 09:32:54 -0500 Subject: [PATCH 204/405] BUG: Fix bug with BIDS split saving (#12451) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Richard Höchenberger Co-authored-by: Stefan Appelhoff --- doc/changes/devel/12451.bugfix.rst | 1 + doc/changes/devel/12451.dependency.rst | 1 + mne/conftest.py | 31 ++++++---------- mne/datasets/__init__.pyi | 2 +- mne/datasets/_infant/__init__.py | 1 + mne/epochs.py | 9 ++++- mne/fixes.py | 5 ++- mne/io/base.py | 8 ++++- mne/io/fiff/tests/test_raw_fiff.py | 28 +++++++++++++++ mne/tests/test_epochs.py | 50 ++++++++++++++++++++++---- mne/utils/check.py | 20 ++++++++++- pyproject.toml | 2 +- 12 files changed, 123 insertions(+), 35 deletions(-) create mode 100644 doc/changes/devel/12451.bugfix.rst create mode 100644 doc/changes/devel/12451.dependency.rst create mode 100644 mne/datasets/_infant/__init__.py diff --git a/doc/changes/devel/12451.bugfix.rst b/doc/changes/devel/12451.bugfix.rst new file mode 100644 index 00000000000..2aca44529f1 --- /dev/null +++ b/doc/changes/devel/12451.bugfix.rst @@ -0,0 +1 @@ +Fix errant redundant use of ``BIDSPath.split`` when writing split raw and epochs data, by `Eric Larson`_. diff --git a/doc/changes/devel/12451.dependency.rst b/doc/changes/devel/12451.dependency.rst new file mode 100644 index 00000000000..8227dd779ad --- /dev/null +++ b/doc/changes/devel/12451.dependency.rst @@ -0,0 +1 @@ +``pytest-harvest`` is no longer used as a test dependency, by `Eric Larson`_. diff --git a/mne/conftest.py b/mne/conftest.py index 17ff64b32f7..fd7d1946843 100644 --- a/mne/conftest.py +++ b/mne/conftest.py @@ -10,6 +10,7 @@ import shutil import sys import warnings +from collections import defaultdict from contextlib import contextmanager from pathlib import Path from textwrap import dedent @@ -900,11 +901,8 @@ def protect_config(): def _test_passed(request): - try: - outcome = request.node.harvest_rep_call - except Exception: - outcome = "passed" - return outcome == "passed" + report = request.node.stash[_phase_report_key] + return "call" in report and report["call"].outcome == "passed" @pytest.fixture() @@ -931,7 +929,6 @@ def brain_gc(request): ignore = set(id(o) for o in gc.get_objects()) yield close_func() - # no need to warn if the test itself failed, pytest-harvest helps us here if not _test_passed(request): return _assert_no_instances(Brain, "after") @@ -960,16 +957,12 @@ def pytest_sessionfinish(session, exitstatus): if n is None: return print("\n") - try: - import pytest_harvest - except ImportError: - print("Module-level timings require pytest-harvest") - return # get the number to print - res = pytest_harvest.get_session_synthesis_dct(session) - files = dict() - for key, val in res.items(): - parts = Path(key.split(":")[0]).parts + files = defaultdict(lambda: 0.0) + for item in session.items: + report = item.stash[_phase_report_key] + dur = sum(x.duration for x in report.values()) + parts = Path(item.nodeid.split(":")[0]).parts # split mne/tests/test_whatever.py into separate categories since these # are essentially submodule-level tests. Keeping just [:3] works, # except for mne/viz where we want level-4 granulatity @@ -978,7 +971,7 @@ def pytest_sessionfinish(session, exitstatus): if not parts[-1].endswith(".py"): parts = parts + ("",) file_key = "/".join(parts) - files[file_key] = files.get(file_key, 0) + val["pytest_duration_s"] + files[file_key] += dur files = sorted(list(files.items()), key=lambda x: x[1])[::-1] # print _files[:] = files[:n] @@ -999,7 +992,7 @@ def pytest_terminal_summary(terminalreporter, exitstatus, config): writer.line(f"{timing.ljust(15)}{name}") -def pytest_report_header(config, startdir): +def pytest_report_header(config, startdir=None): """Add information to the pytest run header.""" return f"MNE {mne.__version__} -- {str(Path(mne.__file__).parent)}" @@ -1122,7 +1115,6 @@ def run(nbexec=nbexec, code=code): return -@pytest.mark.filterwarnings("ignore:.*Extraction of measurement.*:") @pytest.fixture( params=( [nirsport2, nirsport2_snirf, testing._pytest_param()], @@ -1160,8 +1152,7 @@ def qt_windows_closed(request): if "allow_unclosed_pyside2" in marks and API_NAME.lower() == "pyside2": return # Don't check when the test fails - report = request.node.stash[_phase_report_key] - if ("call" not in report) or report["call"].failed: + if not _test_passed(request): return widgets = app.topLevelWidgets() n_after = len(widgets) diff --git a/mne/datasets/__init__.pyi b/mne/datasets/__init__.pyi index 22cb6acce7b..44cee84fe7f 100644 --- a/mne/datasets/__init__.pyi +++ b/mne/datasets/__init__.pyi @@ -66,7 +66,7 @@ from . import ( ) from ._fetch import fetch_dataset from ._fsaverage.base import fetch_fsaverage -from ._infant.base import fetch_infant_template +from ._infant import fetch_infant_template from ._phantom.base import fetch_phantom from .utils import ( _download_all_example_data, diff --git a/mne/datasets/_infant/__init__.py b/mne/datasets/_infant/__init__.py new file mode 100644 index 00000000000..7347d36fcd0 --- /dev/null +++ b/mne/datasets/_infant/__init__.py @@ -0,0 +1 @@ +from .base import fetch_infant_template diff --git a/mne/epochs.py b/mne/epochs.py index 7da05dbd045..7b87dee5179 100644 --- a/mne/epochs.py +++ b/mne/epochs.py @@ -2212,7 +2212,14 @@ def save( ) # check for file existence and expand `~` if present - fname = str(_check_fname(fname=fname, overwrite=overwrite)) + fname = str( + _check_fname( + fname=fname, + overwrite=overwrite, + check_bids_split=True, + name="fname", + ) + ) split_size_bytes = _get_split_size(split_size) diff --git a/mne/fixes.py b/mne/fixes.py index 4759366f386..55e56261866 100644 --- a/mne/fixes.py +++ b/mne/fixes.py @@ -31,9 +31,8 @@ ############################################################################### # distutils -# distutils has been deprecated since Python 3.10 and is scheduled for removal -# from the standard library with the release of Python 3.12. For version -# comparisons, we use setuptools's `parse_version` if available. +# distutils has been deprecated since Python 3.10 and was removed +# from the standard library with the release of Python 3.12. def _compare_version(version_a, operator, version_b): diff --git a/mne/io/base.py b/mne/io/base.py index 4fe7975e1cd..99a8e658fc4 100644 --- a/mne/io/base.py +++ b/mne/io/base.py @@ -1694,7 +1694,13 @@ def save( endings_err = (".fif", ".fif.gz") # convert to str, check for overwrite a few lines later - fname = _check_fname(fname, overwrite=True, verbose="error") + fname = _check_fname( + fname, + overwrite=True, + verbose="error", + check_bids_split=True, + name="fname", + ) check_fname(fname, "raw", endings, endings_err=endings_err) split_size = _get_split_size(split_size) diff --git a/mne/io/fiff/tests/test_raw_fiff.py b/mne/io/fiff/tests/test_raw_fiff.py index cb1626a4ef7..a28844eb5f5 100644 --- a/mne/io/fiff/tests/test_raw_fiff.py +++ b/mne/io/fiff/tests/test_raw_fiff.py @@ -675,6 +675,34 @@ def test_split_files(tmp_path, mod, monkeypatch): assert not fname_3.is_file() +def test_bids_split_files(tmp_path): + """Test that BIDS split files are written safely.""" + mne_bids = pytest.importorskip("mne_bids") + bids_path = mne_bids.BIDSPath( + root=tmp_path, + subject="01", + datatype="meg", + split="01", + suffix="raw", + extension=".fif", + check=False, + ) + (tmp_path / "sub-01" / "meg").mkdir(parents=True) + raw = read_raw_fif(test_fif_fname) + save_kwargs = dict( + buffer_size_sec=1.0, split_size="10MB", split_naming="bids", verbose=True + ) + with pytest.raises(ValueError, match="Passing a BIDSPath"): + raw.save(bids_path, **save_kwargs) + bids_path.split = None + want_paths = [Path(bids_path.copy().update(split=ii).fpath) for ii in range(1, 3)] + for want_path in want_paths: + assert not want_path.is_file() + raw.save(bids_path, **save_kwargs) + for want_path in want_paths: + assert want_path.is_file() + + def _err(*args, **kwargs): raise RuntimeError("Killed mid-write") diff --git a/mne/tests/test_epochs.py b/mne/tests/test_epochs.py index 015974e89cc..fdd91fd96a0 100644 --- a/mne/tests/test_epochs.py +++ b/mne/tests/test_epochs.py @@ -1666,43 +1666,79 @@ def test_split_saving_and_loading_back(tmp_path, epochs_to_split, preload): @pytest.mark.parametrize( - "split_naming, dst_fname, split_fname_fn", + "split_naming, dst_fname, split_fname_fn, check_bids", [ ( "neuromag", "test_epo.fif", lambda i: f"test_epo-{i}.fif" if i else "test_epo.fif", + False, ), ( "bids", - "test_epo.fif", - lambda i: f"test_split-{i + 1:02d}_epo.fif", + Path("sub-01") / "meg" / "sub-01_epo.fif", + lambda i: Path("sub-01") / "meg" / f"sub-01_split-{i + 1:02d}_epo.fif", + True, ), ( "bids", "a_b-epo.fif", # Merely stating the fact: lambda i: f"a_split-{i + 1:02d}_b-epo.fif", + False, ), ], ids=["neuromag", "bids", "mix"], ) def test_split_naming( - tmp_path, epochs_to_split, split_naming, dst_fname, split_fname_fn + tmp_path, epochs_to_split, split_naming, dst_fname, split_fname_fn, check_bids ): """Test naming of the split files.""" epochs, split_size, n_files = epochs_to_split dst_fpath = tmp_path / dst_fname save_kwargs = {"split_size": split_size, "split_naming": split_naming} # we don't test for reserved files as it's not implemented here + if dst_fpath.parent != tmp_path: + dst_fpath.parent.mkdir(parents=True) epochs.save(dst_fpath, verbose=True, **save_kwargs) # check that the filenames match the intended pattern - assert len(list(tmp_path.iterdir())) == n_files - for i in range(n_files): - assert (tmp_path / split_fname_fn(i)).is_file() + assert len(list(dst_fpath.parent.iterdir())) == n_files assert not (tmp_path / split_fname_fn(n_files)).is_file() + want_paths = [tmp_path / split_fname_fn(i) for i in range(n_files)] + for want_path in want_paths: + assert want_path.is_file() + + if not check_bids: + return + # gh-12451 + # If we load sub-01_split-01_epo.fif we should then we shouldn't + # write sub-01_split-01_split-01_epo.fif + mne_bids = pytest.importorskip("mne_bids") + # Let's try to prevent people from making a mistake + bids_path = mne_bids.BIDSPath( + root=tmp_path, + subject="01", + datatype="meg", + split="01", + suffix="epo", + extension=".fif", + check=False, + ) + assert bids_path.fpath.is_file(), bids_path.fpath + for want_path in want_paths: + want_path.unlink() + assert not bids_path.fpath.is_file() + with pytest.raises(ValueError, match="Passing a BIDSPath"): + epochs.save(bids_path, verbose=True, **save_kwargs) + bad_path = bids_path.fpath.parent / (bids_path.fpath.stem[:-3] + "split-01_epo.fif") + assert str(bad_path).count("_split-01") == 2 + assert not bad_path.is_file(), bad_path + bids_path.split = None + epochs.save(bids_path, verbose=True, **save_kwargs) + for want_path in want_paths: + assert want_path.is_file() @pytest.mark.parametrize( diff --git a/mne/utils/check.py b/mne/utils/check.py index 13eca1e0ba0..e73faf0a2e3 100644 --- a/mne/utils/check.py +++ b/mne/utils/check.py @@ -230,11 +230,29 @@ def _check_fname( name="File", need_dir=False, *, + check_bids_split=False, verbose=None, ): """Check for file existence, and return its absolute path.""" _validate_type(fname, "path-like", name) - fname = Path(fname).expanduser().absolute() + # special case for MNE-BIDS, check split + fname_path = Path(fname) + if check_bids_split: + try: + from mne_bids import BIDSPath + except Exception: + pass + else: + if isinstance(fname, BIDSPath) and fname.split is not None: + raise ValueError( + f"Passing a BIDSPath {name} with `{fname.split=}` is unsafe as it " + "can unexpectedly lead to invalid BIDS split naming. Explicitly " + f"set `{name}.split = None` to avoid ambiguity. If you want the " + f"old misleading split naming, you can pass `str({name})`." + ) + + fname = fname_path.expanduser().absolute() + del fname_path if fname.exists(): if not overwrite: diff --git a/pyproject.toml b/pyproject.toml index 39f2d2ee32b..d82518aa70e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -113,7 +113,6 @@ test = [ "pytest>=8.0.0rc2", "pytest-cov", "pytest-timeout", - "pytest-harvest", "pytest-qt", "ruff", "numpydoc", @@ -138,6 +137,7 @@ test_extra = [ "imageio-ffmpeg>=0.4.1", "snirf", "neo", + "mne-bids", ] # Dependencies for building the docuemntation From ae6e55e8eb41f080d6c209c129bb86ecdb2fc30d Mon Sep 17 00:00:00 2001 From: Sophie Herbst Date: Thu, 22 Feb 2024 13:22:49 +0100 Subject: [PATCH 205/405] Report image kwargs (#12454) 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 --- doc/changes/devel/12454.newfeature.rst | 1 + mne/report/report.py | 20 +++++++++++++++++--- mne/report/tests/test_report.py | 4 +++- 3 files changed, 21 insertions(+), 4 deletions(-) create mode 100644 doc/changes/devel/12454.newfeature.rst diff --git a/doc/changes/devel/12454.newfeature.rst b/doc/changes/devel/12454.newfeature.rst new file mode 100644 index 00000000000..5a4a9cc9cdb --- /dev/null +++ b/doc/changes/devel/12454.newfeature.rst @@ -0,0 +1 @@ +Completing PR 12453. Add option to pass ``image_kwargs`` per channel type to :class:`mne.Report.add_epochs`. \ No newline at end of file diff --git a/mne/report/report.py b/mne/report/report.py index 7e80047a32b..43c3d7c7ac4 100644 --- a/mne/report/report.py +++ b/mne/report/report.py @@ -1124,6 +1124,13 @@ def add_epochs( image_kwargs : dict | None Keyword arguments to pass to the "epochs image"-generating function (:meth:`mne.Epochs.plot_image`). + Keys are channel types, values are dicts containing kwargs to pass. + For example, to use the rejection limits per channel type you could pass:: + + image_kwargs=dict( + grad=dict(vmin=-reject['grad'], vmax=-reject['grad']), + mag=dict(vmin=-reject['mag'], vmax=reject['mag']), + ) .. versionadded:: 1.7 %(topomap_kwargs)s @@ -3888,15 +3895,16 @@ def _add_epochs( ch_types = _get_data_ch_types(epochs) epochs.load_data() - if image_kwargs is None: - image_kwargs = dict() + _validate_type(image_kwargs, (dict, None), "image_kwargs") + # ensure dict with shallow copy because we will modify it + image_kwargs = dict() if image_kwargs is None else image_kwargs.copy() for ch_type in ch_types: with use_log_level(_verbose_safe_false(level="error")): figs = ( epochs.copy() .pick(ch_type, verbose=False) - .plot_image(show=False, **image_kwargs) + .plot_image(show=False, **image_kwargs.pop(ch_type, dict())) ) assert len(figs) == 1 @@ -3920,6 +3928,12 @@ def _add_epochs( replace=replace, own_figure=True, ) + if image_kwargs: + raise ValueError( + f"Ensure the keys in image_kwargs map onto channel types plotted in " + f"epochs.plot_image() of {ch_types}, could not use: " + f"{list(image_kwargs)}" + ) # Drop log if epochs._bad_dropped: diff --git a/mne/report/tests/test_report.py b/mne/report/tests/test_report.py index 65d3ceb697a..7374868c559 100644 --- a/mne/report/tests/test_report.py +++ b/mne/report/tests/test_report.py @@ -936,8 +936,10 @@ def test_manual_report_2d(tmp_path, invisible_fig): tags=("epochs",), psd=False, projs=False, - image_kwargs=dict(colorbar=False), + image_kwargs=dict(mag=dict(colorbar=False)), ) + with pytest.raises(ValueError, match="map onto channel types"): + r.add_epochs(epochs=epochs_without_metadata, image_kwargs=dict(a=1), title="a") r.add_epochs( epochs=epochs_without_metadata, title="my epochs 2", psd=1, projs=False ) From 69d29637db82563cd56d8effc396124f243cf4fb Mon Sep 17 00:00:00 2001 From: Will Turner Date: Tue, 27 Feb 2024 04:53:47 +1000 Subject: [PATCH 206/405] Fix dead links to contribution guidelines (#12461) Co-authored-by: Will Turner --- .github/CONTRIBUTING.md | 2 +- .github/PULL_REQUEST_TEMPLATE.md | 2 +- doc/changes/devel/12461.other.rst | 1 + doc/changes/names.inc | 2 ++ 4 files changed, 5 insertions(+), 2 deletions(-) create mode 100644 doc/changes/devel/12461.other.rst diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 53e02d49867..b7ab58dc917 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -10,4 +10,4 @@ This project and everyone participating in it is governed by the [MNE-Python's C ## How to contribute -Before contributing make sure you are familiar with [our contributing guide](https://mne.tools/dev/install/contributing.html). +Before contributing make sure you are familiar with [our contributing guide](https://mne.tools/dev/development/contributing.html). diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index ea102484a7f..1ca19246c37 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,5 +1,5 @@ Thanks for contributing a pull request! Please make sure you have read the -[contribution guidelines](https://mne.tools/dev/install/contributing.html) +[contribution guidelines](https://mne.tools/dev/development/contributing.html) before submitting. Please be aware that we are a loose team of volunteers so patience is diff --git a/doc/changes/devel/12461.other.rst b/doc/changes/devel/12461.other.rst new file mode 100644 index 00000000000..b6fcea48fc7 --- /dev/null +++ b/doc/changes/devel/12461.other.rst @@ -0,0 +1 @@ +Fix dead links in ``README.rst`` documentation by :newcontrib:`Will Turner`. \ No newline at end of file diff --git a/doc/changes/names.inc b/doc/changes/names.inc index b3c74318975..ab249e4641a 100644 --- a/doc/changes/names.inc +++ b/doc/changes/names.inc @@ -594,6 +594,8 @@ .. _Xiaokai Xia: https://github.com/dddd1007 +.. _Will Turner: https://bootstrapbill.github.io + .. _Yaroslav Halchenko: http://haxbylab.dartmouth.edu/ppl/yarik.html .. _Yiping Zuo: https://github.com/frostime From f2fa901ca6f87eaadca6acd45f4c460f1c92f8fe Mon Sep 17 00:00:00 2001 From: Kristijan Armeni Date: Wed, 28 Feb 2024 10:13:07 -0500 Subject: [PATCH 207/405] store online filter freqs and meas_date to from .ncs headers in `RawNeuralynx.info` (#12463) Co-authored-by: Eric Larson --- doc/changes/devel/12463.newfeature.rst | 1 + mne/decoding/base.py | 7 ++ mne/decoding/search_light.py | 7 ++ mne/io/neuralynx/neuralynx.py | 82 ++++++++++++++++++++++++ mne/io/neuralynx/tests/test_neuralynx.py | 14 ++++ 5 files changed, 111 insertions(+) create mode 100644 doc/changes/devel/12463.newfeature.rst diff --git a/doc/changes/devel/12463.newfeature.rst b/doc/changes/devel/12463.newfeature.rst new file mode 100644 index 00000000000..d041b0c912f --- /dev/null +++ b/doc/changes/devel/12463.newfeature.rst @@ -0,0 +1 @@ +Include date of acquisition and filter parameters in ``raw.info`` for :func:`mne.io.read_raw_neuralynx` by `Kristijan Armeni`_. \ No newline at end of file diff --git a/mne/decoding/base.py b/mne/decoding/base.py index e44fcd13f29..8e36ee412a8 100644 --- a/mne/decoding/base.py +++ b/mne/decoding/base.py @@ -11,6 +11,7 @@ import numbers import numpy as np +from scipy.sparse import issparse from ..fixes import BaseEstimator, _check_fit_params, _get_check_scoring from ..parallel import parallel_func @@ -106,6 +107,12 @@ def fit(self, X, y, **fit_params): self : instance of LinearModel Returns the modified instance. """ + # Once we require sklearn 1.1+ we should do: + # from sklearn.utils import check_array + # X = check_array(X, input_name="X") + # y = check_array(y, dtype=None, ensure_2d=False, input_name="y") + if issparse(X): + raise TypeError("X should be a dense array, got sparse instead.") X, y = np.asarray(X), np.asarray(y) if X.ndim != 2: raise ValueError( diff --git a/mne/decoding/search_light.py b/mne/decoding/search_light.py index 369efd7bba3..c8d56b88d6e 100644 --- a/mne/decoding/search_light.py +++ b/mne/decoding/search_light.py @@ -5,6 +5,7 @@ import logging import numpy as np +from scipy.sparse import issparse from ..fixes import _get_check_scoring from ..parallel import parallel_func @@ -254,6 +255,12 @@ def decision_function(self, X): def _check_Xy(self, X, y=None): """Aux. function to check input data.""" + # Once we require sklearn 1.1+ we should do something like: + # from sklearn.utils import check_array + # X = check_array(X, ensure_2d=False, input_name="X") + # y = check_array(y, dtype=None, ensure_2d=False, input_name="y") + if issparse(X): + raise TypeError("X should be a dense array, got sparse instead.") X = np.asarray(X) if y is not None: y = np.asarray(y) diff --git a/mne/io/neuralynx/neuralynx.py b/mne/io/neuralynx/neuralynx.py index 46bca5be27d..4eeb7cf8d08 100644 --- a/mne/io/neuralynx/neuralynx.py +++ b/mne/io/neuralynx/neuralynx.py @@ -1,5 +1,6 @@ # License: BSD-3-Clause # Copyright the MNE-Python contributors. +import datetime import glob import os @@ -38,6 +39,32 @@ def read_raw_neuralynx( See Also -------- mne.io.Raw : Documentation of attributes and methods of RawNeuralynx. + + Notes + ----- + Neuralynx files are read from disk using the `Neo package + `__. + Currently, only reading of the ``.ncs files`` is supported. + + ``raw.info["meas_date"]`` is read from the ``recording_opened`` property + of the first ``.ncs`` file (i.e. channel) in the dataset (a warning is issued + if files have different dates of acquisition). + + Channel-specific high and lowpass frequencies of online filters are determined + based on the ``DspLowCutFrequency`` and ``DspHighCutFrequency`` header fields, + respectively. If no filters were used for a channel, the default lowpass is set + to the Nyquist frequency and the default highpass is set to 0. + If channels have different high/low cutoffs, ``raw.info["highpass"]`` and + ``raw.info["lowpass"]`` are then set to the maximum highpass and minimumlowpass + values across channels, respectively. + + Other header variables can be inspected using Neo directly. For example:: + + from neo.io import NeuralynxIO # doctest: +SKIP + fname = 'path/to/your/data' # doctest: +SKIP + nlx_reader = NeuralynxIO(dirname=fname) # doctest: +SKIP + print(nlx_reader.header) # doctest: +SKIP + print(nlx_reader.file_headers.items()) # doctest: +SKIP """ return RawNeuralynx( fname, @@ -101,6 +128,61 @@ def __init__( sfreq=nlx_reader.get_signal_sampling_rate(), ) + ncs_fnames = nlx_reader.ncs_filenames.values() + ncs_hdrs = [ + hdr + for hdr_key, hdr in nlx_reader.file_headers.items() + if hdr_key in ncs_fnames + ] + + # if all files have the same recording_opened date, write it to info + meas_dates = np.array([hdr["recording_opened"] for hdr in ncs_hdrs]) + # to be sure, only write if all dates are the same + meas_diff = [] + for md in meas_dates: + meas_diff.append((md - meas_dates[0]).total_seconds()) + + # tolerate a +/-1 second meas_date difference (arbitrary threshold) + # else issue a warning + warn_meas = (np.abs(meas_diff) > 1.0).any() + if warn_meas: + logger.warning( + "Not all .ncs files have the same recording_opened date. " + + "Writing meas_date based on the first .ncs file." + ) + + # Neuarlynx allows channel specific low/highpass filters + # if not enabled, assume default lowpass = nyquist, highpass = 0 + default_lowpass = info["sfreq"] / 2 # nyquist + default_highpass = 0 + + has_hp = [hdr["DSPLowCutFilterEnabled"] for hdr in ncs_hdrs] + has_lp = [hdr["DSPHighCutFilterEnabled"] for hdr in ncs_hdrs] + if not all(has_hp) or not all(has_lp): + logger.warning( + "Not all .ncs files have the same high/lowpass filter settings. " + + "Assuming default highpass = 0, lowpass = nyquist." + ) + + highpass_freqs = [ + float(hdr["DspLowCutFrequency"]) + if hdr["DSPLowCutFilterEnabled"] + else default_highpass + for hdr in ncs_hdrs + ] + + lowpass_freqs = [ + float(hdr["DspHighCutFrequency"]) + if hdr["DSPHighCutFilterEnabled"] + else default_lowpass + for hdr in ncs_hdrs + ] + + with info._unlock(): + info["meas_date"] = meas_dates[0].astimezone(datetime.timezone.utc) + info["highpass"] = np.max(highpass_freqs) + info["lowpass"] = np.min(lowpass_freqs) + # Neo reads only valid contiguous .ncs samples grouped as segments n_segments = nlx_reader.header["nb_segment"][0] block_id = 0 # assumes there's only one block of recording diff --git a/mne/io/neuralynx/tests/test_neuralynx.py b/mne/io/neuralynx/tests/test_neuralynx.py index 14e030df23c..ceebdd3c975 100644 --- a/mne/io/neuralynx/tests/test_neuralynx.py +++ b/mne/io/neuralynx/tests/test_neuralynx.py @@ -2,6 +2,7 @@ # Copyright the MNE-Python contributors. import os from ast import literal_eval +from datetime import datetime, timezone import numpy as np import pytest @@ -103,7 +104,12 @@ def _read_nlx_mat_chan_keep_gaps(matfile: str) -> np.ndarray: return x +# set known values for the Neuralynx data for testing expected_chan_names = ["LAHC1", "LAHC2", "LAHC3", "xAIR1", "xEKG1"] +expected_hp_freq = 0.1 +expected_lp_freq = 500.0 +expected_sfreq = 2000.0 +expected_meas_date = datetime.strptime("2023/11/02 13:39:27", "%Y/%m/%d %H:%M:%S") @requires_testing_data @@ -125,6 +131,14 @@ def test_neuralynx(): exclude_fname_patterns=fname_patterns, ) + # test that we picked the right info from headers + assert raw.info["highpass"] == expected_hp_freq, "highpass freq not set correctly" + assert raw.info["lowpass"] == expected_lp_freq, "lowpass freq not set correctly" + assert raw.info["sfreq"] == expected_sfreq, "sampling freq not set correctly" + + meas_date_utc = expected_meas_date.astimezone(timezone.utc) + assert raw.info["meas_date"] == meas_date_utc, "meas_date not set correctly" + # test that channel selection worked assert ( raw.ch_names == expected_chan_names From 985c1959d4b0e2229d9288e78d0a022926042b76 Mon Sep 17 00:00:00 2001 From: Daniel McCloy Date: Thu, 29 Feb 2024 14:45:55 -0600 Subject: [PATCH 208/405] fix leap-year-induced test failure (#12469) --- mne/io/edf/edf.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mne/io/edf/edf.py b/mne/io/edf/edf.py index 888db95c9a6..91a7632701d 100644 --- a/mne/io/edf/edf.py +++ b/mne/io/edf/edf.py @@ -1269,7 +1269,9 @@ def _read_gdf_header(fname, exclude, include=None): if patient["birthday"] != datetime(1, 1, 1, 0, 0, tzinfo=timezone.utc): today = datetime.now(tz=timezone.utc) patient["age"] = today.year - patient["birthday"].year - today = today.replace(year=patient["birthday"].year) + # fudge the day by -1 if today happens to be a leap day + day = 28 if today.month == 2 and today.day == 29 else today.day + today = today.replace(year=patient["birthday"].year, day=day) if today < patient["birthday"]: patient["age"] -= 1 else: From 62a0c40fa01ec65910313109cd414e22b3b0d135 Mon Sep 17 00:00:00 2001 From: Marijn van Vliet Date: Fri, 1 Mar 2024 17:34:25 +0200 Subject: [PATCH 209/405] Fix default color of mne.viz.Brain.add_text (#12470) --- doc/changes/devel/12470.bugfix.rst | 1 + mne/viz/_brain/_brain.py | 2 ++ 2 files changed, 3 insertions(+) create mode 100644 doc/changes/devel/12470.bugfix.rst diff --git a/doc/changes/devel/12470.bugfix.rst b/doc/changes/devel/12470.bugfix.rst new file mode 100644 index 00000000000..d8d72843304 --- /dev/null +++ b/doc/changes/devel/12470.bugfix.rst @@ -0,0 +1 @@ +- Fix the default color of :meth:`mne.viz.Brain.add_text` to properly contrast with the figure background color, by `Marijn van Vliet`_. diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 09e27a96408..432621eadc4 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -2917,6 +2917,8 @@ def add_text( name = text if name is None else name if "text" in self._actors and name in self._actors["text"]: raise ValueError(f"Text with the name {name} already exists") + if color is None: + color = self._fg_color for ri, ci, _ in self._iter_views("vol"): if (row is None or row == ri) and (col is None or col == ci): actor = self._renderer.text2d( From 9ae99424325ef5f100c0774f469a948884fe8b07 Mon Sep 17 00:00:00 2001 From: Clemens Brunner Date: Fri, 1 Mar 2024 17:35:05 +0100 Subject: [PATCH 210/405] Include helper functions from pybv (#12450) --- doc/changes/devel/12450.other.rst | 1 + mne/export/_brainvision.py | 143 +++++++++++++++++++++++++++++- 2 files changed, 142 insertions(+), 2 deletions(-) create mode 100644 doc/changes/devel/12450.other.rst diff --git a/doc/changes/devel/12450.other.rst b/doc/changes/devel/12450.other.rst new file mode 100644 index 00000000000..48265f87416 --- /dev/null +++ b/doc/changes/devel/12450.other.rst @@ -0,0 +1 @@ +Move private data preparation functions for BrainVision export from ``pybv`` to ``mne``, by `Clemens Brunner`_. \ No newline at end of file diff --git a/mne/export/_brainvision.py b/mne/export/_brainvision.py index 0da7647ecb7..d705d8cef9d 100644 --- a/mne/export/_brainvision.py +++ b/mne/export/_brainvision.py @@ -4,11 +4,150 @@ # Copyright the MNE-Python contributors. import os +from pathlib import Path -from ..utils import _check_pybv_installed +import numpy as np + +from mne.channels.channels import _unit2human +from mne.io.constants import FIFF +from mne.utils import _check_pybv_installed, warn _check_pybv_installed() -from pybv._export import _export_mne_raw # noqa: E402 +from pybv import write_brainvision # noqa: E402 + + +def _export_mne_raw(*, raw, fname, events=None, overwrite=False): + """Export raw data from MNE-Python. + + Parameters + ---------- + raw : mne.io.Raw + The raw data to export. + fname : str | pathlib.Path + The name of the file where raw data will be exported to. Must end with + ``".vhdr"``, and accompanying *.vmrk* and *.eeg* files will be written inside + the same directory. + events : np.ndarray | None + Events to be written to the marker file (*.vmrk*). If array, must be in + `MNE-Python format `_. If + ``None`` (default), events will be written based on ``raw.annotations``. + overwrite : bool + Whether or not to overwrite existing data. Defaults to ``False``. + + """ + # prepare file location + if not str(fname).endswith(".vhdr"): + raise ValueError("`fname` must have the '.vhdr' extension for BrainVision.") + fname = Path(fname) + folder_out = fname.parents[0] + fname_base = fname.stem + + # prepare data from raw + data = raw.get_data() # gets data starting from raw.first_samp + sfreq = raw.info["sfreq"] # in Hz + meas_date = raw.info["meas_date"] # datetime.datetime + ch_names = raw.ch_names + + # write voltage units as micro-volts and all other units without scaling + # write units that we don't know as n/a + unit = [] + for ch in raw.info["chs"]: + if ch["unit"] == FIFF.FIFF_UNIT_V: + unit.append("µV") + elif ch["unit"] == FIFF.FIFF_UNIT_CEL: + unit.append("°C") + else: + unit.append(_unit2human.get(ch["unit"], "n/a")) + unit = [u if u != "NA" else "n/a" for u in unit] + + # enforce conversion to float32 format + # XXX: Could add a feature that checks data and optimizes `unit`, `resolution`, and + # `format` so that raw.orig_format could be retained if reasonable. + if raw.orig_format != "single": + warn( + f"Encountered data in '{raw.orig_format}' format. Converting to float32.", + RuntimeWarning, + ) + + fmt = "binary_float32" + resolution = 0.1 + + # handle events + # if we got an ndarray, this is in MNE-Python format + msg = "`events` must be None or array in MNE-Python format." + if events is not None: + # subtract raw.first_samp because brainvision marks events starting from the + # first available data point and ignores the raw.first_samp + assert isinstance(events, np.ndarray), msg + assert events.ndim == 2, msg + assert events.shape[-1] == 3, msg + events[:, 0] -= raw.first_samp + events = events[:, [0, 2]] # reorder for pybv required order + else: # else, prepare pybv style events from raw.annotations + events = _mne_annots2pybv_events(raw) + + # no information about reference channels in mne currently + ref_ch_names = None + + # write to BrainVision + write_brainvision( + data=data, + sfreq=sfreq, + ch_names=ch_names, + ref_ch_names=ref_ch_names, + fname_base=fname_base, + folder_out=folder_out, + overwrite=overwrite, + events=events, + resolution=resolution, + unit=unit, + fmt=fmt, + meas_date=meas_date, + ) + + +def _mne_annots2pybv_events(raw): + """Convert mne Annotations to pybv events.""" + events = [] + for annot in raw.annotations: + # handle onset and duration: seconds to sample, relative to + # raw.first_samp / raw.first_time + onset = annot["onset"] - raw.first_time + onset = raw.time_as_index(onset).astype(int)[0] + duration = int(annot["duration"] * raw.info["sfreq"]) + + # triage type and description + # defaults to type="Comment" and the full description + etype = "Comment" + description = annot["description"] + for start in ["Stimulus/S", "Response/R", "Comment/"]: + if description.startswith(start): + etype = start.split("/")[0] + description = description.replace(start, "") + break + + if etype in ["Stimulus", "Response"] and description.strip().isdigit(): + description = int(description.strip()) + else: + # if cannot convert to int, we must use this as "Comment" + etype = "Comment" + + event_dict = dict( + onset=onset, # in samples + duration=duration, # in samples + description=description, + type=etype, + ) + + if "ch_names" in annot: + # handle channels + channels = list(annot["ch_names"]) + event_dict["channels"] = channels + + # add a "pybv" event + events += [event_dict] + + return events def _export_raw(fname, raw, overwrite): From ff1cfdd8e3ac3c05bfd5987a3e821f13cf7928f9 Mon Sep 17 00:00:00 2001 From: Velu Prabhakar Kumaravel <48288235+vpKumaravel@users.noreply.github.com> Date: Fri, 1 Mar 2024 17:55:06 +0100 Subject: [PATCH 211/405] adding a bad channel detection method using LOF algorithm (#11234) Co-authored-by: Velu Prabhakar Kumaravel Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Eric Larson Co-authored-by: Daniel McCloy --- doc/api/preprocessing.rst | 1 + doc/changes/devel/11234.newfeature.rst | 1 + doc/changes/names.inc | 2 + doc/references.bib | 31 ++++++++ mne/preprocessing/__init__.pyi | 2 + mne/preprocessing/_lof.py | 100 +++++++++++++++++++++++++ mne/preprocessing/tests/test_lof.py | 39 ++++++++++ 7 files changed, 176 insertions(+) create mode 100644 doc/changes/devel/11234.newfeature.rst create mode 100644 mne/preprocessing/_lof.py create mode 100644 mne/preprocessing/tests/test_lof.py diff --git a/doc/api/preprocessing.rst b/doc/api/preprocessing.rst index f5271a1edee..1e0e9e56079 100644 --- a/doc/api/preprocessing.rst +++ b/doc/api/preprocessing.rst @@ -93,6 +93,7 @@ Projections: cortical_signal_suppression create_ecg_epochs create_eog_epochs + find_bad_channels_lof find_bad_channels_maxwell find_ecg_events find_eog_events diff --git a/doc/changes/devel/11234.newfeature.rst b/doc/changes/devel/11234.newfeature.rst new file mode 100644 index 00000000000..46cc408a3d9 --- /dev/null +++ b/doc/changes/devel/11234.newfeature.rst @@ -0,0 +1 @@ +Detecting Bad EEG/MEG channels using the local outlier factor (LOF) algorithm in :func:`mne.preprocessing.find_bad_channels_lof`, by :newcontrib:`Velu Prabhakar Kumaravel`. \ No newline at end of file diff --git a/doc/changes/names.inc b/doc/changes/names.inc index ab249e4641a..51e7bfafbb7 100644 --- a/doc/changes/names.inc +++ b/doc/changes/names.inc @@ -588,6 +588,8 @@ .. _Valerii Chirkov: https://github.com/vagechirkov +.. _Velu Prabhakar Kumaravel: https://github.com/vpKumaravel + .. _Victor Ferat: https://github.com/vferat .. _Victoria Peterson: https://github.com/vpeterson diff --git a/doc/references.bib b/doc/references.bib index 9263379209a..7a992b2c1fa 100644 --- a/doc/references.bib +++ b/doc/references.bib @@ -2450,6 +2450,37 @@ @article{TierneyEtAl2022 author = {Tierney, Tim M. and Mellor, Stephanie nd O'Neill, George C. and Timms, Ryan C. and Barnes, Gareth R.}, } +@article{KumaravelEtAl2022, + doi = {10.3390/s22197314}, + url = {https://doi.org/10.3390/s22197314}, + year = {2022}, + month = sep, + publisher = {{MDPI} {AG}}, + volume = {22}, + number = {19}, + pages = {7314}, + author = {Velu Prabhakar Kumaravel and Marco Buiatti and Eugenio Parise and Elisabetta Farella}, + title = {Adaptable and Robust {EEG} Bad Channel Detection Using Local Outlier Factor ({LOF})}, + journal = {Sensors} +} + +@article{BreunigEtAl2000, + author = {Breunig, Markus M. and Kriegel, Hans-Peter and Ng, Raymond T. and Sander, J\"{o}rg}, + title = {LOF: Identifying Density-Based Local Outliers}, + year = {2000}, + issue_date = {June 2000}, + publisher = {Association for Computing Machinery}, + address = {New York, NY, USA}, + volume = {29}, + number = {2}, + url = {https://doi.org/10.1145/335191.335388}, + doi = {10.1145/335191.335388}, + journal = {SIGMOD Rec.}, + month = {may}, + pages = {93–104}, + numpages = {12}, + keywords = {outlier detection, database mining} +} @article{OyamaEtAl2015, title = {Dry phantom for magnetoencephalography —{Configuration}, calibration, and contribution}, diff --git a/mne/preprocessing/__init__.pyi b/mne/preprocessing/__init__.pyi index 0ea66345687..54f1c825c13 100644 --- a/mne/preprocessing/__init__.pyi +++ b/mne/preprocessing/__init__.pyi @@ -21,6 +21,7 @@ __all__ = [ "create_eog_epochs", "equalize_bads", "eyetracking", + "find_bad_channels_lof", "find_bad_channels_maxwell", "find_ecg_events", "find_eog_events", @@ -54,6 +55,7 @@ from ._fine_cal import ( read_fine_calibration, write_fine_calibration, ) +from ._lof import find_bad_channels_lof from ._peak_finder import peak_finder from ._regress import EOGRegression, read_eog_regression, regress_artifact from .artifact_detection import ( diff --git a/mne/preprocessing/_lof.py b/mne/preprocessing/_lof.py new file mode 100644 index 00000000000..1af9c5b16e9 --- /dev/null +++ b/mne/preprocessing/_lof.py @@ -0,0 +1,100 @@ +"""Bad channel detection using Local Outlier Factor (LOF).""" + +# Authors: Velu Prabhakar Kumaravel +# +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. + + +import numpy as np + +from .._fiff.pick import _picks_to_idx +from ..io.base import BaseRaw +from ..utils import _soft_import, _validate_type, logger, verbose + + +@verbose +def find_bad_channels_lof( + raw, + n_neighbors=20, + *, + picks=None, + metric="euclidean", + threshold=1.5, + return_scores=False, + verbose=None, +): + """Find bad channels using Local Outlier Factor (LOF) algorithm. + + Parameters + ---------- + raw : instance of Raw + Raw data to process. + n_neighbors : int + Number of neighbors defining the local neighborhood (default is 20). + Smaller values will lead to higher LOF scores. + %(picks_good_data)s + metric : str + Metric to use for distance computation. Default is “euclidean”, + see :func:`sklearn.metrics.pairwise.distance_metrics` for details. + threshold : float + Threshold to define outliers. Theoretical threshold ranges anywhere + between 1.0 and any positive integer. Default: 1.5 + It is recommended to consider this as an hyperparameter to optimize. + return_scores : bool + If ``True``, return a dictionary with LOF scores for each + evaluated channel. Default is ``False``. + %(verbose)s + + Returns + ------- + noisy_chs : list + List of bad M/EEG channels that were automatically detected. + scores : ndarray, shape (n_picks,) + Only returned when ``return_scores`` is ``True``. It contains the + LOF outlier score for each channel in ``picks``. + + See Also + -------- + maxwell_filter + annotate_amplitude + + Notes + ----- + See :footcite:`KumaravelEtAl2022` and :footcite:`BreunigEtAl2000` for background on + choosing ``threshold``. + + .. versionadded:: 1.7 + + References + ---------- + .. footbibliography:: + """ # noqa: E501 + _soft_import("sklearn", "using LOF detection", strict=True) + from sklearn.neighbors import LocalOutlierFactor + + _validate_type(raw, BaseRaw, "raw") + # Get the channel types + channel_types = raw.get_channel_types() + picks = _picks_to_idx(raw.info, picks=picks, none="data", exclude="bads") + picked_ch_types = set(channel_types[p] for p in picks) + + # Check if there are different channel types + if len(picked_ch_types) != 1: + raise ValueError( + f"Need exactly one channel type in picks, got {sorted(picked_ch_types)}" + ) + ch_names = [raw.ch_names[pick] for pick in picks] + data = raw.get_data(picks=picks) + clf = LocalOutlierFactor(n_neighbors=n_neighbors, metric=metric) + clf.fit_predict(data) + scores_lof = clf.negative_outlier_factor_ + bad_channel_indices = [ + i for i, v in enumerate(np.abs(scores_lof)) if v >= threshold + ] + bads = [ch_names[idx] for idx in bad_channel_indices] + logger.info(f"LOF: Detected bad channel(s): {bads}") + if return_scores: + return bads, scores_lof + else: + return bads diff --git a/mne/preprocessing/tests/test_lof.py b/mne/preprocessing/tests/test_lof.py new file mode 100644 index 00000000000..858fa0e4432 --- /dev/null +++ b/mne/preprocessing/tests/test_lof.py @@ -0,0 +1,39 @@ +# Authors: Velu Prabhakar Kumaravel +# +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. + +from pathlib import Path + +import pytest + +from mne.io import read_raw_fif +from mne.preprocessing import find_bad_channels_lof + +base_dir = Path(__file__).parent.parent.parent / "io" / "tests" / "data" +raw_fname = base_dir / "test_raw.fif" + + +@pytest.mark.parametrize( + "n_neighbors, ch_type, n_ch, n_bad", + [ + (8, "eeg", 60, 8), + (10, "grad", 204, 2), + (20, "mag", 102, 0), + (30, "grad", 204, 2), + ], +) +def test_lof(n_neighbors, ch_type, n_ch, n_bad): + """Test LOF detection.""" + pytest.importorskip("sklearn") + raw = read_raw_fif(raw_fname).load_data() + assert raw.info["bads"] == [] + bads, scores = find_bad_channels_lof( + raw, n_neighbors, picks=ch_type, return_scores=True + ) + bads_2 = find_bad_channels_lof(raw, n_neighbors, picks=ch_type) + assert len(scores) == n_ch + assert len(bads) == n_bad + assert bads == bads_2 + with pytest.raises(ValueError, match="channel type"): + find_bad_channels_lof(raw) From 668b508a7342828cf30e090a02f2e1a6e7402f35 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Fri, 1 Mar 2024 14:58:54 -0500 Subject: [PATCH 212/405] MAINT: Fix links (#12471) --- doc/changes/names.inc | 6 +++--- doc/conf.py | 2 ++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/doc/changes/names.inc b/doc/changes/names.inc index 51e7bfafbb7..d3dfd61b916 100644 --- a/doc/changes/names.inc +++ b/doc/changes/names.inc @@ -20,7 +20,7 @@ .. _Alex Gramfort: https://alexandre.gramfort.net -.. _Alex Kiefer: https://home.alex101.dev +.. _Alex Kiefer: https://home.alexk101.dev .. _Alex Rockhill: https://github.com/alexrockhill/ @@ -208,7 +208,7 @@ .. _Henrich Kolkhorst: https://github.com/hekolk -.. _Hongjiang Ye: https://github.com/rubyyhj +.. _Hongjiang Ye: https://github.com/hongjiang-ye .. _Hubert Banville: https://github.com/hubertjb @@ -418,7 +418,7 @@ .. _Okba Bekhelifi: https://github.com/okbalefthanded -.. _Olaf Hauk: https://www.neuroscience.cam.ac.uk/directory/profile.php?olafhauk +.. _Olaf Hauk: https://neuroscience.cam.ac.uk/member/olafhauk .. _Oleh Kozynets: https://github.com/OlehKSS diff --git a/doc/conf.py b/doc/conf.py index 03d3961151a..5d43b594b3a 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -748,6 +748,8 @@ def append_attr_meth_examples(app, what, name, obj, options, lines): # Too slow "https://speakerdeck.com/dengemann/", "https://www.dtu.dk/english/service/phonebook/person", + # SSL problems sometimes + "http://ilabs.washington.edu", ] linkcheck_anchors = False # saves a bit of time linkcheck_timeout = 15 # some can be quite slow From d17d885a36935a40b4d1b62796510cf5015b1d2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20H=C3=B6chenberger?= Date: Sat, 2 Mar 2024 00:57:11 +0100 Subject: [PATCH 213/405] MRG: Allow `tmin` and `tmax` in `mne.epochs.make_metadata()` to accept strings (#12462) Co-authored-by: Sophie Herbst --- doc/changes/devel/12462.newfeature.rst | 1 + examples/preprocessing/epochs_metadata.py | 171 +++++++++++++++++++ mne/epochs.py | 120 ++++++++++--- mne/tests/test_epochs.py | 30 +++- tutorials/epochs/40_autogenerate_metadata.py | 6 +- 5 files changed, 297 insertions(+), 31 deletions(-) create mode 100644 doc/changes/devel/12462.newfeature.rst create mode 100644 examples/preprocessing/epochs_metadata.py diff --git a/doc/changes/devel/12462.newfeature.rst b/doc/changes/devel/12462.newfeature.rst new file mode 100644 index 00000000000..4624579ba26 --- /dev/null +++ b/doc/changes/devel/12462.newfeature.rst @@ -0,0 +1 @@ +:func:`mne.epochs.make_metadata` now accepts strings as ``tmin`` and ``tmax`` parameter values, simplifying metadata creation based on time-varying events such as responses to a stimulus, by `Richard Höchenberger`_. diff --git a/examples/preprocessing/epochs_metadata.py b/examples/preprocessing/epochs_metadata.py new file mode 100644 index 00000000000..d1ea9a85996 --- /dev/null +++ b/examples/preprocessing/epochs_metadata.py @@ -0,0 +1,171 @@ +""" +.. _epochs-metadata: + +=============================================================== +Automated epochs metadata generation with variable time windows +=============================================================== + +When working with :class:`~mne.Epochs`, :ref:`metadata ` can be +invaluable. There is an extensive tutorial on +:ref:`how it can be generated automatically `. +In the brief examples below, we will demonstrate different ways to bound the time +windows used to generate the metadata. + +""" +# Authors: Richard Höchenberger +# +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. + +# %% +# We will use data from an EEG recording during an Eriksen flanker task. For the +# purpose of demonstration, we'll only load the first 60 seconds of data. + +import mne + +data_dir = mne.datasets.erp_core.data_path() +infile = data_dir / "ERP-CORE_Subject-001_Task-Flankers_eeg.fif" + +raw = mne.io.read_raw(infile, preload=True) +raw.crop(tmax=60).filter(l_freq=0.1, h_freq=40) + +# %% +# Visualizing the events +# ^^^^^^^^^^^^^^^^^^^^^^ +# +# All experimental events are stored in the :class:`~mne.io.Raw` instance as +# :class:`~mne.Annotations`. We first need to convert these to events and the +# corresponding mapping from event codes to event names (``event_id``). We then +# visualize the events. +all_events, all_event_id = mne.events_from_annotations(raw) +mne.viz.plot_events(events=all_events, event_id=all_event_id, sfreq=raw.info["sfreq"]) + + +# %% +# As you can see, there are four types of ``stimulus`` and two types of ``response`` +# events. +# +# Declaring "row events" +# ^^^^^^^^^^^^^^^^^^^^^^ +# +# For the sake of this example, we will assume that during analysis our epochs will be +# time-locked to the stimulus onset events. Hence, we would like to create metadata with +# one row per ``stimulus``. We can achieve this by specifying all stimulus event names +# as ``row_events``. + +row_events = [ + "stimulus/compatible/target_left", + "stimulus/compatible/target_right", + "stimulus/incompatible/target_left", + "stimulus/incompatible/target_right", +] + +# %% +# Specifying metadata time windows +# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +# +# Now, we will explore different ways of specifying the time windows around the +# ``row_events`` when generating metadata. Any events falling within the same time +# window will be added to the same row in the metadata table. +# +# Fixed time window +# ~~~~~~~~~~~~~~~~~ +# +# A simple way to specify the time window extent is by specifying the time in seconds +# relative to the row event. In the following example, the time window spans from the +# row event (time point zero) up until three seconds later. + +metadata_tmin = 0.0 +metadata_tmax = 3.0 + +metadata, events, event_id = mne.epochs.make_metadata( + events=all_events, + event_id=all_event_id, + tmin=metadata_tmin, + tmax=metadata_tmax, + sfreq=raw.info["sfreq"], + row_events=row_events, +) + +metadata + +# %% +# This looks good at the first glance. However, for example in the 2nd and 3rd row, we +# have two responses listed (left and right). This is because the 3-second time window +# is obviously a bit too wide and captures more than one trial. While we could make it +# narrower, this could lead to a loss of events – if the window might become **too** +# narrow. Ultimately, this problem arises because the response time varies from trial +# to trial, so it's difficult for us to set a fixed upper bound for the time window. +# +# Fixed time window with ``keep_first`` +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# +# One workaround is using the ``keep_first`` parameter, which will create a new column +# containing the first event of the specified type. + +metadata_tmin = 0.0 +metadata_tmax = 3.0 +keep_first = "response" # <-- new + +metadata, events, event_id = mne.epochs.make_metadata( + events=all_events, + event_id=all_event_id, + tmin=metadata_tmin, + tmax=metadata_tmax, + sfreq=raw.info["sfreq"], + row_events=row_events, + keep_first=keep_first, # <-- new +) + +metadata + +# %% +# As you can see, a new column ``response`` was created with the time of the first +# response event falling inside the time window. The ``first_response`` column specifies +# **which** response occurred first (left or right). +# +# Variable time window +# ~~~~~~~~~~~~~~~~~~~~ +# +# Another way to address the challenge of variable time windows **without** the need to +# create new columns is by specifying ``tmin`` and ``tmax`` as event names. In this +# example, we use ``tmin=row_events``, because we want the time window to start +# with the time-locked event. ``tmax``, on the other hand, are the response events: +# The first response event following ``tmin`` will be used to determine the duration of +# the time window. + +metadata_tmin = row_events +metadata_tmax = ["response/left", "response/right"] + +metadata, events, event_id = mne.epochs.make_metadata( + events=all_events, + event_id=all_event_id, + tmin=metadata_tmin, + tmax=metadata_tmax, + sfreq=raw.info["sfreq"], + row_events=row_events, +) + +metadata + +# %% +# Variable time window (simplified) +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# +# We can slightly simplify the above code: Since ``tmin`` shall be set to the +# ``row_events``, we can paass ``tmin=None``, which is a more convenient way to express +# ``tmin=row_events``. The resulting metadata looks the same as in the previous example. + +metadata_tmin = None # <-- new +metadata_tmax = ["response/left", "response/right"] + +metadata, events, event_id = mne.epochs.make_metadata( + events=all_events, + event_id=all_event_id, + tmin=metadata_tmin, + tmax=metadata_tmax, + sfreq=raw.info["sfreq"], + row_events=row_events, +) + +metadata diff --git a/mne/epochs.py b/mne/epochs.py index 7b87dee5179..83b427ac394 100644 --- a/mne/epochs.py +++ b/mne/epochs.py @@ -2829,14 +2829,15 @@ def make_metadata( A mapping from event names (keys) to event IDs (values). The event names will be incorporated as columns of the returned metadata :class:`~pandas.DataFrame`. - tmin, tmax : float | None - Start and end of the time interval for metadata generation in seconds, relative - to the time-locked event of the respective time window (the "row events"). + tmin, tmax : float | str | list of str | None + If float, start and end of the time interval for metadata generation in seconds, + relative to the time-locked event of the respective time window (the "row + events"). .. note:: If you are planning to attach the generated metadata to `~mne.Epochs` and intend to include only events that fall inside - your epochs time interval, pass the same ``tmin`` and ``tmax`` + your epoch's time interval, pass the same ``tmin`` and ``tmax`` values here as you use for your epochs. If ``None``, the time window used for metadata generation is bounded by the @@ -2849,8 +2850,17 @@ def make_metadata( the first row event. If ``tmax=None``, the last time window for metadata generation ends with the last event in ``events``. + If a string or a list of strings, the events bounding the metadata around each + "row event". For ``tmin``, the events are assumed to occur **before** the row + event, and for ``tmax``, the events are assumed to occur **after** – unless + ``tmin`` or ``tmax`` are equal to a row event, in which case the row event + serves as the bound. + .. versionchanged:: 1.6.0 Added support for ``None``. + + .. versionadded:: 1.7.0 + Added support for strings. sfreq : float The sampling frequency of the data from which the events array was extracted. @@ -2936,8 +2946,8 @@ def make_metadata( be attached; it may well be much shorter or longer, or not overlap at all, if desired. This can be useful, for example, to include events that occurred before or after an epoch, e.g. during the inter-trial interval. - If either ``tmin``, ``tmax``, or both are ``None``, the time window will - typically vary, too. + If either ``tmin``, ``tmax``, or both are ``None``, or a string referring e.g. to a + response event, the time window will typically vary, too. .. versionadded:: 0.23 @@ -2950,11 +2960,11 @@ def make_metadata( _validate_type(events, types=("array-like",), item_name="events") _validate_type(event_id, types=(dict,), item_name="event_id") _validate_type(sfreq, types=("numeric",), item_name="sfreq") - _validate_type(tmin, types=("numeric", None), item_name="tmin") - _validate_type(tmax, types=("numeric", None), item_name="tmax") - _validate_type(row_events, types=(None, str, list, tuple), item_name="row_events") - _validate_type(keep_first, types=(None, str, list, tuple), item_name="keep_first") - _validate_type(keep_last, types=(None, str, list, tuple), item_name="keep_last") + _validate_type(tmin, types=("numeric", str, "array-like", None), item_name="tmin") + _validate_type(tmax, types=("numeric", str, "array-like", None), item_name="tmax") + _validate_type(row_events, types=(None, str, "array-like"), item_name="row_events") + _validate_type(keep_first, types=(None, str, "array-like"), item_name="keep_first") + _validate_type(keep_last, types=(None, str, "array-like"), item_name="keep_last") if not event_id: raise ValueError("event_id dictionary must contain at least one entry") @@ -2971,6 +2981,19 @@ def _ensure_list(x): keep_first = _ensure_list(keep_first) keep_last = _ensure_list(keep_last) + # Turn tmin, tmax into a list if they're strings or arrays of strings + try: + _validate_type(tmin, types=(str, "array-like"), item_name="tmin") + tmin = _ensure_list(tmin) + except TypeError: + pass + + try: + _validate_type(tmax, types=(str, "array-like"), item_name="tmax") + tmax = _ensure_list(tmax) + except TypeError: + pass + keep_first_and_last = set(keep_first) & set(keep_last) if keep_first_and_last: raise ValueError( @@ -2990,18 +3013,40 @@ def _ensure_list(x): f"{param_name}, cannot be found in event_id dictionary" ) - event_name_diff = sorted(set(row_events) - set(event_id.keys())) - if event_name_diff: - raise ValueError( - f"Present in row_events, but missing from event_id: " - f'{", ".join(event_name_diff)}' + # If tmin, tmax are strings, ensure these event names are present in event_id + def _diff_input_strings_vs_event_id(input_strings, input_name, event_id): + event_name_diff = sorted(set(input_strings) - set(event_id.keys())) + if event_name_diff: + raise ValueError( + f"Present in {input_name}, but missing from event_id: " + f'{", ".join(event_name_diff)}' + ) + + _diff_input_strings_vs_event_id( + input_strings=row_events, input_name="row_events", event_id=event_id + ) + if isinstance(tmin, list): + _diff_input_strings_vs_event_id( + input_strings=tmin, input_name="tmin", event_id=event_id + ) + if isinstance(tmax, list): + _diff_input_strings_vs_event_id( + input_strings=tmax, input_name="tmax", event_id=event_id ) - del event_name_diff # First and last sample of each epoch, relative to the time-locked event # This follows the approach taken in mne.Epochs - start_sample = None if tmin is None else int(round(tmin * sfreq)) - stop_sample = None if tmax is None else int(round(tmax * sfreq)) + 1 + # For strings and None, we don't know the start and stop samples in advance as the + # time window can vary. + if isinstance(tmin, (type(None), list)): + start_sample = None + else: + start_sample = int(round(tmin * sfreq)) + + if isinstance(tmax, (type(None), list)): + stop_sample = None + else: + stop_sample = int(round(tmax * sfreq)) + 1 # Make indexing easier # We create the DataFrame before subsetting the events so we end up with @@ -3055,14 +3100,47 @@ def _ensure_list(x): metadata.loc[row_idx, "event_name"] = id_to_name_map[row_event.id] # Determine which events fall into the current time window - if start_sample is None: + if start_sample is None and isinstance(tmin, list): + # Lower bound is the the current or the closest previpus event with a name + # in "tmin"; if there is no such event (e.g., beginning of the recording is + # being approached), the upper lower becomes the last event in the + # recording. + prev_matching_events = events_df.loc[ + (events_df["sample"] <= row_event.sample) + & (events_df["id"].isin([event_id[name] for name in tmin])), + :, + ] + if prev_matching_events.size == 0: + # No earlier matching event. Use the current one as the beginning of the + # time window. This may occur at the beginning of a recording. + window_start_sample = row_event.sample + else: + # At least one earlier matching event. Use the closest one. + window_start_sample = prev_matching_events.iloc[-1]["sample"] + elif start_sample is None: # Lower bound is the current event. window_start_sample = row_event.sample else: # Lower bound is determined by tmin. window_start_sample = row_event.sample + start_sample - if stop_sample is None: + if stop_sample is None and isinstance(tmax, list): + # Upper bound is the the current or the closest following event with a name + # in "tmax"; if there is no such event (e.g., end of the recording is being + # approached), the upper bound becomes the last event in the recording. + next_matching_events = events_df.loc[ + (events_df["sample"] >= row_event.sample) + & (events_df["id"].isin([event_id[name] for name in tmax])), + :, + ] + if next_matching_events.size == 0: + # No matching event after the current one; use the end of the recording + # as upper bound. This may occur at the end of a recording. + window_stop_sample = events_df["sample"].iloc[-1] + else: + # At least one matching later event. Use the closest one.. + window_stop_sample = next_matching_events.iloc[0]["sample"] + elif stop_sample is None: # Upper bound: next event of the same type, or the last event (of # any type) if no later event of the same type can be found. next_events = events_df.loc[ diff --git a/mne/tests/test_epochs.py b/mne/tests/test_epochs.py index fdd91fd96a0..0bede8b53d4 100644 --- a/mne/tests/test_epochs.py +++ b/mne/tests/test_epochs.py @@ -4286,8 +4286,19 @@ def test_make_metadata(all_event_id, row_events, tmin, tmax, keep_first, keep_la Epochs(raw, events=events, event_id=event_id, metadata=metadata, verbose="warning") -def test_make_metadata_bounded_by_row_events(): - """Test make_metadata() with tmin, tmax set to None.""" +@pytest.mark.parametrize( + ("tmin", "tmax"), + [ + (None, None), + ("cue", "resp"), + (["cue"], ["resp"]), + (None, "resp"), + ("cue", None), + (["rec_start", "cue"], ["resp", "rec_end"]), + ], +) +def test_make_metadata_bounded_by_row_or_tmin_tmax_event_names(tmin, tmax): + """Test make_metadata() with tmin, tmax set to None or strings.""" pytest.importorskip("pandas") sfreq = 100 @@ -4332,8 +4343,8 @@ def test_make_metadata_bounded_by_row_events(): metadata, events_new, event_id_new = mne.epochs.make_metadata( events=events, event_id=event_id, - tmin=None, - tmax=None, + tmin=tmin, + tmax=tmax, sfreq=raw.info["sfreq"], row_events="cue", ) @@ -4356,8 +4367,15 @@ def test_make_metadata_bounded_by_row_events(): # 2nd trial assert np.isnan(metadata.iloc[1]["rec_end"]) - # 3rd trial until end of the recording - assert metadata.iloc[2]["resp"] < metadata.iloc[2]["rec_end"] + # 3rd trial + if tmax is None: + # until end of the recording + assert metadata.iloc[2]["resp"] < metadata.iloc[2]["rec_end"] + else: + # until tmax + assert np.isnan(metadata.iloc[2]["rec_end"]) + last_event_name = tmax[0] if isinstance(tmax, list) else tmax + assert metadata.iloc[2][last_event_name] > 0 def test_events_list(): diff --git a/tutorials/epochs/40_autogenerate_metadata.py b/tutorials/epochs/40_autogenerate_metadata.py index 8f7f3f5a90e..9e769a5ff5e 100644 --- a/tutorials/epochs/40_autogenerate_metadata.py +++ b/tutorials/epochs/40_autogenerate_metadata.py @@ -46,13 +46,11 @@ # Copyright the MNE-Python contributors. # %% -from pathlib import Path - import matplotlib.pyplot as plt import mne -data_dir = Path(mne.datasets.erp_core.data_path()) +data_dir = mne.datasets.erp_core.data_path() infile = data_dir / "ERP-CORE_Subject-001_Task-Flankers_eeg.fif" raw = mne.io.read_raw(infile, preload=True) @@ -88,7 +86,7 @@ # i.e. starting with stimulus onset and expanding beyond the end of the epoch metadata_tmin, metadata_tmax = 0.0, 1.5 -# auto-create metadata +# auto-create metadata: # this also returns a new events array and an event_id dictionary. we'll see # later why this is important metadata, events, event_id = mne.epochs.make_metadata( From 64901c4e24db697b23930f7557480ca0b97b4f8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20H=C3=B6chenberger?= Date: Sat, 2 Mar 2024 14:10:25 +0100 Subject: [PATCH 214/405] Bump ruff to v0.3.0 (#12477) --- .pre-commit-config.yaml | 2 +- doc/conf.py | 8 ++-- doc/sphinxext/unit_role.py | 2 +- examples/datasets/hf_sef_data.py | 1 - examples/decoding/decoding_csp_eeg.py | 1 - examples/decoding/decoding_csp_timefreq.py | 1 - examples/decoding/ssd_spatial_filters.py | 1 - examples/io/elekta_epochs.py | 1 - examples/preprocessing/css.py | 6 +-- .../preprocessing/eog_artifact_histogram.py | 1 - examples/preprocessing/xdawn_denoising.py | 1 - .../visualization/eyetracking_plot_heatmap.py | 1 - .../visualization/topo_compare_conditions.py | 1 - examples/visualization/topo_customized.py | 1 - mne/_fiff/tests/test_meas_info.py | 5 ++- mne/beamformer/resolution_matrix.py | 1 + mne/beamformer/tests/test_lcmv.py | 8 ++-- mne/commands/mne_bti2fiff.py | 1 - mne/commands/mne_clean_eog_ecg.py | 1 - mne/commands/mne_make_scalp_surfaces.py | 1 + mne/conftest.py | 5 ++- mne/export/tests/test_export.py | 10 +++-- mne/forward/tests/test_forward.py | 5 ++- mne/io/_read_raw.py | 1 - mne/io/artemis123/tests/test_artemis123.py | 5 ++- mne/io/besa/tests/test_besa.py | 1 + mne/io/brainvision/tests/test_brainvision.py | 5 ++- mne/io/edf/edf.py | 2 +- mne/io/eeglab/tests/test_eeglab.py | 5 ++- mne/io/eyelink/_utils.py | 1 - mne/io/eyelink/tests/test_eyelink.py | 5 ++- mne/io/fiff/tests/test_raw_fiff.py | 5 ++- mne/io/neuralynx/neuralynx.py | 6 +-- mne/minimum_norm/resolution_matrix.py | 1 + mne/minimum_norm/spatial_resolution.py | 1 + mne/preprocessing/_lof.py | 1 - mne/preprocessing/tests/test_ica.py | 10 +++-- mne/preprocessing/tests/test_ssp.py | 5 ++- .../bootstrap-icons/gen_css_for_mne.py | 1 - mne/report/tests/test_report.py | 18 +++++--- mne/source_space/tests/test_source_space.py | 5 ++- mne/stats/tests/test_cluster_level.py | 5 ++- mne/tests/test_bem.py | 15 ++++--- mne/time_frequency/tfr.py | 2 +- mne/viz/circle.py | 1 - mne/viz/evoked_field.py | 43 ++++++++++--------- mne/viz/montage.py | 1 + mne/viz/tests/test_3d.py | 12 +++--- mne/viz/tests/test_epochs.py | 5 ++- mne/viz/tests/test_misc.py | 10 +++-- mne/viz/tests/test_raw.py | 5 ++- mne/viz/ui_events.py | 1 + tutorials/visualization/20_ui_events.py | 1 + 53 files changed, 133 insertions(+), 111 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b564f516d2b..0298815545a 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.2.2 + rev: v0.3.0 hooks: - id: ruff name: ruff lint mne diff --git a/doc/conf.py b/doc/conf.py index 5d43b594b3a..a00a34debc3 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -1194,21 +1194,21 @@ def append_attr_meth_examples(app, what, name, obj, options, lines): "carousel": [ dict( title="Source Estimation", - text="Distributed, sparse, mixed-norm, beam\u00ADformers, dipole fitting, and more.", # noqa E501 + text="Distributed, sparse, mixed-norm, beam\u00adformers, dipole fitting, and more.", # noqa E501 url="auto_tutorials/inverse/index.html", img="sphx_glr_30_mne_dspm_loreta_008.gif", alt="dSPM", ), dict( title="Machine Learning", - text="Advanced decoding models including time general\u00ADiza\u00ADtion.", # noqa E501 + text="Advanced decoding models including time general\u00adiza\u00adtion.", # noqa E501 url="auto_tutorials/machine-learning/50_decoding.html", img="sphx_glr_50_decoding_006.png", alt="Decoding", ), dict( title="Encoding Models", - text="Receptive field estima\u00ADtion with optional smooth\u00ADness priors.", # noqa E501 + text="Receptive field estima\u00adtion with optional smooth\u00adness priors.", # noqa E501 url="auto_tutorials/machine-learning/30_strf.html", img="sphx_glr_30_strf_001.png", alt="STRF", @@ -1222,7 +1222,7 @@ def append_attr_meth_examples(app, what, name, obj, options, lines): ), dict( title="Connectivity", - text="All-to-all spectral and effective connec\u00ADtivity measures.", # noqa E501 + text="All-to-all spectral and effective connec\u00adtivity measures.", # noqa E501 url="https://mne.tools/mne-connectivity/stable/auto_examples/mne_inverse_label_connectivity.html", # noqa E501 img="https://mne.tools/mne-connectivity/stable/_images/sphx_glr_mne_inverse_label_connectivity_001.png", # noqa E501 alt="Connectivity", diff --git a/doc/sphinxext/unit_role.py b/doc/sphinxext/unit_role.py index 89b7543548c..4d9c9d94252 100644 --- a/doc/sphinxext/unit_role.py +++ b/doc/sphinxext/unit_role.py @@ -24,7 +24,7 @@ def pass_error_to_sphinx(rawtext, text, lineno, inliner): except ValueError: return pass_error_to_sphinx(rawtext, text, lineno, inliner) # input is well-formatted: proceed - node = nodes.Text("\u202F".join(parts)) + node = nodes.Text("\u202f".join(parts)) return [node], [] diff --git a/examples/datasets/hf_sef_data.py b/examples/datasets/hf_sef_data.py index ec6ef61bcb2..44aa6e8f9a4 100644 --- a/examples/datasets/hf_sef_data.py +++ b/examples/datasets/hf_sef_data.py @@ -14,7 +14,6 @@ # %% - import os import mne diff --git a/examples/decoding/decoding_csp_eeg.py b/examples/decoding/decoding_csp_eeg.py index 893e7969c7a..6120bd5e5dd 100644 --- a/examples/decoding/decoding_csp_eeg.py +++ b/examples/decoding/decoding_csp_eeg.py @@ -20,7 +20,6 @@ # %% - import matplotlib.pyplot as plt import numpy as np from sklearn.discriminant_analysis import LinearDiscriminantAnalysis diff --git a/examples/decoding/decoding_csp_timefreq.py b/examples/decoding/decoding_csp_timefreq.py index 2f36064b615..6f13175846e 100644 --- a/examples/decoding/decoding_csp_timefreq.py +++ b/examples/decoding/decoding_csp_timefreq.py @@ -21,7 +21,6 @@ # %% - import matplotlib.pyplot as plt import numpy as np from sklearn.discriminant_analysis import LinearDiscriminantAnalysis diff --git a/examples/decoding/ssd_spatial_filters.py b/examples/decoding/ssd_spatial_filters.py index 5f4ea3fbcf7..b7c8c4f2c94 100644 --- a/examples/decoding/ssd_spatial_filters.py +++ b/examples/decoding/ssd_spatial_filters.py @@ -20,7 +20,6 @@ # %% - import matplotlib.pyplot as plt import mne diff --git a/examples/io/elekta_epochs.py b/examples/io/elekta_epochs.py index 5619a0e5174..4afa0ad888d 100644 --- a/examples/io/elekta_epochs.py +++ b/examples/io/elekta_epochs.py @@ -15,7 +15,6 @@ # %% - import os import mne diff --git a/examples/preprocessing/css.py b/examples/preprocessing/css.py index 9095094d93c..ba4e2385d0c 100644 --- a/examples/preprocessing/css.py +++ b/examples/preprocessing/css.py @@ -75,9 +75,9 @@ def subcortical_waveform(times): labels=[postcenlab, hiplab], data_fun=cortical_waveform, ) -stc.data[ - np.where(np.isin(stc.vertices[0], hiplab.vertices))[0], : -] = subcortical_waveform(times) +stc.data[np.where(np.isin(stc.vertices[0], hiplab.vertices))[0], :] = ( + subcortical_waveform(times) +) evoked = simulate_evoked(fwd, stc, raw.info, cov, nave=15) ############################################################################### diff --git a/examples/preprocessing/eog_artifact_histogram.py b/examples/preprocessing/eog_artifact_histogram.py index d883fa427f8..8a89f9d8a44 100644 --- a/examples/preprocessing/eog_artifact_histogram.py +++ b/examples/preprocessing/eog_artifact_histogram.py @@ -15,7 +15,6 @@ # %% - import matplotlib.pyplot as plt import numpy as np diff --git a/examples/preprocessing/xdawn_denoising.py b/examples/preprocessing/xdawn_denoising.py index 6fc38a55b94..20a6abc72fb 100644 --- a/examples/preprocessing/xdawn_denoising.py +++ b/examples/preprocessing/xdawn_denoising.py @@ -25,7 +25,6 @@ # %% - from mne import Epochs, compute_raw_covariance, io, pick_types, read_events from mne.datasets import sample from mne.preprocessing import Xdawn diff --git a/examples/visualization/eyetracking_plot_heatmap.py b/examples/visualization/eyetracking_plot_heatmap.py index bbfb9b13739..9225493ef88 100644 --- a/examples/visualization/eyetracking_plot_heatmap.py +++ b/examples/visualization/eyetracking_plot_heatmap.py @@ -24,7 +24,6 @@ # :ref:`example data `: eye-tracking data recorded from SR research's # ``'.asc'`` file format. - import matplotlib.pyplot as plt import mne diff --git a/examples/visualization/topo_compare_conditions.py b/examples/visualization/topo_compare_conditions.py index 7572eab47e5..3ab4e46d5f2 100644 --- a/examples/visualization/topo_compare_conditions.py +++ b/examples/visualization/topo_compare_conditions.py @@ -19,7 +19,6 @@ # %% - import matplotlib.pyplot as plt import mne diff --git a/examples/visualization/topo_customized.py b/examples/visualization/topo_customized.py index 2d3c6662ebc..2303961f9da 100644 --- a/examples/visualization/topo_customized.py +++ b/examples/visualization/topo_customized.py @@ -19,7 +19,6 @@ # %% - import matplotlib.pyplot as plt import numpy as np diff --git a/mne/_fiff/tests/test_meas_info.py b/mne/_fiff/tests/test_meas_info.py index 3cf1f79cceb..8552585eec4 100644 --- a/mne/_fiff/tests/test_meas_info.py +++ b/mne/_fiff/tests/test_meas_info.py @@ -350,8 +350,9 @@ def test_read_write_info(tmp_path): @testing.requires_testing_data def test_dir_warning(): """Test that trying to read a bad filename emits a warning before an error.""" - with pytest.raises(OSError, match="directory"), pytest.warns( - RuntimeWarning, match="does not conform" + with ( + pytest.raises(OSError, match="directory"), + pytest.warns(RuntimeWarning, match="does not conform"), ): read_info(ctf_fname) diff --git a/mne/beamformer/resolution_matrix.py b/mne/beamformer/resolution_matrix.py index 108fb7a4dbf..ce55a09584b 100644 --- a/mne/beamformer/resolution_matrix.py +++ b/mne/beamformer/resolution_matrix.py @@ -1,4 +1,5 @@ """Compute resolution matrix for beamformers.""" + # Authors: olaf.hauk@mrc-cbu.cam.ac.uk # # License: BSD-3-Clause diff --git a/mne/beamformer/tests/test_lcmv.py b/mne/beamformer/tests/test_lcmv.py index f6c7ef20492..509afbcf79e 100644 --- a/mne/beamformer/tests/test_lcmv.py +++ b/mne/beamformer/tests/test_lcmv.py @@ -589,9 +589,11 @@ def test_make_lcmv_sphere(pick_ori, weight_norm): fwd_sphere = mne.make_forward_solution(evoked.info, None, src, sphere) # Test that we get an error if not reducing rank - with pytest.raises( - ValueError, match="Singular matrix detected" - ), _record_warnings(), pytest.warns(RuntimeWarning, match="positive semidefinite"): + with ( + pytest.raises(ValueError, match="Singular matrix detected"), + _record_warnings(), + pytest.warns(RuntimeWarning, match="positive semidefinite"), + ): make_lcmv( evoked.info, fwd_sphere, diff --git a/mne/commands/mne_bti2fiff.py b/mne/commands/mne_bti2fiff.py index c8664ca5a35..2c4e4083df1 100644 --- a/mne/commands/mne_bti2fiff.py +++ b/mne/commands/mne_bti2fiff.py @@ -30,7 +30,6 @@ # License: BSD-3-Clause # Copyright the MNE-Python contributors. - import sys import mne diff --git a/mne/commands/mne_clean_eog_ecg.py b/mne/commands/mne_clean_eog_ecg.py index 8f18f16f6cb..10b84540756 100644 --- a/mne/commands/mne_clean_eog_ecg.py +++ b/mne/commands/mne_clean_eog_ecg.py @@ -14,7 +14,6 @@ # License: BSD-3-Clause # Copyright the MNE-Python contributors. - import sys import mne diff --git a/mne/commands/mne_make_scalp_surfaces.py b/mne/commands/mne_make_scalp_surfaces.py index 91ed2fdae60..85b7acd2883 100644 --- a/mne/commands/mne_make_scalp_surfaces.py +++ b/mne/commands/mne_make_scalp_surfaces.py @@ -17,6 +17,7 @@ $ mne make_scalp_surfaces --overwrite --subject sample """ + import os import sys diff --git a/mne/conftest.py b/mne/conftest.py index fd7d1946843..0feb92b0ada 100644 --- a/mne/conftest.py +++ b/mne/conftest.py @@ -802,8 +802,9 @@ def src_volume_labels(): """Create a 7mm source space with labels.""" pytest.importorskip("nibabel") volume_labels = mne.get_volume_labels_from_aseg(fname_aseg) - with _record_warnings(), pytest.warns( - RuntimeWarning, match="Found no usable.*t-vessel.*" + with ( + _record_warnings(), + pytest.warns(RuntimeWarning, match="Found no usable.*t-vessel.*"), ): src = mne.setup_volume_source_space( "sample", diff --git a/mne/export/tests/test_export.py b/mne/export/tests/test_export.py index fc5e68c9225..808b020bfb4 100644 --- a/mne/export/tests/test_export.py +++ b/mne/export/tests/test_export.py @@ -78,8 +78,9 @@ def test_export_raw_pybv(tmp_path, meas_date, orig_time, ext): raw.set_annotations(annots) temp_fname = tmp_path / ("test" + ext) - with _record_warnings(), pytest.warns( - RuntimeWarning, match="'short' format. Converting" + with ( + _record_warnings(), + pytest.warns(RuntimeWarning, match="'short' format. Converting"), ): raw.export(temp_fname) raw_read = read_raw_brainvision(str(temp_fname).replace(".eeg", ".vhdr")) @@ -303,8 +304,9 @@ def test_export_edf_signal_clipping(tmp_path, physical_range, exceeded_bound): raw = read_raw_fif(fname_raw) raw.pick(picks=["eeg", "ecog", "seeg"]).load_data() temp_fname = tmp_path / "test.edf" - with _record_warnings(), pytest.warns( - RuntimeWarning, match=f"The {exceeded_bound}" + with ( + _record_warnings(), + pytest.warns(RuntimeWarning, match=f"The {exceeded_bound}"), ): raw.export(temp_fname, physical_range=physical_range) raw_read = read_raw_edf(temp_fname, preload=True) diff --git a/mne/forward/tests/test_forward.py b/mne/forward/tests/test_forward.py index dd73d1099f1..7442c68959c 100644 --- a/mne/forward/tests/test_forward.py +++ b/mne/forward/tests/test_forward.py @@ -230,8 +230,9 @@ def test_apply_forward(): # Evoked evoked = read_evokeds(fname_evoked, condition=0) evoked.pick(picks="meg") - with _record_warnings(), pytest.warns( - RuntimeWarning, match="only .* positive values" + with ( + _record_warnings(), + pytest.warns(RuntimeWarning, match="only .* positive values"), ): evoked = apply_forward(fwd, stc, evoked.info, start=start, stop=stop) data = evoked.data diff --git a/mne/io/_read_raw.py b/mne/io/_read_raw.py index c226bf63285..6df23ee02f1 100644 --- a/mne/io/_read_raw.py +++ b/mne/io/_read_raw.py @@ -5,7 +5,6 @@ # License: BSD-3-Clause # Copyright the MNE-Python contributors. - from functools import partial from pathlib import Path diff --git a/mne/io/artemis123/tests/test_artemis123.py b/mne/io/artemis123/tests/test_artemis123.py index 2dac9645c33..ec4d3d4017f 100644 --- a/mne/io/artemis123/tests/test_artemis123.py +++ b/mne/io/artemis123/tests/test_artemis123.py @@ -97,8 +97,9 @@ def test_dev_head_t(): assert_equal(raw.info["sfreq"], 5000.0) # test with head loc and digitization - with pytest.warns(RuntimeWarning, match="consistency"), pytest.warns( - RuntimeWarning, match="Large difference" + with ( + pytest.warns(RuntimeWarning, match="consistency"), + pytest.warns(RuntimeWarning, match="Large difference"), ): raw = read_raw_artemis123( short_HPI_dip_fname, add_head_trans=True, pos_fname=dig_fname diff --git a/mne/io/besa/tests/test_besa.py b/mne/io/besa/tests/test_besa.py index aeecf48cd63..2ee2843840b 100644 --- a/mne/io/besa/tests/test_besa.py +++ b/mne/io/besa/tests/test_besa.py @@ -1,6 +1,7 @@ # License: BSD-3-Clause # Copyright the MNE-Python contributors. """Test reading BESA fileformats.""" + import inspect from pathlib import Path diff --git a/mne/io/brainvision/tests/test_brainvision.py b/mne/io/brainvision/tests/test_brainvision.py index 166c3564fae..309e44e3cf8 100644 --- a/mne/io/brainvision/tests/test_brainvision.py +++ b/mne/io/brainvision/tests/test_brainvision.py @@ -613,8 +613,9 @@ def test_brainvision_vectorized_data(): def test_coodinates_extraction(): """Test reading of [Coordinates] section if present.""" # vhdr 2 has a Coordinates section - with _record_warnings(), pytest.warns( - RuntimeWarning, match="coordinate information" + with ( + _record_warnings(), + pytest.warns(RuntimeWarning, match="coordinate information"), ): raw = read_raw_brainvision(vhdr_v2_path) diff --git a/mne/io/edf/edf.py b/mne/io/edf/edf.py index 91a7632701d..4c3b2da8e24 100644 --- a/mne/io/edf/edf.py +++ b/mne/io/edf/edf.py @@ -924,7 +924,7 @@ def _read_edf_header(fname, exclude, infer_types, include=None): if i in exclude: continue # allow μ (greek mu), µ (micro symbol) and μ (sjis mu) codepoints - if unit in ("\u03BCV", "\u00B5V", "\x83\xCAV", "uV"): + if unit in ("\u03bcV", "\u00b5V", "\x83\xcaV", "uV"): edf_info["units"].append(1e-6) elif unit == "mV": edf_info["units"].append(1e-3) diff --git a/mne/io/eeglab/tests/test_eeglab.py b/mne/io/eeglab/tests/test_eeglab.py index 88c18d2aab0..af1a3bbfc77 100644 --- a/mne/io/eeglab/tests/test_eeglab.py +++ b/mne/io/eeglab/tests/test_eeglab.py @@ -140,8 +140,9 @@ def test_io_set_raw_more(tmp_path): shutil.copyfile( base_dir / "test_raw.fdt", negative_latency_fname.with_suffix(".fdt") ) - with _record_warnings(), pytest.warns( - RuntimeWarning, match="has a sample index of -1." + with ( + _record_warnings(), + pytest.warns(RuntimeWarning, match="has a sample index of -1."), ): read_raw_eeglab(input_fname=negative_latency_fname, preload=True) diff --git a/mne/io/eyelink/_utils.py b/mne/io/eyelink/_utils.py index f6ab2f8790d..cefc184c2f9 100644 --- a/mne/io/eyelink/_utils.py +++ b/mne/io/eyelink/_utils.py @@ -3,7 +3,6 @@ # License: BSD-3-Clause # Copyright the MNE-Python contributors. - import re from datetime import datetime, timedelta, timezone diff --git a/mne/io/eyelink/tests/test_eyelink.py b/mne/io/eyelink/tests/test_eyelink.py index 953fde5b67d..dd3a32c270d 100644 --- a/mne/io/eyelink/tests/test_eyelink.py +++ b/mne/io/eyelink/tests/test_eyelink.py @@ -256,8 +256,9 @@ def test_multi_block_misc_channels(fname, tmp_path): out_file = tmp_path / "tmp_eyelink.asc" _simulate_eye_tracking_data(fname, out_file) - with _record_warnings(), pytest.warns( - RuntimeWarning, match="Raw eyegaze coordinates" + with ( + _record_warnings(), + pytest.warns(RuntimeWarning, match="Raw eyegaze coordinates"), ): raw = read_raw_eyelink(out_file, apply_offsets=True) diff --git a/mne/io/fiff/tests/test_raw_fiff.py b/mne/io/fiff/tests/test_raw_fiff.py index a28844eb5f5..91125de98be 100644 --- a/mne/io/fiff/tests/test_raw_fiff.py +++ b/mne/io/fiff/tests/test_raw_fiff.py @@ -2120,8 +2120,9 @@ def test_corrupted(tmp_path, offset): bad_fname = tmp_path / "test_raw.fif" with open(bad_fname, "wb") as fid: fid.write(data) - with _record_warnings(), pytest.warns( - RuntimeWarning, match=".*tag directory.*corrupt.*" + with ( + _record_warnings(), + pytest.warns(RuntimeWarning, match=".*tag directory.*corrupt.*"), ): raw_bad = read_raw_fif(bad_fname) assert_allclose(raw.get_data(), raw_bad.get_data()) diff --git a/mne/io/neuralynx/neuralynx.py b/mne/io/neuralynx/neuralynx.py index 4eeb7cf8d08..0390fb70071 100644 --- a/mne/io/neuralynx/neuralynx.py +++ b/mne/io/neuralynx/neuralynx.py @@ -347,9 +347,9 @@ def _read_segment_file(self, data, idx, fi, start, stop, cals, mult): sel_samples_local[0:-1, 1] = ( sel_samples_global[0:-1, 1] - sel_samples_global[0:-1, 0] ) - sel_samples_local[ - 1::, 0 - ] = 0 # now set the start sample for all segments after the first to 0 + sel_samples_local[1::, 0] = ( + 0 # now set the start sample for all segments after the first to 0 + ) sel_samples_local[0, 0] = ( start - sel_samples_global[0, 0] diff --git a/mne/minimum_norm/resolution_matrix.py b/mne/minimum_norm/resolution_matrix.py index 3dd24ac6847..655ca991914 100644 --- a/mne/minimum_norm/resolution_matrix.py +++ b/mne/minimum_norm/resolution_matrix.py @@ -1,4 +1,5 @@ """Compute resolution matrix for linear estimators.""" + # Authors: olaf.hauk@mrc-cbu.cam.ac.uk # # License: BSD-3-Clause diff --git a/mne/minimum_norm/spatial_resolution.py b/mne/minimum_norm/spatial_resolution.py index d68be423494..c9d28aef4d8 100644 --- a/mne/minimum_norm/spatial_resolution.py +++ b/mne/minimum_norm/spatial_resolution.py @@ -7,6 +7,7 @@ Resolution metrics: localisation error, spatial extent, relative amplitude. Metrics can be computed for point-spread and cross-talk functions (PSFs/CTFs). """ + import numpy as np from ..source_estimate import SourceEstimate diff --git a/mne/preprocessing/_lof.py b/mne/preprocessing/_lof.py index 1af9c5b16e9..6d777599a8a 100644 --- a/mne/preprocessing/_lof.py +++ b/mne/preprocessing/_lof.py @@ -5,7 +5,6 @@ # License: BSD-3-Clause # Copyright the MNE-Python contributors. - import numpy as np from .._fiff.pick import _picks_to_idx diff --git a/mne/preprocessing/tests/test_ica.py b/mne/preprocessing/tests/test_ica.py index 67aabf14b12..299b1e961b3 100644 --- a/mne/preprocessing/tests/test_ica.py +++ b/mne/preprocessing/tests/test_ica.py @@ -173,8 +173,9 @@ def test_ica_simple(method): info = create_info(data.shape[-2], 1000.0, "eeg") cov = make_ad_hoc_cov(info) ica = ICA(n_components=n_components, method=method, random_state=0, noise_cov=cov) - with pytest.warns(RuntimeWarning, match="high-pass filtered"), pytest.warns( - RuntimeWarning, match="No average EEG.*" + with ( + pytest.warns(RuntimeWarning, match="high-pass filtered"), + pytest.warns(RuntimeWarning, match="No average EEG.*"), ): ica.fit(RawArray(data, info)) transform = ica.unmixing_matrix_ @ ica.pca_components_ @ A @@ -1259,8 +1260,9 @@ def test_fit_params_epochs_vs_raw(param_name, param_val, tmp_path): ica = ICA(n_components=n_components, max_iter=max_iter, method=method) fit_params = {param_name: param_val} - with _record_warnings(), pytest.warns( - RuntimeWarning, match="parameters.*will be ignored" + with ( + _record_warnings(), + pytest.warns(RuntimeWarning, match="parameters.*will be ignored"), ): ica.fit(inst=epochs, **fit_params) assert ica.reject_ == reject diff --git a/mne/preprocessing/tests/test_ssp.py b/mne/preprocessing/tests/test_ssp.py index 36bfa3505c1..a6ece5ea2e1 100644 --- a/mne/preprocessing/tests/test_ssp.py +++ b/mne/preprocessing/tests/test_ssp.py @@ -151,8 +151,9 @@ def test_compute_proj_eog(average, short_raw): assert projs == [] raw._data[raw.ch_names.index("EOG 061"), :] = 1.0 - with _record_warnings(), pytest.warns( - RuntimeWarning, match="filter.*longer than the signal" + with ( + _record_warnings(), + pytest.warns(RuntimeWarning, match="filter.*longer than the signal"), ): projs, events = compute_proj_eog(raw=raw, tmax=dur_use, ch_name="EOG 061") diff --git a/mne/report/js_and_css/bootstrap-icons/gen_css_for_mne.py b/mne/report/js_and_css/bootstrap-icons/gen_css_for_mne.py index 95b99c306f7..7eac8ecdaa0 100644 --- a/mne/report/js_and_css/bootstrap-icons/gen_css_for_mne.py +++ b/mne/report/js_and_css/bootstrap-icons/gen_css_for_mne.py @@ -15,7 +15,6 @@ # License: BSD-3-Clause # Copyright the MNE-Python contributors. - import base64 from pathlib import Path diff --git a/mne/report/tests/test_report.py b/mne/report/tests/test_report.py index 7374868c559..437cfec3cc7 100644 --- a/mne/report/tests/test_report.py +++ b/mne/report/tests/test_report.py @@ -1012,8 +1012,12 @@ def test_manual_report_2d(tmp_path, invisible_fig): for ch in evoked_no_ch_locs.info["chs"]: ch["loc"][:3] = np.nan - with _record_warnings(), pytest.warns( - RuntimeWarning, match="No EEG channel locations found, cannot create joint plot" + with ( + _record_warnings(), + pytest.warns( + RuntimeWarning, + match="No EEG channel locations found, cannot create joint plot", + ), ): r.add_evokeds( evokeds=evoked_no_ch_locs, @@ -1041,8 +1045,9 @@ def test_manual_report_2d(tmp_path, invisible_fig): for ch in ica_no_ch_locs.info["chs"]: ch["loc"][:3] = np.nan - with _record_warnings(), pytest.warns( - RuntimeWarning, match="No Magnetometers channel locations" + with ( + _record_warnings(), + pytest.warns(RuntimeWarning, match="No Magnetometers channel locations"), ): r.add_ica( ica=ica_no_ch_locs, picks=[0], inst=raw.copy().load_data(), title="ICA" @@ -1067,8 +1072,9 @@ def test_manual_report_3d(tmp_path, renderer): add_kwargs = dict( trans=trans_fname, info=info, subject="sample", subjects_dir=subjects_dir ) - with _record_warnings(), pytest.warns( - RuntimeWarning, match="could not be calculated" + with ( + _record_warnings(), + pytest.warns(RuntimeWarning, match="could not be calculated"), ): r.add_trans(title="coreg no dig", **add_kwargs) with info._unlock(): diff --git a/mne/source_space/tests/test_source_space.py b/mne/source_space/tests/test_source_space.py index a2648459fa6..14e5242ffe2 100644 --- a/mne/source_space/tests/test_source_space.py +++ b/mne/source_space/tests/test_source_space.py @@ -700,8 +700,9 @@ def test_source_space_exclusive_complete(src_volume_labels): for si, s in enumerate(src): assert_allclose(src_full[0]["rr"], s["rr"], atol=1e-6) # also check single_volume=True -- should be the same result - with _record_warnings(), pytest.warns( - RuntimeWarning, match="Found no usable.*Left-vessel.*" + with ( + _record_warnings(), + pytest.warns(RuntimeWarning, match="Found no usable.*Left-vessel.*"), ): src_single = setup_volume_source_space( src[0]["subject_his_id"], diff --git a/mne/stats/tests/test_cluster_level.py b/mne/stats/tests/test_cluster_level.py index 59bb0611aeb..1b020d11d28 100644 --- a/mne/stats/tests/test_cluster_level.py +++ b/mne/stats/tests/test_cluster_level.py @@ -96,8 +96,9 @@ def test_thresholds(numba_conditional): # nan handling in TFCE X = np.repeat(X[0], 2, axis=1) X[:, 1] = 0 - with _record_warnings(), pytest.warns( - RuntimeWarning, match="invalid value" + with ( + _record_warnings(), + pytest.warns(RuntimeWarning, match="invalid value"), ): # NumPy out = permutation_cluster_1samp_test( X, seed=0, threshold=dict(start=0, step=0.1), out_type="mask" diff --git a/mne/tests/test_bem.py b/mne/tests/test_bem.py index 261fd9efe55..3217205ba9f 100644 --- a/mne/tests/test_bem.py +++ b/mne/tests/test_bem.py @@ -461,8 +461,9 @@ def test_fit_sphere_to_headshape(): for d in info_shift["dig"]: d["r"] -= center d["r"] += shift_center - with _record_warnings(), pytest.warns( - RuntimeWarning, match="from head frame origin" + with ( + _record_warnings(), + pytest.warns(RuntimeWarning, match="from head frame origin"), ): r, oh, od = fit_sphere_to_headshape(info_shift, dig_kinds=dig_kinds, units="m") assert_allclose(oh, shift_center, atol=1e-6) @@ -483,8 +484,9 @@ def test_fit_sphere_to_headshape(): assert_allclose(od, od2, atol=1e-7) # this one should pass, 1 EXTRA point and 3 EEG (but the fit is terrible) info = Info(dig=dig[:7], dev_head_t=dev_head_t) - with _record_warnings(), pytest.warns( - RuntimeWarning, match="Estimated head radius" + with ( + _record_warnings(), + pytest.warns(RuntimeWarning, match="Estimated head radius"), ): r, oh, od = fit_sphere_to_headshape(info, units="m") # this one should fail, 1 EXTRA point and 3 EEG (but the fit is terrible) @@ -556,8 +558,9 @@ def _decimate_surface(points, triangles, n_triangles): # These are ignorable monkeypatch.setattr(mne.bem, "_tri_levels", dict(sparse=315)) - with _record_warnings(), pytest.warns( - RuntimeWarning, match=".*have fewer than three.*" + with ( + _record_warnings(), + pytest.warns(RuntimeWarning, match=".*have fewer than three.*"), ): make_scalp_surfaces(subject, subjects_dir, force=True, overwrite=True) (surf,) = read_bem_surfaces(sparse_path, on_defects="ignore") diff --git a/mne/time_frequency/tfr.py b/mne/time_frequency/tfr.py index 4f8a43b51e3..0233f82edac 100644 --- a/mne/time_frequency/tfr.py +++ b/mne/time_frequency/tfr.py @@ -2099,7 +2099,7 @@ def plot_joint( if (time_half_range == 0) and (freq_half_range == 0): sub_map_title = "(%.2f s,\n%.1f Hz)" % (time, freq) else: - sub_map_title = "(%.1f \u00B1 %.1f s,\n%.1f \u00B1 %.1f Hz)" % ( + sub_map_title = "(%.1f \u00b1 %.1f s,\n%.1f \u00b1 %.1f Hz)" % ( time, time_half_range, freq, diff --git a/mne/viz/circle.py b/mne/viz/circle.py index 5a3406a7c1b..b19130b3bff 100644 --- a/mne/viz/circle.py +++ b/mne/viz/circle.py @@ -7,7 +7,6 @@ # License: BSD-3-Clause # Copyright the MNE-Python contributors. - from functools import partial from itertools import cycle diff --git a/mne/viz/evoked_field.py b/mne/viz/evoked_field.py index 8223f2ec624..3ce9c6756e2 100644 --- a/mne/viz/evoked_field.py +++ b/mne/viz/evoked_field.py @@ -2,6 +2,7 @@ author: Marijn van Vliet """ + # License: BSD-3-Clause # Copyright the MNE-Python contributors. from functools import partial @@ -392,28 +393,28 @@ def _callback(vmax, kind, scaling): rng = [0, np.max(np.abs(surf_map["data"])) * scaling] hlayout = r._dock_add_layout(vertical=False) - self._widgets[ - f"vmax_slider_{surf_map['map_kind']}" - ] = r._dock_add_slider( - name=surf_map["map_kind"].upper(), - value=surf_map["map_vmax"] * scaling, - rng=rng, - callback=partial( - _callback, kind=surf_map["map_kind"], scaling=scaling - ), - double=True, - layout=hlayout, + self._widgets[f"vmax_slider_{surf_map['map_kind']}"] = ( + r._dock_add_slider( + name=surf_map["map_kind"].upper(), + value=surf_map["map_vmax"] * scaling, + rng=rng, + callback=partial( + _callback, kind=surf_map["map_kind"], scaling=scaling + ), + double=True, + layout=hlayout, + ) ) - self._widgets[ - f"vmax_spin_{surf_map['map_kind']}" - ] = r._dock_add_spin_box( - name="", - value=surf_map["map_vmax"] * scaling, - rng=rng, - callback=partial( - _callback, kind=surf_map["map_kind"], scaling=scaling - ), - layout=hlayout, + self._widgets[f"vmax_spin_{surf_map['map_kind']}"] = ( + r._dock_add_spin_box( + name="", + value=surf_map["map_vmax"] * scaling, + rng=rng, + callback=partial( + _callback, kind=surf_map["map_kind"], scaling=scaling + ), + layout=hlayout, + ) ) r._layout_add_widget(layout, hlayout) diff --git a/mne/viz/montage.py b/mne/viz/montage.py index e51cbcfb762..935a306e0d9 100644 --- a/mne/viz/montage.py +++ b/mne/viz/montage.py @@ -1,6 +1,7 @@ # License: BSD-3-Clause # Copyright the MNE-Python contributors. """Functions to plot EEG sensor montages or digitizer montages.""" + from copy import deepcopy import numpy as np diff --git a/mne/viz/tests/test_3d.py b/mne/viz/tests/test_3d.py index 14fdb544d83..5109becb645 100644 --- a/mne/viz/tests/test_3d.py +++ b/mne/viz/tests/test_3d.py @@ -128,9 +128,9 @@ def test_plot_sparse_source_estimates(renderer_interactive, brain_gc): n_verts = sum(len(v) for v in vertices) stc_data = np.zeros(n_verts * n_time) stc_size = stc_data.size - stc_data[ - (np.random.rand(stc_size // 20) * stc_size).astype(int) - ] = np.random.RandomState(0).rand(stc_data.size // 20) + stc_data[(np.random.rand(stc_size // 20) * stc_size).astype(int)] = ( + np.random.RandomState(0).rand(stc_data.size // 20) + ) stc_data.shape = (n_verts, n_time) stc = SourceEstimate(stc_data, vertices, 1, 1) @@ -1209,9 +1209,9 @@ def test_link_brains(renderer_interactive): n_verts = sum(len(v) for v in vertices) stc_data = np.zeros(n_verts * n_time) stc_size = stc_data.size - stc_data[ - (np.random.rand(stc_size // 20) * stc_size).astype(int) - ] = np.random.RandomState(0).rand(stc_data.size // 20) + stc_data[(np.random.rand(stc_size // 20) * stc_size).astype(int)] = ( + np.random.RandomState(0).rand(stc_data.size // 20) + ) stc_data.shape = (n_verts, n_time) stc = SourceEstimate(stc_data, vertices, 1, 1) diff --git a/mne/viz/tests/test_epochs.py b/mne/viz/tests/test_epochs.py index 1eccf64bbc2..9679a787277 100644 --- a/mne/viz/tests/test_epochs.py +++ b/mne/viz/tests/test_epochs.py @@ -301,8 +301,9 @@ def test_plot_epochs_image(epochs): picks=[0, 1], order=lambda times, data: np.arange(len(data))[::-1] ) # test warning - with _record_warnings(), pytest.warns( - RuntimeWarning, match="Only one channel in group" + with ( + _record_warnings(), + pytest.warns(RuntimeWarning, match="Only one channel in group"), ): epochs.plot_image(picks=[1], combine="mean") # group_by should be a dict diff --git a/mne/viz/tests/test_misc.py b/mne/viz/tests/test_misc.py index 180f8bb414a..aa0fa0f1959 100644 --- a/mne/viz/tests/test_misc.py +++ b/mne/viz/tests/test_misc.py @@ -215,8 +215,9 @@ def test_plot_events(): assert fig.axes[0].get_legend() is not None with pytest.warns(RuntimeWarning, match="Color was not assigned"): plot_events(events, raw.info["sfreq"], raw.first_samp, color=color) - with _record_warnings(), pytest.warns( - RuntimeWarning, match=r"vent \d+ missing from event_id" + with ( + _record_warnings(), + pytest.warns(RuntimeWarning, match=r"vent \d+ missing from event_id"), ): plot_events( events, @@ -246,8 +247,9 @@ def test_plot_events(): on_missing="ignore", ) extra_id = {"aud_l": 1, "missing": 111} - with _record_warnings(), pytest.warns( - RuntimeWarning, match="from event_id is not present in" + with ( + _record_warnings(), + pytest.warns(RuntimeWarning, match="from event_id is not present in"), ): plot_events( events, diff --git a/mne/viz/tests/test_raw.py b/mne/viz/tests/test_raw.py index 14233a4c98b..c1920aebf93 100644 --- a/mne/viz/tests/test_raw.py +++ b/mne/viz/tests/test_raw.py @@ -973,8 +973,9 @@ def test_plot_raw_psd(raw, raw_orig): # with channel information not available for idx in range(len(raw.info["chs"])): raw.info["chs"][idx]["loc"] = np.zeros(12) - with _record_warnings(), pytest.warns( - RuntimeWarning, match="locations not available" + with ( + _record_warnings(), + pytest.warns(RuntimeWarning, match="locations not available"), ): raw.compute_psd().plot(spatial_colors=True, average=False) # with a flat channel diff --git a/mne/viz/ui_events.py b/mne/viz/ui_events.py index b28754664d4..adad59c4be0 100644 --- a/mne/viz/ui_events.py +++ b/mne/viz/ui_events.py @@ -9,6 +9,7 @@ Authors: Marijn van Vliet """ + # License: BSD-3-Clause # Copyright the MNE-Python contributors. import contextlib diff --git a/tutorials/visualization/20_ui_events.py b/tutorials/visualization/20_ui_events.py index ce268e1d8a5..e119b5032c1 100644 --- a/tutorials/visualization/20_ui_events.py +++ b/tutorials/visualization/20_ui_events.py @@ -16,6 +16,7 @@ Since the figures on our website don't have any interaction capabilities, this example will only work properly when run in an interactive environment. """ + # Author: Marijn van Vliet # # License: BSD-3-Clause From 97b745a2ff2db3661ed64a3c54fc85abeea558cf Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Mon, 4 Mar 2024 12:52:56 -0500 Subject: [PATCH 215/405] MAINT: Fix pip-pre errors (#12478) --- tools/azure_dependencies.sh | 2 +- tools/github_actions_dependencies.sh | 10 +++------- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/tools/azure_dependencies.sh b/tools/azure_dependencies.sh index 53629f256a8..95b44858160 100755 --- a/tools/azure_dependencies.sh +++ b/tools/azure_dependencies.sh @@ -9,7 +9,7 @@ elif [ "${TEST_MODE}" == "pip-pre" ]; then # python -m pip install $STD_ARGS --only-binary ":all:" --extra-index-url "https://www.riverbankcomputing.com/pypi/simple" "PyQt6!=6.6.1,!=6.6.2" PyQt6-sip PyQt6-Qt6 "PyQt6-Qt6!=6.6.1,!=6.6.2" python -m pip install $STD_ARGS --only-binary ":all:" "PyQt6!=6.6.1,!=6.6.2" PyQt6-sip PyQt6-Qt6 "PyQt6-Qt6!=6.6.1,!=6.6.2" echo "Numpy etc." - python -m pip install $STD_ARGS --only-binary ":all:" --default-timeout=60 --extra-index-url "https://pypi.anaconda.org/scientific-python-nightly-wheels/simple" "numpy>=2.0.0.dev0" "scipy>=1.12.0.dev0" "scikit-learn>=1.5.dev0" matplotlib pillow statsmodels pyarrow + python -m pip install $STD_ARGS --only-binary ":all:" --default-timeout=60 --extra-index-url "https://pypi.anaconda.org/scientific-python-nightly-wheels/simple" "numpy<2.0.0.dev0" "scipy>=1.12.0.dev0" "scikit-learn>=1.5.dev0" matplotlib pillow statsmodels pyarrow h5py # echo "dipy" # python -m pip install $STD_ARGS --only-binary ":all:" --extra-index-url "https://pypi.anaconda.org/scipy-wheels-nightly/simple" dipy # echo "OpenMEEG" diff --git a/tools/github_actions_dependencies.sh b/tools/github_actions_dependencies.sh index 11529447b77..c10244783ee 100755 --- a/tools/github_actions_dependencies.sh +++ b/tools/github_actions_dependencies.sh @@ -30,13 +30,9 @@ else # pip install $STD_ARGS --only-binary ":all:" --default-timeout=60 --extra-index-url https://www.riverbankcomputing.com/pypi/simple "PyQt6!=6.6.1,!=6.6.2" "PyQt6-Qt6!=6.6.1,!=6.6.2" pip install $STD_ARGS --only-binary ":all:" --default-timeout=60 "PyQt6!=6.6.1,!=6.6.2" "PyQt6-Qt6!=6.6.1,!=6.6.2" echo "NumPy/SciPy/pandas etc." - pip install $STD_ARGS --only-binary ":all:" --default-timeout=60 --extra-index-url "https://pypi.anaconda.org/scientific-python-nightly-wheels/simple" "numpy>=2.0.0.dev0" "scipy>=1.12.0.dev0" "scikit-learn>=1.5.dev0" matplotlib pillow statsmodels pyarrow - # No pandas, dipy, h5py, openmeeg, python-picard (needs numexpr) until they update to NumPy 2.0 compat + pip install $STD_ARGS --only-binary ":all:" --default-timeout=60 --extra-index-url "https://pypi.anaconda.org/scientific-python-nightly-wheels/simple" "numpy<2.0.0.dev0" "scipy>=1.12.0.dev0" "scikit-learn>=1.5.dev0" matplotlib pillow statsmodels pyarrow pandas h5py + # No dipy, openmeeg, python-picard (needs numexpr) until they update to NumPy 2.0 compat INSTALL_KIND="test_extra" - # echo "dipy" - # pip install $STD_ARGS --only-binary ":all:" --default-timeout=60 --extra-index-url "https://pypi.anaconda.org/scipy-wheels-nightly/simple" dipy - # echo "H5py" - # pip install $STD_ARGS --only-binary ":all:" -f "https://7933911d6844c6c53a7d-47bd50c35cd79bd838daf386af554a83.ssl.cf2.rackcdn.com" h5py # echo "OpenMEEG" # pip install $STD_ARGS --only-binary ":all:" --extra-index-url "https://test.pypi.org/simple" openmeeg # No Numba because it forces an old NumPy version @@ -60,7 +56,7 @@ else echo "edfio" pip install $STD_ARGS git+https://github.com/the-siesta-group/edfio # Make sure we're on a NumPy 2.0 variant - python -c "import numpy as np; assert np.__version__[0] == '2', np.__version__" + # python -c "import numpy as np; assert np.__version__[0] == '2', np.__version__" fi echo "" From 795d6d9510a691e589c43c8cc2aef4edd97661d7 Mon Sep 17 00:00:00 2001 From: Liberty Hamilton Date: Mon, 4 Mar 2024 14:31:40 -0600 Subject: [PATCH 216/405] Make ECoG and sEEG size defaults more realistic (#12474) Co-authored-by: Daniel McCloy --- doc/changes/devel/12474.bugfix.rst | 1 + mne/defaults.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 doc/changes/devel/12474.bugfix.rst diff --git a/doc/changes/devel/12474.bugfix.rst b/doc/changes/devel/12474.bugfix.rst new file mode 100644 index 00000000000..875d7574f7b --- /dev/null +++ b/doc/changes/devel/12474.bugfix.rst @@ -0,0 +1 @@ +- Changed default ECoG and sEEG electrode sizes in brain plots to better reflect real world sizes, by `Liberty Hamilton`_ diff --git a/mne/defaults.py b/mne/defaults.py index b9e6702edec..31fc53299e9 100644 --- a/mne/defaults.py +++ b/mne/defaults.py @@ -235,8 +235,8 @@ eeg_scale=4e-3, eegp_scale=20e-3, eegp_height=0.1, - ecog_scale=5e-3, - seeg_scale=5e-3, + ecog_scale=2e-3, + seeg_scale=2e-3, meg_scale=1.0, # sensors are already in SI units ref_meg_scale=1.0, dbs_scale=5e-3, From 946c616b6fe65663b66cf7433064eef2ff0e2cc0 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Mon, 4 Mar 2024 17:10:04 -0500 Subject: [PATCH 217/405] MAINT: Include OpenMEEG in pre jobs (#12482) --- mne/conftest.py | 2 ++ tools/azure_dependencies.sh | 4 ++-- tools/github_actions_dependencies.sh | 6 +++--- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/mne/conftest.py b/mne/conftest.py index 0feb92b0ada..5b19dec59c5 100644 --- a/mne/conftest.py +++ b/mne/conftest.py @@ -902,6 +902,8 @@ def protect_config(): def _test_passed(request): + if _phase_report_key not in request.node.stash: + return True report = request.node.stash[_phase_report_key] return "call" in report and report["call"].outcome == "passed" diff --git a/tools/azure_dependencies.sh b/tools/azure_dependencies.sh index 95b44858160..00395c8ac67 100755 --- a/tools/azure_dependencies.sh +++ b/tools/azure_dependencies.sh @@ -12,8 +12,8 @@ elif [ "${TEST_MODE}" == "pip-pre" ]; then python -m pip install $STD_ARGS --only-binary ":all:" --default-timeout=60 --extra-index-url "https://pypi.anaconda.org/scientific-python-nightly-wheels/simple" "numpy<2.0.0.dev0" "scipy>=1.12.0.dev0" "scikit-learn>=1.5.dev0" matplotlib pillow statsmodels pyarrow h5py # echo "dipy" # python -m pip install $STD_ARGS --only-binary ":all:" --extra-index-url "https://pypi.anaconda.org/scipy-wheels-nightly/simple" dipy - # echo "OpenMEEG" - # pip install $STD_ARGS --only-binary ":all:" --extra-index-url "https://test.pypi.org/simple" openmeeg + echo "OpenMEEG" + pip install $STD_ARGS --only-binary ":all:" --extra-index-url "https://test.pypi.org/simple" "openmeeg>=2.6.0.dev4" echo "vtk" python -m pip install $STD_ARGS --only-binary ":all:" --extra-index-url "https://wheels.vtk.org" vtk echo "nilearn" diff --git a/tools/github_actions_dependencies.sh b/tools/github_actions_dependencies.sh index c10244783ee..fcb808fd812 100755 --- a/tools/github_actions_dependencies.sh +++ b/tools/github_actions_dependencies.sh @@ -31,10 +31,10 @@ else pip install $STD_ARGS --only-binary ":all:" --default-timeout=60 "PyQt6!=6.6.1,!=6.6.2" "PyQt6-Qt6!=6.6.1,!=6.6.2" echo "NumPy/SciPy/pandas etc." pip install $STD_ARGS --only-binary ":all:" --default-timeout=60 --extra-index-url "https://pypi.anaconda.org/scientific-python-nightly-wheels/simple" "numpy<2.0.0.dev0" "scipy>=1.12.0.dev0" "scikit-learn>=1.5.dev0" matplotlib pillow statsmodels pyarrow pandas h5py - # No dipy, openmeeg, python-picard (needs numexpr) until they update to NumPy 2.0 compat + # No dipy, python-picard (needs numexpr) until they update to NumPy 2.0 compat INSTALL_KIND="test_extra" - # echo "OpenMEEG" - # pip install $STD_ARGS --only-binary ":all:" --extra-index-url "https://test.pypi.org/simple" openmeeg + echo "OpenMEEG" + pip install $STD_ARGS --only-binary ":all:" --extra-index-url "https://test.pypi.org/simple" "openmeeg>=2.6.0.dev4" # No Numba because it forces an old NumPy version echo "nilearn" pip install $STD_ARGS git+https://github.com/nilearn/nilearn From 17d0c87083847f1279b5bd4a941f5daeff40a693 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Mon, 4 Mar 2024 17:11:45 -0500 Subject: [PATCH 218/405] FIX: Image --- .circleci/config.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index bca927a36d3..93874db5441 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -444,8 +444,8 @@ jobs: deploy: - machine: - image: ubuntu-2004:202111-01 + docker: + - image: cimg/base:current-22.04 steps: - attach_workspace: at: /tmp/build From ae69b03a00eb2c72ae9b13c71ae541747daf55da Mon Sep 17 00:00:00 2001 From: Dominik Welke Date: Tue, 5 Mar 2024 15:10:18 +0000 Subject: [PATCH 219/405] fix merge_asof usage in read_raw_eyelink() (#12481) --- doc/changes/devel/12481.bugfix.rst | 1 + mne/io/eyelink/_utils.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 doc/changes/devel/12481.bugfix.rst diff --git a/doc/changes/devel/12481.bugfix.rst b/doc/changes/devel/12481.bugfix.rst new file mode 100644 index 00000000000..a9108fe4040 --- /dev/null +++ b/doc/changes/devel/12481.bugfix.rst @@ -0,0 +1 @@ +- Fix reading segmented recordings with :func:`mne.io.read_raw_eyelink` by `Dominik Welke`_. \ No newline at end of file diff --git a/mne/io/eyelink/_utils.py b/mne/io/eyelink/_utils.py index cefc184c2f9..99c1e1c96f6 100644 --- a/mne/io/eyelink/_utils.py +++ b/mne/io/eyelink/_utils.py @@ -507,7 +507,7 @@ def _adjust_times( np.arange(first, last + step / 2, step), columns=[time_col] ) return pd.merge_asof( - new_times, df, on=time_col, direction="nearest", tolerance=step / 10 + new_times, df, on=time_col, direction="nearest", tolerance=step / 2 ) From 72799349bd6c4987dcef8e71adaf1a4a8cc6e061 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Wed, 6 Mar 2024 11:49:19 -0500 Subject: [PATCH 220/405] MAINT: Play nicer with themes (#12483) --- doc/changes/devel/12483.bugfix.rst | 1 + mne/viz/backends/_qt.py | 44 ++++++++++++++++++------------ mne/viz/backends/_utils.py | 7 +++-- 3 files changed, 31 insertions(+), 21 deletions(-) create mode 100644 doc/changes/devel/12483.bugfix.rst diff --git a/doc/changes/devel/12483.bugfix.rst b/doc/changes/devel/12483.bugfix.rst new file mode 100644 index 00000000000..601bf94838c --- /dev/null +++ b/doc/changes/devel/12483.bugfix.rst @@ -0,0 +1 @@ +Improve compatibility with other Qt-based GUIs by handling theme icons better, by `Eric Larson`_. diff --git a/mne/viz/backends/_qt.py b/mne/viz/backends/_qt.py index 35cdc4de502..6e59c2b6c20 100644 --- a/mne/viz/backends/_qt.py +++ b/mne/viz/backends/_qt.py @@ -112,6 +112,7 @@ _take_3d_screenshot, # noqa: F401 ) from ._utils import ( + _ICONS_PATH, _init_mne_qtapp, _qt_app_exec, _qt_detect_theme, @@ -276,13 +277,13 @@ def __init__(self, value, callback, icon=None): self.setText(value) self.released.connect(callback) if icon: - self.setIcon(QIcon.fromTheme(icon)) + self.setIcon(_qicon(icon)) def _click(self): self.click() def _set_icon(self, icon): - self.setIcon(QIcon.fromTheme(icon)) + self.setIcon(_qicon(icon)) class _Slider(QSlider, _AbstractSlider, _Widget, metaclass=_BaseWidget): @@ -474,16 +475,16 @@ def __init__(self, value, rng, callback): self._slider.valueChanged.connect(callback) self._nav_hbox = QHBoxLayout() self._play_button = QPushButton() - self._play_button.setIcon(QIcon.fromTheme("play")) + self._play_button.setIcon(_qicon("play")) self._nav_hbox.addWidget(self._play_button) self._pause_button = QPushButton() - self._pause_button.setIcon(QIcon.fromTheme("pause")) + self._pause_button.setIcon(_qicon("pause")) self._nav_hbox.addWidget(self._pause_button) self._reset_button = QPushButton() - self._reset_button.setIcon(QIcon.fromTheme("reset")) + self._reset_button.setIcon(_qicon("reset")) self._nav_hbox.addWidget(self._reset_button) self._loop_button = QPushButton() - self._loop_button.setIcon(QIcon.fromTheme("restore")) + self._loop_button.setIcon(_qicon("restore")) self._loop_button.setStyleSheet("background-color : lightgray;") self._loop_button._checked = True @@ -1494,18 +1495,18 @@ def closeEvent(event): self._window.closeEvent = closeEvent def _window_load_icons(self): - self._icons["help"] = QIcon.fromTheme("help") - self._icons["play"] = QIcon.fromTheme("play") - self._icons["pause"] = QIcon.fromTheme("pause") - self._icons["reset"] = QIcon.fromTheme("reset") - self._icons["scale"] = QIcon.fromTheme("scale") - self._icons["clear"] = QIcon.fromTheme("clear") - self._icons["movie"] = QIcon.fromTheme("movie") - self._icons["restore"] = QIcon.fromTheme("restore") - self._icons["screenshot"] = QIcon.fromTheme("screenshot") - self._icons["visibility_on"] = QIcon.fromTheme("visibility_on") - self._icons["visibility_off"] = QIcon.fromTheme("visibility_off") - self._icons["folder"] = QIcon.fromTheme("folder") + self._icons["help"] = _qicon("help") + self._icons["play"] = _qicon("play") + self._icons["pause"] = _qicon("pause") + self._icons["reset"] = _qicon("reset") + self._icons["scale"] = _qicon("scale") + self._icons["clear"] = _qicon("clear") + self._icons["movie"] = _qicon("movie") + self._icons["restore"] = _qicon("restore") + self._icons["screenshot"] = _qicon("screenshot") + self._icons["visibility_on"] = _qicon("visibility_on") + self._icons["visibility_off"] = _qicon("visibility_off") + self._icons["folder"] = _qicon("folder") def _window_clean(self): self.figure._plotter = None @@ -1844,3 +1845,10 @@ def _testing_context(interactive): finally: pyvista.OFF_SCREEN = orig_offscreen renderer.MNE_3D_BACKEND_TESTING = orig_testing + + +def _qicon(name): + # Get icon from theme with a file fallback + return QIcon.fromTheme( + name, QIcon(str(_ICONS_PATH / "light" / "actions" / f"{name}.svg")) + ) diff --git a/mne/viz/backends/_utils.py b/mne/viz/backends/_utils.py index e67656a25ed..56405bc3cdb 100644 --- a/mne/viz/backends/_utils.py +++ b/mne/viz/backends/_utils.py @@ -33,6 +33,7 @@ "notebook", ) ALLOWED_QUIVER_MODES = ("2darrow", "arrow", "cone", "cylinder", "sphere", "oct") +_ICONS_PATH = Path(__file__).parents[2] / "icons" def _get_colormap_from_array( @@ -89,9 +90,9 @@ def _alpha_blend_background(ctable, background_color): def _qt_init_icons(): from qtpy.QtGui import QIcon - icons_path = str(Path(__file__).parents[2] / "icons") - QIcon.setThemeSearchPaths([icons_path]) - return icons_path + QIcon.setThemeSearchPaths([str(_ICONS_PATH)] + QIcon.themeSearchPaths()) + QIcon.setFallbackThemeName("light") + return str(_ICONS_PATH) @contextmanager From a622a467074ea9ba0f1d1168c4ec3b8c6af74323 Mon Sep 17 00:00:00 2001 From: Daniel McCloy Date: Sat, 9 Mar 2024 13:39:44 -0600 Subject: [PATCH 221/405] fixes found by sphinx-lint (#12487) --- doc/_includes/channel_interpolation.rst | 4 +- doc/_includes/forward.rst | 70 ++++++++++----------- doc/_includes/ssp.rst | 4 +- doc/api/events.rst | 2 +- doc/api/file_io.rst | 2 +- doc/changes/v0.10.rst | 2 +- doc/changes/v0.12.rst | 70 ++++++++++----------- doc/changes/v0.13.rst | 2 +- doc/changes/v0.15.rst | 2 +- doc/changes/v0.17.rst | 84 ++++++++++++------------- doc/changes/v1.2.rst | 2 +- doc/documentation/datasets.rst | 2 +- 12 files changed, 123 insertions(+), 123 deletions(-) diff --git a/doc/_includes/channel_interpolation.rst b/doc/_includes/channel_interpolation.rst index 4639604af58..e90a763d214 100644 --- a/doc/_includes/channel_interpolation.rst +++ b/doc/_includes/channel_interpolation.rst @@ -59,7 +59,7 @@ where :math:`G_{ds} \in R^{M \times N}` computes :math:`g_{m}(\boldsymbol{r_i}, To interpolate bad channels, one can simply do: - >>> evoked.interpolate_bads(reset_bads=False) # doctest: +SKIP + >>> evoked.interpolate_bads(reset_bads=False) # doctest: +SKIP and the bad channel will be fixed. @@ -67,4 +67,4 @@ and the bad channel will be fixed. .. topic:: Examples: - * :ref:`ex-interpolate-bad-channels` + * :ref:`ex-interpolate-bad-channels` diff --git a/doc/_includes/forward.rst b/doc/_includes/forward.rst index f92632f8220..d04eeba7b5b 100644 --- a/doc/_includes/forward.rst +++ b/doc/_includes/forward.rst @@ -130,26 +130,26 @@ transformation symbols (:math:`T_x`) indicate the transformations actually present in the FreeSurfer files. Generally, .. math:: \begin{bmatrix} - x_2 \\ - y_2 \\ - z_2 \\ - 1 - \end{bmatrix} = T_{12} \begin{bmatrix} - x_1 \\ - y_1 \\ - z_1 \\ - 1 - \end{bmatrix} = \begin{bmatrix} - R_{11} & R_{12} & R_{13} & x_0 \\ - R_{21} & R_{22} & R_{23} & y_0 \\ - R_{31} & R_{32} & R_{33} & z_0 \\ - 0 & 0 & 0 & 1 - \end{bmatrix} \begin{bmatrix} - x_1 \\ - y_1 \\ - z_1 \\ - 1 - \end{bmatrix}\ , + x_2 \\ + y_2 \\ + z_2 \\ + 1 + \end{bmatrix} = T_{12} \begin{bmatrix} + x_1 \\ + y_1 \\ + z_1 \\ + 1 + \end{bmatrix} = \begin{bmatrix} + R_{11} & R_{12} & R_{13} & x_0 \\ + R_{21} & R_{22} & R_{23} & y_0 \\ + R_{31} & R_{32} & R_{33} & z_0 \\ + 0 & 0 & 0 & 1 + \end{bmatrix} \begin{bmatrix} + x_1 \\ + y_1 \\ + z_1 \\ + 1 + \end{bmatrix}\ , where :math:`x_k`, :math:`y_k`,and :math:`z_k` are the location coordinates in two coordinate systems, :math:`T_{12}` is the coordinate transformation from @@ -161,20 +161,20 @@ files produced by FreeSurfer and MNE. The fixed transformations :math:`T_-` and :math:`T_+` are: .. math:: T_{-} = \begin{bmatrix} - 0.99 & 0 & 0 & 0 \\ - 0 & 0.9688 & 0.042 & 0 \\ - 0 & -0.0485 & 0.839 & 0 \\ - 0 & 0 & 0 & 1 - \end{bmatrix} + 0.99 & 0 & 0 & 0 \\ + 0 & 0.9688 & 0.042 & 0 \\ + 0 & -0.0485 & 0.839 & 0 \\ + 0 & 0 & 0 & 1 + \end{bmatrix} and .. math:: T_{+} = \begin{bmatrix} - 0.99 & 0 & 0 & 0 \\ - 0 & 0.9688 & 0.046 & 0 \\ - 0 & -0.0485 & 0.9189 & 0 \\ - 0 & 0 & 0 & 1 - \end{bmatrix} + 0.99 & 0 & 0 & 0 \\ + 0 & 0.9688 & 0.046 & 0 \\ + 0 & -0.0485 & 0.9189 & 0 \\ + 0 & 0 & 0 & 1 + \end{bmatrix} .. note:: This section does not discuss the transformation between the MRI voxel @@ -352,11 +352,11 @@ coordinates (:math:`r_D`) by where .. math:: T = \begin{bmatrix} - e_x & 0 \\ - e_y & 0 \\ - e_z & 0 \\ - r_{0D} & 1 - \end{bmatrix}\ . + e_x & 0 \\ + e_y & 0 \\ + e_z & 0 \\ + r_{0D} & 1 + \end{bmatrix}\ . Calculation of the magnetic field --------------------------------- diff --git a/doc/_includes/ssp.rst b/doc/_includes/ssp.rst index 1bc860d15db..40b25a237db 100644 --- a/doc/_includes/ssp.rst +++ b/doc/_includes/ssp.rst @@ -101,12 +101,12 @@ The EEG average reference is the mean signal over all the sensors. It is typical in EEG analysis to subtract the average reference from all the sensor signals :math:`b^{1}(t), ..., b^{n}(t)`. That is: -.. math:: {b}^{j}_{s}(t) = b^{j}(t) - \frac{1}{n}\sum_{k}{b^k(t)} +.. math:: {b}^{j}_{s}(t) = b^{j}(t) - \frac{1}{n}\sum_{k}{b^k(t)} :name: eeg_proj where the noise term :math:`b_{n}^{j}(t)` is given by -.. math:: b_{n}^{j}(t) = \frac{1}{n}\sum_{k}{b^k(t)} +.. math:: b_{n}^{j}(t) = \frac{1}{n}\sum_{k}{b^k(t)} :name: noise_term Thus, the projector vector :math:`P_{\perp}` will be given by diff --git a/doc/api/events.rst b/doc/api/events.rst index f9447741a09..3f7159a22d5 100644 --- a/doc/api/events.rst +++ b/doc/api/events.rst @@ -55,4 +55,4 @@ Events average_movements combine_event_ids equalize_epoch_counts - make_metadata \ No newline at end of file + make_metadata diff --git a/doc/api/file_io.rst b/doc/api/file_io.rst index 3b43de6ce64..2da9059deb3 100644 --- a/doc/api/file_io.rst +++ b/doc/api/file_io.rst @@ -63,4 +63,4 @@ Base class: :toctree: ../generated/ :template: autosummary/class_no_members.rst - BaseEpochs \ No newline at end of file + BaseEpochs diff --git a/doc/changes/v0.10.rst b/doc/changes/v0.10.rst index 6a0c3322e88..ac4f2e42857 100644 --- a/doc/changes/v0.10.rst +++ b/doc/changes/v0.10.rst @@ -91,7 +91,7 @@ BUG - Fix dropping of events after downsampling stim channels by `Marijn van Vliet`_ -- Fix scaling in :func:``mne.viz.utils._setup_vmin_vmax`` by `Jaakko Leppakangas`_ +- Fix scaling in ``mne.viz.utils._setup_vmin_vmax`` by `Jaakko Leppakangas`_ - Fix order of component selection in :class:`mne.decoding.CSP` by `Clemens Brunner`_ diff --git a/doc/changes/v0.12.rst b/doc/changes/v0.12.rst index cf01f8ff62c..b3b7aba1a39 100644 --- a/doc/changes/v0.12.rst +++ b/doc/changes/v0.12.rst @@ -129,7 +129,7 @@ BUG - Fix bug in :func:`mne.io.Raw.save` where, in rare cases, automatically split files could end up writing an extra empty file that wouldn't be read properly by `Eric Larson`_ -- Fix :class:``mne.realtime.StimServer`` by removing superfluous argument ``ip`` used while initializing the object by `Mainak Jas`_. +- Fix ``mne.realtime.StimServer`` by removing superfluous argument ``ip`` used while initializing the object by `Mainak Jas`_. - Fix removal of projectors in :func:`mne.preprocessing.maxwell_filter` in ``st_only=True`` mode by `Eric Larson`_ @@ -175,37 +175,37 @@ Authors The committer list for this release is the following (preceded by number of commits): -* 348 Eric Larson -* 347 Jaakko Leppakangas -* 157 Alexandre Gramfort -* 139 Jona Sassenhagen -* 67 Jean-Remi King -* 32 Chris Holdgraf -* 31 Denis A. Engemann -* 30 Mainak Jas -* 16 Christopher J. Bailey -* 13 Marijn van Vliet -* 10 Mark Wronkiewicz -* 9 Teon Brooks -* 9 kaichogami -* 8 Clément Moutard -* 5 Camilo Lamus -* 5 mmagnuski -* 4 Christian Brodbeck -* 4 Daniel McCloy -* 4 Yousra Bekhti -* 3 Fede Raimondo -* 1 Jussi Nurminen -* 1 MartinBaBer -* 1 Mikolaj Magnuski -* 1 Natalie Klein -* 1 Niklas Wilming -* 1 Richard Höchenberger -* 1 Sagun Pai -* 1 Sourav Singh -* 1 Tom Dupré la Tour -* 1 jona-sassenhagen@ -* 1 kambysese -* 1 pbnsilva -* 1 sviter -* 1 zuxfoucault +* 348 Eric Larson +* 347 Jaakko Leppakangas +* 157 Alexandre Gramfort +* 139 Jona Sassenhagen +* 67 Jean-Remi King +* 32 Chris Holdgraf +* 31 Denis A. Engemann +* 30 Mainak Jas +* 16 Christopher J. Bailey +* 13 Marijn van Vliet +* 10 Mark Wronkiewicz +* 9 Teon Brooks +* 9 kaichogami +* 8 Clément Moutard +* 5 Camilo Lamus +* 5 mmagnuski +* 4 Christian Brodbeck +* 4 Daniel McCloy +* 4 Yousra Bekhti +* 3 Fede Raimondo +* 1 Jussi Nurminen +* 1 MartinBaBer +* 1 Mikolaj Magnuski +* 1 Natalie Klein +* 1 Niklas Wilming +* 1 Richard Höchenberger +* 1 Sagun Pai +* 1 Sourav Singh +* 1 Tom Dupré la Tour +* 1 jona-sassenhagen@ +* 1 kambysese +* 1 pbnsilva +* 1 sviter +* 1 zuxfoucault diff --git a/doc/changes/v0.13.rst b/doc/changes/v0.13.rst index 425ba4c76a1..aee297d9d2d 100644 --- a/doc/changes/v0.13.rst +++ b/doc/changes/v0.13.rst @@ -198,7 +198,7 @@ API - Deprecated ``mne.time_frequency.cwt_morlet`` and ``mne.time_frequency.single_trial_power`` in favour of :func:`mne.time_frequency.tfr_morlet` with parameter average=False, by `Jean-Remi King`_ and `Alex Gramfort`_ -- Add argument ``mask_type`` to func:`mne.read_events` and func:`mne.find_events` to support MNE-C style of trigger masking by `Teon Brooks`_ and `Eric Larson`_ +- Add argument ``mask_type`` to :func:`mne.read_events` and :func:`mne.find_events` to support MNE-C style of trigger masking by `Teon Brooks`_ and `Eric Larson`_ - Extended Infomax is now the new default in :func:`mne.preprocessing.infomax` (``extended=True``), by `Clemens Brunner`_ diff --git a/doc/changes/v0.15.rst b/doc/changes/v0.15.rst index ada8180d4ac..e2de7301973 100644 --- a/doc/changes/v0.15.rst +++ b/doc/changes/v0.15.rst @@ -226,7 +226,7 @@ API - ``mne.viz.decoding.plot_gat_times``, ``mne.viz.decoding.plot_gat_matrix`` are now deprecated. Use matplotlib instead as shown in the examples, by `Jean-Remi King`_ and `Alex Gramfort`_ -- Add ``norm_trace`` parameter to control single-epoch covariance normalization in :class:mne.decoding.CSP, by `Jean-Remi King`_ +- Add ``norm_trace`` parameter to control single-epoch covariance normalization in :class:`mne.decoding.CSP`, by `Jean-Remi King`_ - Allow passing a list of channel names as ``show_names`` in function :func:`mne.viz.plot_sensors` and methods :meth:`mne.Evoked.plot_sensors`, :meth:`mne.Epochs.plot_sensors` and :meth:`mne.io.Raw.plot_sensors` to show only a subset of channel names by `Jaakko Leppakangas`_ diff --git a/doc/changes/v0.17.rst b/doc/changes/v0.17.rst index 40896b6f383..49e722c584d 100644 --- a/doc/changes/v0.17.rst +++ b/doc/changes/v0.17.rst @@ -234,7 +234,7 @@ API In 0.19 The ``stim_channel`` keyword arguments will be removed from ``read_raw_...`` functions. -- Calling :meth:``mne.io.pick.pick_info`` removing channels that are needed by compensation matrices (``info['comps']``) no longer raises ``RuntimeException`` but instead logs an info level message. By `Luke Bloy`_ +- Calling ``mne.io.pick.pick_info`` removing channels that are needed by compensation matrices (``info['comps']``) no longer raises ``RuntimeException`` but instead logs an info level message. By `Luke Bloy`_ - :meth:`mne.Epochs.save` now has the parameter ``fmt`` to specify the desired format (precision) saving epoched data, by `Stefan Repplinger`_, `Eric Larson`_ and `Alex Gramfort`_ @@ -274,44 +274,44 @@ Authors People who contributed to this release (in alphabetical order): -* Alexandre Gramfort -* Antoine Gauthier -* Britta Westner -* Christian Brodbeck -* Clemens Brunner -* Daniel McCloy -* David Sabbagh -* Denis A. Engemann -* Eric Larson -* Ezequiel Mikulan -* Henrich Kolkhorst -* Hubert Banville -* Jasper J.F. van den Bosch -* Jen Evans -* Joan Massich -* Johan van der Meer -* Jona Sassenhagen -* Kambiz Tavabi -* Lorenz Esch -* Luke Bloy -* Mainak Jas -* Manu Sutela -* Marcin Koculak -* Marijn van Vliet -* Mikolaj Magnuski -* Peter J. Molfese -* Sam Perry -* Sara Sommariva -* Sergey Antopolskiy -* Sheraz Khan -* Stefan Appelhoff -* Stefan Repplinger -* Steven Bethard -* Teekuningas -* Teon Brooks -* Thomas Hartmann -* Thomas Jochmann -* Tom Dupré la Tour -* Tristan Stenner -* buildqa -* jeythekey +* Alexandre Gramfort +* Antoine Gauthier +* Britta Westner +* Christian Brodbeck +* Clemens Brunner +* Daniel McCloy +* David Sabbagh +* Denis A. Engemann +* Eric Larson +* Ezequiel Mikulan +* Henrich Kolkhorst +* Hubert Banville +* Jasper J.F. van den Bosch +* Jen Evans +* Joan Massich +* Johan van der Meer +* Jona Sassenhagen +* Kambiz Tavabi +* Lorenz Esch +* Luke Bloy +* Mainak Jas +* Manu Sutela +* Marcin Koculak +* Marijn van Vliet +* Mikolaj Magnuski +* Peter J. Molfese +* Sam Perry +* Sara Sommariva +* Sergey Antopolskiy +* Sheraz Khan +* Stefan Appelhoff +* Stefan Repplinger +* Steven Bethard +* Teekuningas +* Teon Brooks +* Thomas Hartmann +* Thomas Jochmann +* Tom Dupré la Tour +* Tristan Stenner +* buildqa +* jeythekey diff --git a/doc/changes/v1.2.rst b/doc/changes/v1.2.rst index b6a8b5a8edf..e292b472b03 100644 --- a/doc/changes/v1.2.rst +++ b/doc/changes/v1.2.rst @@ -63,7 +63,7 @@ Bugs API changes ~~~~~~~~~~~ -- In meth:`mne.Evoked.plot`, the default value of the ``spatial_colors`` parameter has been changed to ``'auto'``, which will use spatial colors if channel locations are available (:gh:`11201` by :newcontrib:`Hüseyin Orkun Elmas` and `Daniel McCloy`_) +- In :meth:`mne.Evoked.plot`, the default value of the ``spatial_colors`` parameter has been changed to ``'auto'``, which will use spatial colors if channel locations are available (:gh:`11201` by :newcontrib:`Hüseyin Orkun Elmas` and `Daniel McCloy`_) - Starting with this release we now follow the Python convention of using ``FutureWarning`` instead of ``DeprecationWarning`` to signal user-facing changes to our API (:gh:`11120` by `Daniel McCloy`_) - The ``names`` parameter of :func:`mne.viz.plot_arrowmap` and :func:`mne.viz.plot_regression_weights` has been deprecated; sensor names will be automatically drawn from the ``info_from`` or ``model`` parameter (respectively), and can be hidden, shown, or altered via the ``show_names`` parameter (:gh:`11123` by `Daniel McCloy`_) - The ``bands`` parameter of :meth:`mne.Epochs.plot_psd_topomap` now accepts :class:`dict` input; legacy :class:`tuple` input is supported, but discouraged for new code (:gh:`11050` by `Daniel McCloy`_) diff --git a/doc/documentation/datasets.rst b/doc/documentation/datasets.rst index 063d06da363..70da39cccd8 100644 --- a/doc/documentation/datasets.rst +++ b/doc/documentation/datasets.rst @@ -516,7 +516,7 @@ Contains both EEG (EGI) and eye-tracking (ASCII format) data recorded from a pupillary light reflex experiment, stored in separate files. 1 participant fixated on the screen while short light flashes appeared. Event onsets were recorded by a photodiode attached to the screen and were sent to both the EEG and eye-tracking -systems. +systems. .. topic:: Examples From d7fdcb004ae5dc95d86f5be598bd87677ad1c251 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Mar 2024 16:38:34 +0000 Subject: [PATCH 222/405] Bump davidslusser/actions_python_bandit from 1.0.0 to 1.0.1 (#12488) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 5abfae43a3b..908555af797 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -30,7 +30,7 @@ jobs: needs: style runs-on: ubuntu-latest steps: - - uses: davidslusser/actions_python_bandit@v1.0.0 + - uses: davidslusser/actions_python_bandit@v1.0.1 with: src: "mne" options: "-c pyproject.toml -ll -r" From 90067893e330c1941c055be22bc5f442ad320ec3 Mon Sep 17 00:00:00 2001 From: Mathieu Scheltienne Date: Mon, 11 Mar 2024 20:14:57 +0100 Subject: [PATCH 223/405] Fix cleaning of channel names which can create duplicate for non vectorview or CTF systems (#12489) --- doc/changes/devel/12489.bugfix.rst | 1 + mne/utils/misc.py | 11 ++++++----- mne/utils/tests/test_misc.py | 15 ++++++++++++++- 3 files changed, 21 insertions(+), 6 deletions(-) create mode 100644 doc/changes/devel/12489.bugfix.rst diff --git a/doc/changes/devel/12489.bugfix.rst b/doc/changes/devel/12489.bugfix.rst new file mode 100644 index 00000000000..9172ec64f7e --- /dev/null +++ b/doc/changes/devel/12489.bugfix.rst @@ -0,0 +1 @@ +Fix cleaning of channel names for non vectorview or CTF dataset including whitespaces or dash in their channel names, by `Mathieu Scheltienne`_. diff --git a/mne/utils/misc.py b/mne/utils/misc.py index 2cebf8e5450..a86688ca2a7 100644 --- a/mne/utils/misc.py +++ b/mne/utils/misc.py @@ -269,9 +269,8 @@ def running_subprocess(command, after="wait", verbose=None, *args, **kwargs): def _clean_names(names, remove_whitespace=False, before_dash=True): """Remove white-space on topo matching. - This function handles different naming - conventions for old VS new VectorView systems (`remove_whitespace`). - Also it allows to remove system specific parts in CTF channel names + This function handles different naming conventions for old VS new VectorView systems + (`remove_whitespace`) and removes system specific parts in CTF channel names (`before_dash`). Usage @@ -281,7 +280,6 @@ def _clean_names(names, remove_whitespace=False, before_dash=True): # for CTF ch_names = _clean_names(epochs.ch_names, before_dash=True) - """ cleaned = [] for name in names: @@ -292,7 +290,10 @@ def _clean_names(names, remove_whitespace=False, before_dash=True): if name.endswith("_v"): name = name[:-2] cleaned.append(name) - + if len(set(cleaned)) != len(names): + # this was probably not a VectorView or CTF dataset, and we now broke the + # dataset by creating duplicates, so let's use the original channel names. + return names return cleaned diff --git a/mne/utils/tests/test_misc.py b/mne/utils/tests/test_misc.py index 06b29964dd1..4168101fab3 100644 --- a/mne/utils/tests/test_misc.py +++ b/mne/utils/tests/test_misc.py @@ -8,7 +8,7 @@ import pytest import mne -from mne.utils import catch_logging, run_subprocess, sizeof_fmt +from mne.utils import _clean_names, catch_logging, run_subprocess, sizeof_fmt def test_sizeof_fmt(): @@ -144,3 +144,16 @@ def remove_traceback(log): other = stdout assert std == want assert other == "" + + +def test_clean_names(): + """Test cleaning names on OPM dataset. + + This channel name list is a subset from a user OPM dataset reported on the forum + https://mne.discourse.group/t/error-when-trying-to-plot-projectors-ssp/8456 + where the function _clean_names ended up creating a duplicate channel name L108_bz. + """ + ch_names = ["R305_bz-s2", "L108_bz-s77", "R112_bz-s109", "L108_bz-s110"] + ch_names_clean = _clean_names(ch_names, before_dash=True) + assert ch_names == ch_names_clean + assert len(set(ch_names_clean)) == len(ch_names_clean) From b752d2a3cd247df18371eae6d8c29ebb8a938b87 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Tue, 12 Mar 2024 16:59:13 -0400 Subject: [PATCH 224/405] MAINT: Improve qdarkstyle logic (#12491) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- README.rst | 6 +- doc/changes/devel/12491.dependency.rst | 1 + mne/conftest.py | 2 + mne/gui/tests/test_gui_api.py | 10 +-- mne/viz/backends/_utils.py | 107 ++++++++----------------- mne/viz/backends/tests/test_utils.py | 15 +--- 6 files changed, 42 insertions(+), 99 deletions(-) create mode 100644 doc/changes/devel/12491.dependency.rst diff --git a/README.rst b/README.rst index 433c6a1d82f..806f5469e1d 100644 --- a/README.rst +++ b/README.rst @@ -88,12 +88,12 @@ For full functionality, some functions require: - `scikit-learn `__ ≥ 1.0 - `Joblib `__ ≥ 0.15 (for parallelization) - `mne-qt-browser `__ ≥ 0.1 (for fast raw data visualization) -- `Qt `__ ≥ 5.12 via one of the following bindings (for fast raw data visualization and interactive 3D visualization): +- `Qt `__ ≥ 5.15 via one of the following bindings (for fast raw data visualization and interactive 3D visualization): - `PyQt6 `__ ≥ 6.0 - `PySide6 `__ ≥ 6.0 - - `PyQt5 `__ ≥ 5.12 - - `PySide2 `__ ≥ 5.12 + - `PyQt5 `__ ≥ 5.15 + - `PySide2 `__ ≥ 5.15 - `Numba `__ ≥ 0.54.0 - `NiBabel `__ ≥ 3.2.1 diff --git a/doc/changes/devel/12491.dependency.rst b/doc/changes/devel/12491.dependency.rst new file mode 100644 index 00000000000..423082320ca --- /dev/null +++ b/doc/changes/devel/12491.dependency.rst @@ -0,0 +1 @@ +The minimum supported version of Qt bindings is 5.15, by `Eric Larson`_. diff --git a/mne/conftest.py b/mne/conftest.py index 5b19dec59c5..2d153f92f40 100644 --- a/mne/conftest.py +++ b/mne/conftest.py @@ -963,6 +963,8 @@ def pytest_sessionfinish(session, exitstatus): # get the number to print files = defaultdict(lambda: 0.0) for item in session.items: + if _phase_report_key not in item.stash: + continue report = item.stash[_phase_report_key] dur = sum(x.duration for x in report.values()) parts = Path(item.nodeid.split(":")[0]).parts diff --git a/mne/gui/tests/test_gui_api.py b/mne/gui/tests/test_gui_api.py index 004c670a5ca..ae04124dd14 100644 --- a/mne/gui/tests/test_gui_api.py +++ b/mne/gui/tests/test_gui_api.py @@ -11,10 +11,9 @@ pytest.importorskip("nibabel") -def test_gui_api(renderer_notebook, nbexec, *, n_warn=0, backend="qt"): +def test_gui_api(renderer_notebook, nbexec, *, backend="qt"): """Test GUI API.""" import contextlib - import sys import warnings import mne @@ -25,7 +24,6 @@ def test_gui_api(renderer_notebook, nbexec, *, n_warn=0, backend="qt"): except Exception: # Notebook standalone mode backend = "notebook" - n_warn = 0 # nbexec does not expose renderer_notebook so I use a # temporary variable to synchronize the tests if backend == "notebook": @@ -44,8 +42,7 @@ def test_gui_api(renderer_notebook, nbexec, *, n_warn=0, backend="qt"): with mne.utils._record_warnings() as w: renderer._window_set_theme("dark") w = [ww for ww in w if "is not yet supported" in str(ww.message)] - if sys.platform != "darwin": # sometimes this is fine - assert len(w) == n_warn, [ww.message for ww in w] + assert len(w) == 0, [ww.message for ww in w] # window without 3d plotter if backend == "qt": @@ -387,10 +384,9 @@ def _check_widget_trigger( def test_gui_api_qt(renderer_interactive_pyvistaqt): """Test GUI API with the Qt backend.""" _, api = _check_qt_version(return_api=True) - n_warn = int(api in ("PySide6", "PyQt6")) # TODO: After merging https://github.com/mne-tools/mne-python/pull/11567 # The Qt CI run started failing about 50% of the time, so let's skip this # for now. if api == "PySide6": pytest.skip("PySide6 causes segfaults on CIs sometimes") - test_gui_api(None, None, n_warn=n_warn, backend="qt") + test_gui_api(None, None, backend="qt") diff --git a/mne/viz/backends/_utils.py b/mne/viz/backends/_utils.py index 56405bc3cdb..123546db035 100644 --- a/mne/viz/backends/_utils.py +++ b/mne/viz/backends/_utils.py @@ -274,84 +274,42 @@ def _qt_detect_theme(): def _qt_get_stylesheet(theme): _validate_type(theme, ("path-like",), "theme") theme = str(theme) - orig_theme = theme - system_theme = None - stylesheet = "" - extra_msg = "" - if theme == "auto": - theme = system_theme = _qt_detect_theme() - if theme in ("dark", "light"): - if system_theme is None: - system_theme = _qt_detect_theme() - qt_version, api = _check_qt_version(return_api=True) - # On macOS, we shouldn't need to set anything when the requested theme - # matches that of the current OS state - if sys.platform == "darwin": - extra_msg = f"when in {system_theme} mode on macOS" - # But before 5.13, we need to patch some mistakes - if sys.platform == "darwin" and theme == system_theme: - if theme == "dark" and _compare_version(qt_version, "<", "5.13"): - # Taken using "Digital Color Meter" on macOS 12.2.1 looking at - # Meld, and also adapting (MIT-licensed) - # https://github.com/ColinDuquesnoy/QDarkStyleSheet/blob/master/qdarkstyle/dark/style.qss # noqa: E501 - # Something around rgb(51, 51, 51) worked as the bgcolor here, - # but it's easy enough just to set it transparent and inherit - # the bgcolor of the window (which is the same). We also take - # the separator images from QDarkStyle (MIT). - icons_path = _qt_init_icons() - stylesheet = """\ -QStatusBar { - border: 1px solid rgb(76, 76, 75); - background: transparent; -} -QStatusBar QLabel { - background: transparent; -} -QToolBar { - background-color: transparent; - border-bottom: 1px solid rgb(99, 99, 99); -} -QToolBar::separator:horizontal { - width: 16px; - image: url("%(icons_path)s/toolbar_separator_horizontal@2x.png"); -} -QToolBar::separator:vertical { - height: 16px; - image: url("%(icons_path)s/toolbar_separator_vertical@2x.png"); -} -QToolBar::handle:horizontal { - width: 16px; - image: url("%(icons_path)s/toolbar_move_horizontal@2x.png"); -} -QToolBar::handle:vertical { - height: 16px; - image: url("%(icons_path)s/toolbar_move_vertical@2x.png"); -} -""" % dict(icons_path=icons_path) + stylesheet = "" # no stylesheet + if theme in ("auto", "dark", "light"): + if theme == "auto": + return stylesheet + assert theme in ("dark", "light") + system_theme = _qt_detect_theme() + if theme == system_theme: + return stylesheet + _, api = _check_qt_version(return_api=True) + # On macOS or Qt 6, we shouldn't need to set anything when the requested + # theme matches that of the current OS state + try: + import qdarkstyle + except ModuleNotFoundError: + logger.info( + f'To use {theme} mode when in {system_theme} mode, "qdarkstyle" has' + "to be installed! You can install it with:\n" + "pip install qdarkstyle\n" + ) else: - # Here we are on non-macOS (or on macOS but our sys theme does not - # match the requested theme) - if api in ("PySide6", "PyQt6"): - if orig_theme != "auto" and not (theme == system_theme == "light"): - warn( - f"Setting theme={repr(theme)} is not yet supported " - f"for {api} in qdarkstyle, it will be ignored" - ) + if api in ("PySide6", "PyQt6") and _compare_version( + qdarkstyle.__version__, "<", "3.2.3" + ): + warn( + f"Setting theme={repr(theme)} is not supported for {api} in " + f"qdarkstyle {qdarkstyle.__version__}, it will be ignored. " + "Consider upgrading qdarkstyle to >=3.2.3." + ) else: - try: - import qdarkstyle - except ModuleNotFoundError: - logger.info( - f'To use {theme} mode{extra_msg}, "qdarkstyle" has to ' - "be installed! You can install it with:\n" - "pip install qdarkstyle\n" - ) - else: - klass = getattr( + stylesheet = qdarkstyle.load_stylesheet( + getattr( getattr(qdarkstyle, theme).palette, f"{theme.capitalize()}Palette", ) - stylesheet = qdarkstyle.load_stylesheet(klass) + ) + return stylesheet else: try: file = open(theme) @@ -363,8 +321,7 @@ def _qt_get_stylesheet(theme): else: with file as fid: stylesheet = fid.read() - - return stylesheet + return stylesheet def _should_raise_window(): diff --git a/mne/viz/backends/tests/test_utils.py b/mne/viz/backends/tests/test_utils.py index 196eb030cea..26636004026 100644 --- a/mne/viz/backends/tests/test_utils.py +++ b/mne/viz/backends/tests/test_utils.py @@ -8,7 +8,6 @@ import platform from colorsys import rgb_to_hls -from contextlib import nullcontext import numpy as np import pytest @@ -66,19 +65,7 @@ def test_theme_colors(pg_backend, theme, monkeypatch, tmp_path): monkeypatch.setattr(darkdetect, "theme", lambda: "light") raw = RawArray(np.zeros((1, 1000)), create_info(1, 1000.0, "eeg")) _, api = _check_qt_version(return_api=True) - if api in ("PyQt6", "PySide6"): - if theme == "dark": # we force darkdetect to say the sys is light - ctx = pytest.warns(RuntimeWarning, match="not yet supported") - else: - ctx = nullcontext() - return_early = True - else: - ctx = nullcontext() - return_early = False - with ctx: - fig = raw.plot(theme=theme) - if return_early: - return # we could add a ton of conditionals below, but KISS + fig = raw.plot(theme=theme) is_dark = _qt_is_dark(fig) # on Darwin these checks get complicated, so don't bother for now if platform.system() == "Darwin": From b0ac8a3650a6094ed6efe5ef548aa0ad31513711 Mon Sep 17 00:00:00 2001 From: Clemens Brunner Date: Wed, 13 Mar 2024 13:08:21 +0100 Subject: [PATCH 225/405] Do not set browse raw icon when app icon already exists (#12494) --- mne/viz/backends/_utils.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/mne/viz/backends/_utils.py b/mne/viz/backends/_utils.py index 123546db035..6045e9eeac8 100644 --- a/mne/viz/backends/_utils.py +++ b/mne/viz/backends/_utils.py @@ -181,7 +181,11 @@ def _init_mne_qtapp(enable_icon=True, pg_app=False, splash=False): if enable_icon or splash: icons_path = _qt_init_icons() - if enable_icon and app.windowIcon().cacheKey() != _QT_ICON_KEYS["app"]: + if ( + enable_icon + and app.windowIcon().cacheKey() != _QT_ICON_KEYS["app"] + and app.windowIcon().isNull() # don't overwrite existing icon (e.g. MNELAB) + ): # Set icon kind = "bigsur_" if platform.mac_ver()[0] >= "10.16" else "default_" icon = QIcon(f"{icons_path}/mne_{kind}icon.png") From b906b784430af749977828da0eeda922e28efd98 Mon Sep 17 00:00:00 2001 From: Jacob Woessner Date: Wed, 13 Mar 2024 10:31:38 -0500 Subject: [PATCH 226/405] [BUG] Drop annotations with NaN onset in EEGLAB raw files (#12484) Co-authored-by: Eric Larson --- doc/changes/devel/12484.bugfix.rst | 1 + mne/io/eeglab/eeglab.py | 16 +++++++++++++++ mne/io/eeglab/tests/test_eeglab.py | 33 ++++++++++++++++++++++++++++++ 3 files changed, 50 insertions(+) create mode 100644 doc/changes/devel/12484.bugfix.rst diff --git a/doc/changes/devel/12484.bugfix.rst b/doc/changes/devel/12484.bugfix.rst new file mode 100644 index 00000000000..2430f534661 --- /dev/null +++ b/doc/changes/devel/12484.bugfix.rst @@ -0,0 +1 @@ +- Fix problem caused by onsets with NaN values using :func:`mne.io.read_raw_eeglab` by `Jacob Woessner`_ \ No newline at end of file diff --git a/mne/io/eeglab/eeglab.py b/mne/io/eeglab/eeglab.py index 50cfc39c820..e2f1ce320c5 100644 --- a/mne/io/eeglab/eeglab.py +++ b/mne/io/eeglab/eeglab.py @@ -799,6 +799,22 @@ def _read_annotations_eeglab(eeg, uint16_codec=None): ) duration[idx] = np.nan if is_empty_array else event.duration + # Drop events with NaN onset see PR #12484 + valid_indices = [ + idx for idx, onset_idx in enumerate(onset) if not np.isnan(onset_idx) + ] + n_dropped = len(onset) - len(valid_indices) + if len(valid_indices) != len(onset): + warn( + f"{n_dropped} events have an onset that is NaN. These values are " + "usually ignored by EEGLAB and will be dropped from the " + "annotations." + ) + + onset = np.array([onset[idx] for idx in valid_indices]) + duration = np.array([duration[idx] for idx in valid_indices]) + description = [description[idx] for idx in valid_indices] + return Annotations( onset=np.array(onset) / eeg.srate, duration=duration / eeg.srate, diff --git a/mne/io/eeglab/tests/test_eeglab.py b/mne/io/eeglab/tests/test_eeglab.py index af1a3bbfc77..ebd5a6a6706 100644 --- a/mne/io/eeglab/tests/test_eeglab.py +++ b/mne/io/eeglab/tests/test_eeglab.py @@ -719,3 +719,36 @@ def get_bad_information(eeg, get_pos, *, montage_units): assert len(pos["lpa"]) == 3 assert len(pos["rpa"]) == 3 assert len(raw.info["dig"]) == n_eeg + 3 + + +@testing.requires_testing_data +def test_eeglab_drop_nan_annotations(tmp_path): + """Test reading file with NaN annotations.""" + pytest.importorskip("eeglabio") + from eeglabio.raw import export_set + + file_path = tmp_path / "test_nan_anno.set" + raw = read_raw_eeglab(raw_fname_mat, preload=True) + data = raw.get_data() + sfreq = raw.info["sfreq"] + ch_names = raw.ch_names + anno = [ + raw.annotations.description, + raw.annotations.onset, + raw.annotations.duration, + ] + anno[1][0] = np.nan + + export_set( + str(file_path), + data, + sfreq, + ch_names, + ch_locs=None, + annotations=anno, + ref_channels="common", + ch_types=np.repeat("EEG", len(ch_names)), + ) + + with pytest.raises(RuntimeWarning, match="1 .* have an onset that is NaN.*"): + raw = read_raw_eeglab(file_path, preload=True) From 25e6aecf1c0fd0b0f1e2b3fc8d46377dffa752e4 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 13 Mar 2024 11:37:32 -0400 Subject: [PATCH 227/405] [pre-commit.ci] pre-commit autoupdate (#12492) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Mathieu Scheltienne Co-authored-by: Eric Larson --- .pre-commit-config.yaml | 4 ++-- examples/preprocessing/eeg_bridging.py | 9 +++------ mne/_freesurfer.py | 2 +- mne/_ola.py | 10 +++------- mne/annotations.py | 17 +++++++---------- mne/bem.py | 9 ++++----- mne/channels/montage.py | 8 +++----- mne/coreg.py | 8 ++++---- mne/cov.py | 14 ++++++-------- mne/dipole.py | 12 +++++------- mne/epochs.py | 10 +++++----- mne/event.py | 2 +- mne/evoked.py | 4 ++-- mne/filter.py | 22 ++++------------------ mne/forward/_make_forward.py | 5 +++-- mne/io/array/array.py | 5 ++--- mne/io/base.py | 5 +++-- mne/label.py | 6 +++--- mne/morph.py | 6 +++--- mne/source_estimate.py | 8 ++++---- mne/time_frequency/csd.py | 5 +++-- mne/viz/evoked.py | 10 ++++------ pyproject.toml | 3 +++ 23 files changed, 78 insertions(+), 106 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0298815545a..47e6936a83b 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.0 + rev: v0.3.2 hooks: - id: ruff name: ruff lint mne @@ -48,7 +48,7 @@ repos: # mypy - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.8.0 + rev: v1.9.0 hooks: - id: mypy # Avoid the conflict between mne/__init__.py and mne/__init__.pyi by ignoring the former diff --git a/examples/preprocessing/eeg_bridging.py b/examples/preprocessing/eeg_bridging.py index fbab43cfc5f..87e1d8621f0 100644 --- a/examples/preprocessing/eeg_bridging.py +++ b/examples/preprocessing/eeg_bridging.py @@ -320,12 +320,9 @@ # compute variance of residuals print( "Variance of residual (interpolated data - original data)\n\n" - "With adding virtual channel: {}\n" - "Compared to interpolation only using other channels: {}" - "".format( - np.mean(np.var(data_virtual - data_orig, axis=1)), - np.mean(np.var(data_comp - data_orig, axis=1)), - ) + f"With adding virtual channel: {np.mean(np.var(data_virtual - data_orig, axis=1))}\n" + f"Compared to interpolation only using other channels: {np.mean(np.var(data_comp - data_orig, axis=1))}" + "" ) # plot results diff --git a/mne/_freesurfer.py b/mne/_freesurfer.py index 67a27d59860..dd868c1ee0d 100644 --- a/mne/_freesurfer.py +++ b/mne/_freesurfer.py @@ -249,7 +249,7 @@ def get_volume_labels_from_aseg(mgz_fname, return_colors=False, atlas_ids=None): if atlas_ids is None: atlas_ids, colors = read_freesurfer_lut() elif return_colors: - raise ValueError("return_colors must be False if atlas_ids are " "provided") + raise ValueError("return_colors must be False if atlas_ids are provided") # restrict to the ones in the MRI, sorted by label name keep = np.isin(list(atlas_ids.values()), want) keys = sorted( diff --git a/mne/_ola.py b/mne/_ola.py index c339293ee81..a7da98905b9 100644 --- a/mne/_ola.py +++ b/mne/_ola.py @@ -370,13 +370,9 @@ def feed(self, *datas, verbose=None, **kwargs): or self._in_buffers[di].dtype != data.dtype ): raise TypeError( - "data must dtype {} and shape[:-1]=={}, got dtype {} shape[:-1]=" - "{}".format( - self._in_buffers[di].dtype, - self._in_buffers[di].shape[:-1], - data.dtype, - data.shape[:-1], - ) + f"data must dtype {self._in_buffers[di].dtype} and " + f"shape[:-1]=={self._in_buffers[di].shape[:-1]}, got dtype " + f"{data.dtype} shape[:-1]={data.shape[:-1]}" ) logger.debug( " + Appending %d->%d" diff --git a/mne/annotations.py b/mne/annotations.py index a6be1f7a62d..1c66fee1be5 100644 --- a/mne/annotations.py +++ b/mne/annotations.py @@ -307,11 +307,9 @@ def __repr__(self): kinds = ", ".join(["{} ({})".format(*k) for k in sorted(counter.items())]) kinds = (": " if len(kinds) > 0 else "") + kinds ch_specific = ", channel-specific" if self._any_ch_names() else "" - s = "Annotations | {} segment{}{}{}".format( - len(self.onset), - _pl(len(self.onset)), - ch_specific, - kinds, + s = ( + f"Annotations | {len(self.onset)} segment" + f"{_pl(len(self.onset))}{ch_specific}{kinds}" ) return "<" + shorten(s, width=77, placeholder=" ...") + ">" @@ -820,11 +818,10 @@ def set_annotations(self, annotations, on_missing="raise", *, verbose=None): else: if getattr(self, "_unsafe_annot_add", False): warn( - "Adding annotations to Epochs created (and saved to " - "disk) before 1.0 will yield incorrect results if " - "decimation or resampling was performed on the instance, " - "we recommend regenerating the Epochs and re-saving them " - "to disk." + "Adding annotations to Epochs created (and saved to disk) before " + "1.0 will yield incorrect results if decimation or resampling was " + "performed on the instance, we recommend regenerating the Epochs " + "and re-saving them to disk." ) new_annotations = annotations.copy() new_annotations._prune_ch_names(self.info, on_missing) diff --git a/mne/bem.py b/mne/bem.py index dd9b5a1e24e..88104ea9cc2 100644 --- a/mne/bem.py +++ b/mne/bem.py @@ -1082,10 +1082,9 @@ def get_fitting_dig(info, dig_kinds="auto", exclude_frontal=True, verbose=None): if len(hsp) <= 10: kinds_str = ", ".join(['"%s"' % _dig_kind_rev[d] for d in sorted(dig_kinds)]) - msg = "Only {} head digitization points of the specified kind{} ({},)".format( - len(hsp), - _pl(dig_kinds), - kinds_str, + msg = ( + f"Only {len(hsp)} head digitization points of the specified " + f"kind{_pl(dig_kinds)} ({kinds_str},)" ) if len(hsp) < 4: raise ValueError(msg + ", at least 4 required") @@ -2455,7 +2454,7 @@ def check_seghead(surf_path=subj_path / "surf"): surf = check_seghead() if surf is None: - raise RuntimeError("mkheadsurf did not produce the standard output " "file.") + raise RuntimeError("mkheadsurf did not produce the standard output file.") bem_dir = subjects_dir / subject / "bem" if not bem_dir.is_dir(): diff --git a/mne/channels/montage.py b/mne/channels/montage.py index 6e63ec28cf5..abc9f2f62b7 100644 --- a/mne/channels/montage.py +++ b/mne/channels/montage.py @@ -1709,11 +1709,9 @@ def compute_dev_head_t(montage): if not (len(hpi_head) == len(hpi_dev) and len(hpi_dev) > 0): raise ValueError( - ( - "To compute Device-to-Head transformation, the same number of HPI" - " points in device and head coordinates is required. (Got {dev}" - " points in device and {head} points in head coordinate systems)" - ).format(dev=len(hpi_dev), head=len(hpi_head)) + "To compute Device-to-Head transformation, the same number of HPI" + f" points in device and head coordinates is required. (Got {len(hpi_dev)}" + f" points in device and {len(hpi_head)} points in head coordinate systems)" ) trans = _quat_to_affine(_fit_matched_points(hpi_dev, hpi_head)[0]) diff --git a/mne/coreg.py b/mne/coreg.py index c83b8f3106f..7dae561c2a2 100644 --- a/mne/coreg.py +++ b/mne/coreg.py @@ -167,7 +167,7 @@ def coregister_fiducials(info, fiducials, tol=0.01): coord_frame_to = FIFF.FIFFV_COORD_MRI frames_from = {d["coord_frame"] for d in info["dig"]} if len(frames_from) > 1: - raise ValueError("info contains fiducials from different coordinate " "frames") + raise ValueError("info contains fiducials from different coordinate frames") else: coord_frame_from = frames_from.pop() coords_from = _fiducial_coords(info["dig"]) @@ -220,7 +220,7 @@ def create_default_subject(fs_home=None, update=False, subjects_dir=None, verbos 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 + "fsaverage not found at %r. Is fs_home specified correctly?" % fs_src ) for name in ("label", "mri", "surf"): dirname = os.path.join(fs_src, name) @@ -468,7 +468,7 @@ def fit_matched_points( return trans else: raise ValueError( - "Invalid out parameter: %r. Needs to be 'params' or " "'trans'." % out + "Invalid out parameter: %r. Needs to be 'params' or 'trans'." % out ) @@ -1945,7 +1945,7 @@ def fit_fiducials( n_scale_params = self._n_scale_params if n_scale_params == 3: # enforce 1 even for 3-axis here (3 points is not enough) - logger.info("Enforcing 1 scaling parameter for fit " "with fiducials.") + logger.info("Enforcing 1 scaling parameter for fit with fiducials.") n_scale_params = 1 self._lpa_weight = lpa_weight self._nasion_weight = nasion_weight diff --git a/mne/cov.py b/mne/cov.py index 7b9a4b24252..60e2f21c893 100644 --- a/mne/cov.py +++ b/mne/cov.py @@ -85,12 +85,12 @@ def _check_covs_algebra(cov1, cov2): if cov1.ch_names != cov2.ch_names: - raise ValueError("Both Covariance do not have the same list of " "channels.") + raise ValueError("Both Covariance do not have the same list of channels.") projs1 = [str(c) for c in cov1["projs"]] projs2 = [str(c) for c in cov1["projs"]] if projs1 != projs2: raise ValueError( - "Both Covariance do not have the same list of " "SSP projections." + "Both Covariance do not have the same list of SSP projections." ) @@ -859,7 +859,7 @@ def _check_method_params( for p, v in _method_params.items(): if v.get("assume_centered", None) is False: raise ValueError( - "`assume_centered` must be True" " if `keep_sample_mean` is False" + "`assume_centered` must be True if `keep_sample_mean` is False" ) return method, _method_params @@ -1074,9 +1074,7 @@ def _unpack_epochs(epochs): and keep_sample_mean for epochs_t in epochs ): - warn( - "Epochs are not baseline corrected, covariance " "matrix may be inaccurate" - ) + warn("Epochs are not baseline corrected, covariance matrix may be inaccurate") orig = epochs[0].info["dev_head_t"] _check_on_missing(on_mismatch, "on_mismatch") @@ -1372,7 +1370,7 @@ def _compute_covariance_auto( estimator_cov_info.append((fa, fa.get_covariance(), _info)) del fa else: - raise ValueError("Oh no! Your estimator does not have" " a .fit method") + raise ValueError("Oh no! Your estimator does not have a .fit method") logger.info("Done.") if len(method) > 1: @@ -2338,7 +2336,7 @@ def _read_cov(fid, node, cov_kind, limited=False, verbose=None): names = _safe_name_list(tag.data, "read", "names") if len(names) != dim: raise ValueError( - "Number of names does not match " "covariance matrix dimension" + "Number of names does not match covariance matrix dimension" ) tag = find_tag(fid, this, FIFF.FIFF_MNE_COV) diff --git a/mne/dipole.py b/mne/dipole.py index 5c1d6423c91..9dcc88c2b01 100644 --- a/mne/dipole.py +++ b/mne/dipole.py @@ -1532,13 +1532,13 @@ def fit_dipole( ] if len(R) == 0: raise RuntimeError( - "No MEG channels found, but MEG-only " "sphere model used" + "No MEG channels found, but MEG-only sphere model used" ) R = np.min(np.sqrt(np.sum(R * R, axis=1))) # use dist to sensors kind = "max_rad" logger.info( - "Sphere model : origin at ({: 7.2f} {: 7.2f} {: 7.2f}) mm, " - "{} = {:6.1f} mm".format(1000 * r0[0], 1000 * r0[1], 1000 * r0[2], kind, R) + f"Sphere model : origin at ({1000 * r0[0]: 7.2f} {1000 * r0[1]: 7.2f} " + f"{1000 * r0[2]: 7.2f}) mm, {kind} = {R:6.1f} mm" ) inner_skull = dict(R=R, r0=r0) # NB sphere model defined in head frame del R, r0 @@ -1548,9 +1548,7 @@ def fit_dipole( fixed_position = True pos = np.array(pos, float) if pos.shape != (3,): - raise ValueError( - "pos must be None or a 3-element array-like," f" got {pos}" - ) + raise ValueError(f"pos must be None or a 3-element array-like, got {pos}") logger.info( "Fixed position : {:6.1f} {:6.1f} {:6.1f} mm".format(*tuple(1000 * pos)) ) @@ -1558,7 +1556,7 @@ def fit_dipole( ori = np.array(ori, float) if ori.shape != (3,): raise ValueError( - "oris must be None or a 3-element array-like," f" got {ori}" + f"oris must be None or a 3-element array-like, got {ori}" ) norm = np.sqrt(np.sum(ori * ori)) if not np.isclose(norm, 1): diff --git a/mne/epochs.py b/mne/epochs.py index 83b427ac394..7006fb10f3e 100644 --- a/mne/epochs.py +++ b/mne/epochs.py @@ -1163,8 +1163,8 @@ def _compute_aggregate(self, picks, mode="mean"): assert len(self.events) == len(self._data) if data.shape != self._data.shape[1:]: raise RuntimeError( - "You passed a function that resulted n data of shape {}, " - "but it should be {}.".format(data.shape, self._data.shape[1:]) + f"You passed a function that resulted n data of shape " + f"{data.shape}, but it should be {self._data.shape[1:]}." ) else: if mode not in {"mean", "std"}: @@ -2464,7 +2464,7 @@ def equalize_event_counts(self, event_ids=None, method="mintime"): for ii, id_ in enumerate(event_ids): if len(id_) == 0: raise KeyError( - f"{orig_ids[ii]} not found in the epoch " "object's event_id." + f"{orig_ids[ii]} not found in the epoch object's event_id." ) elif len({sub_id in ids for sub_id in id_}) != 1: err = ( @@ -3593,11 +3593,11 @@ def __init__( data = np.asanyarray(data, dtype=dtype) if data.ndim != 3: raise ValueError( - "Data must be a 3D array of shape (n_epochs, " "n_channels, n_samples)" + "Data must be a 3D array of shape (n_epochs, n_channels, n_samples)" ) if len(info["ch_names"]) != data.shape[1]: - raise ValueError("Info and data must have same number of " "channels.") + raise ValueError("Info and data must have same number of channels.") if events is None: n_epochs = len(data) events = _gen_events(n_epochs) diff --git a/mne/event.py b/mne/event.py index cfc112a2e8c..a79ea13dbcc 100644 --- a/mne/event.py +++ b/mne/event.py @@ -1030,7 +1030,7 @@ def concatenate_events(events, first_samps, last_samps): _validate_type(events, list, "events") if not (len(events) == len(last_samps) and len(events) == len(first_samps)): raise ValueError( - "events, first_samps, and last_samps must all have " "the same lengths" + "events, first_samps, and last_samps must all have the same lengths" ) first_samps = np.array(first_samps) last_samps = np.array(last_samps) diff --git a/mne/evoked.py b/mne/evoked.py index 36831db8ce0..f6f752cadbf 100644 --- a/mne/evoked.py +++ b/mne/evoked.py @@ -2075,12 +2075,12 @@ def _get_peak(data, times, tmin=None, tmax=None, mode="abs", *, strict=True): if mode == "pos": if strict and not np.any(data[~mask] > 0): raise ValueError( - "No positive values encountered. Cannot " "operate in pos mode." + "No positive values encountered. Cannot operate in pos mode." ) elif mode == "neg": if strict and not np.any(data[~mask] < 0): raise ValueError( - "No negative values encountered. Cannot " "operate in neg mode." + "No negative values encountered. Cannot operate in neg mode." ) maxfun = np.argmin diff --git a/mne/filter.py b/mne/filter.py index 477434a7ca4..290ddf7f7d7 100644 --- a/mne/filter.py +++ b/mne/filter.py @@ -873,12 +873,7 @@ def construct_iir_filter( # ensure we have a valid ftype if "ftype" not in iir_params: raise RuntimeError( - "ftype must be an entry in iir_params if " - "b" - " " - "and " - "a" - " are not specified" + "ftype must be an entry in iir_params if 'b' and 'a' are not specified." ) ftype = iir_params["ftype"] if ftype not in known_filters: @@ -932,14 +927,7 @@ def construct_iir_filter( Ws = np.asanyarray(f_stop) / (float(sfreq) / 2) if "gpass" not in iir_params or "gstop" not in iir_params: raise ValueError( - "iir_params must have at least " - "gstop" - " and" - " " - "gpass" - " (or " - "N" - ") entries" + "iir_params must have at least 'gstop' and 'gpass' (or N) entries." ) system = signal.iirdesign( Wp, @@ -1488,7 +1476,7 @@ def create_filter( freq = np.r_[freq, [sfreq / 2.0]] gain = np.r_[gain, [1.0]] if np.any(np.abs(np.diff(gain, 2)) > 1): - raise ValueError("Stop bands are not sufficiently " "separated.") + raise ValueError("Stop bands are not sufficiently separated.") if method == "fir": out = _construct_fir_filter( sfreq, freq, gain, filter_length, phase, fir_window, fir_design @@ -2392,9 +2380,7 @@ def float_array(c): if l_freq is not None: l_check = min(np.atleast_1d(l_trans_bandwidth)) mult_fact = 2.0 if fir_design == "firwin2" else 1.0 - filter_length = "{}s".format( - _length_factors[fir_window] * mult_fact / float(min(h_check, l_check)), - ) + filter_length = f"{_length_factors[fir_window] * mult_fact / float(min(h_check, l_check))}s" # noqa: E501 next_pow_2 = False # disable old behavior else: next_pow_2 = isinstance(filter_length, str) and phase == "zero-double" diff --git a/mne/forward/_make_forward.py b/mne/forward/_make_forward.py index 313da3a4922..812be7daf7e 100644 --- a/mne/forward/_make_forward.py +++ b/mne/forward/_make_forward.py @@ -819,8 +819,9 @@ def make_forward_dipole(dipole, bem, info, trans=None, n_jobs=None, *, verbose=N head = "The following dipoles are outside the inner skull boundary" msg = len(head) * "#" + "\n" + head + "\n" for t, pos in zip(times[np.logical_not(inuse)], pos[np.logical_not(inuse)]): - msg += " t={:.0f} ms, pos=({:.0f}, {:.0f}, {:.0f}) mm\n".format( - t * 1000.0, pos[0] * 1000.0, pos[1] * 1000.0, pos[2] * 1000.0 + msg += ( + f" t={t * 1000.0:.0f} ms, pos=({pos[0] * 1000.0:.0f}, " + f"{pos[1] * 1000.0:.0f}, {pos[2] * 1000.0:.0f}) mm\n" ) msg += len(head) * "#" logger.error(msg) diff --git a/mne/io/array/array.py b/mne/io/array/array.py index 16f4888ec72..dda73b80a23 100644 --- a/mne/io/array/array.py +++ b/mne/io/array/array.py @@ -81,9 +81,8 @@ def __init__(self, data, info, first_samp=0, copy="auto", verbose=None): "to get to double floating point precision" ) logger.info( - "Creating RawArray with {} data, n_channels={}, n_times={}".format( - dtype.__name__, data.shape[0], data.shape[1] - ) + f"Creating RawArray with {dtype.__name__} data, " + f"n_channels={data.shape[0]}, n_times={data.shape[1]}" ) super().__init__( info, data, first_samps=(int(first_samp),), dtype=dtype, verbose=verbose diff --git a/mne/io/base.py b/mne/io/base.py index 99a8e658fc4..e68b49af3da 100644 --- a/mne/io/base.py +++ b/mne/io/base.py @@ -2733,8 +2733,9 @@ def _check_start_stop_within_bounds(self): # we've done something wrong if we hit this n_times_max = len(self.raw.times) error_msg = ( - "Can't write raw file with no data: {} -> {} (max: {}) requested" - ).format(self.start, self.stop, n_times_max) + f"Can't write raw file with no data: {self.start} -> {self.stop} " + f"(max: {n_times_max}) requested" + ) if self.start >= self.stop or self.stop > n_times_max: raise RuntimeError(error_msg) diff --git a/mne/label.py b/mne/label.py index 24aed492b12..0fba226d4a4 100644 --- a/mne/label.py +++ b/mne/label.py @@ -1794,10 +1794,10 @@ def grow_labels( n_seeds = len(seeds) if len(extents) != 1 and len(extents) != n_seeds: - raise ValueError("The extents parameter has to be of length 1 or " "len(seeds)") + raise ValueError("The extents parameter has to be of length 1 or len(seeds)") if len(hemis) != 1 and len(hemis) != n_seeds: - raise ValueError("The hemis parameter has to be of length 1 or " "len(seeds)") + raise ValueError("The hemis parameter has to be of length 1 or len(seeds)") if colors is not None: if len(colors.shape) == 1: # if one color for all seeds @@ -2393,7 +2393,7 @@ def _check_labels_subject(labels, subject, name): ) if subject is None: raise ValueError( - "if label.subject is None for all labels, " "%s must be provided" % name + f"if label.subject is None for all labels, {name} must be provided." ) return subject diff --git a/mne/morph.py b/mne/morph.py index db1d65236c7..e3ac5dd1589 100644 --- a/mne/morph.py +++ b/mne/morph.py @@ -241,7 +241,7 @@ def compute_source_morph( if src_to is None: if kind == "mixed": raise ValueError( - "src_to must be provided when using a " "mixed source space" + "src_to must be provided when using a mixed source space" ) else: surf_offset = 2 if src_to.kind == "mixed" else 0 @@ -268,9 +268,9 @@ def compute_source_morph( vertices_from = src_data["vertices_from"] if sparse: if spacing is not None: - raise ValueError("spacing must be set to None if " "sparse=True.") + raise ValueError("spacing must be set to None if sparse=True.") if xhemi: - raise ValueError("xhemi=True can only be used with " "sparse=False") + raise ValueError("xhemi=True can only be used with sparse=False") vertices_to_surf, morph_mat = _compute_sparse_morph( vertices_from, subject_from, subject_to, subjects_dir ) diff --git a/mne/source_estimate.py b/mne/source_estimate.py index 91b56289062..ccf4f8f7d19 100644 --- a/mne/source_estimate.py +++ b/mne/source_estimate.py @@ -1728,7 +1728,7 @@ def expand(self, vertices): if not isinstance(vertices, list): raise TypeError("vertices must be a list") if not len(self.vertices) == len(vertices): - raise ValueError("vertices must have the same length as " "stc.vertices") + raise ValueError("vertices must have the same length as stc.vertices") # can no longer use kernel and sensor data self._remove_kernel_sens_data_() @@ -1942,7 +1942,7 @@ def save(self, fname, ftype="stc", *, overwrite=False, verbose=None): ) elif ftype == "w": if self.shape[1] != 1: - raise ValueError("w files can only contain a single time " "point") + raise ValueError("w files can only contain a single time point.") logger.info("Writing STC to disk (w format)...") fname_l = str(_check_fname(fname + "-lh.w", overwrite=overwrite)) fname_r = str(_check_fname(fname + "-rh.w", overwrite=overwrite)) @@ -2500,7 +2500,7 @@ def in_label(self, label, mri, src, *, verbose=None): """ if len(self.vertices) != 1: raise RuntimeError( - "This method can only be used with whole-brain " "volume source spaces" + "This method can only be used with whole-brain volume source spaces" ) _validate_type(label, (str, "int-like"), "label") if isinstance(label, str): @@ -3126,7 +3126,7 @@ def spatio_temporal_src_adjacency(src, n_times, dist=None, verbose=None): if src[0]["type"] == "vol": if dist is not None: raise ValueError( - "dist must be None for a volume " "source space. Got %s." % dist + f"dist must be None for a volume source space. Got {dist}." ) adjacency = _spatio_temporal_src_adjacency_vol(src, n_times) diff --git a/mne/time_frequency/csd.py b/mne/time_frequency/csd.py index 5bca1d03508..8744a77f376 100644 --- a/mne/time_frequency/csd.py +++ b/mne/time_frequency/csd.py @@ -198,8 +198,9 @@ def __repr__(self): # noqa: D105 time_str = "unknown" return ( - "" - ).format(self.n_channels, time_str, freq_str) + "" + ) def sum(self, fmin=None, fmax=None): """Calculate the sum CSD in the given frequency range(s). diff --git a/mne/viz/evoked.py b/mne/viz/evoked.py index f2a47fbe4d0..bbbede964a8 100644 --- a/mne/viz/evoked.py +++ b/mne/viz/evoked.py @@ -2219,9 +2219,8 @@ def _validate_linestyles_pce(linestyles, conditions, tags): # should be a dict by now... if not isinstance(linestyles, dict): raise TypeError( - '"linestyles" must be a dict, list, or None; got {}.'.format( - type(linestyles).__name__ - ) + '"linestyles" must be a dict, list, or None; got ' + f"{type(linestyles).__name__}." ) # validate linestyle dict keys if not set(linestyles).issubset(tags.union(conditions)): @@ -2906,9 +2905,8 @@ def plot_compare_evokeds( # cannot combine a single channel if (len(picks) < 2) and combine is not None: warn( - 'Only {} channel in "picks"; cannot combine by method "{}".'.format( - len(picks), combine - ) + f'Only {len(picks)} channel in "picks"; cannot combine by method ' + f'"{combine}".' ) # `combine` defaults to GFP unless picked a single channel or axes='topo' do_topo = isinstance(axes, str) and axes == "topo" diff --git a/pyproject.toml b/pyproject.toml index d82518aa70e..f0f3402e460 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -242,6 +242,9 @@ ignore-decorators = [ "examples/*/*.py" = [ "D205", # 1 blank line required between summary line and description ] +"examples/preprocessing/eeg_bridging.py" = [ + "E501", # line too long +] [tool.pytest.ini_options] # -r f (failed), E (error), s (skipped), x (xfail), X (xpassed), w (warnings) From 2a973338657e6d0609b1d60a8c109cad533c033c Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Wed, 13 Mar 2024 13:27:31 -0400 Subject: [PATCH 228/405] MAINT: Work around sklearn bug (#12496) --- mne/preprocessing/tests/test_lof.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/mne/preprocessing/tests/test_lof.py b/mne/preprocessing/tests/test_lof.py index 858fa0e4432..3ded089235c 100644 --- a/mne/preprocessing/tests/test_lof.py +++ b/mne/preprocessing/tests/test_lof.py @@ -3,12 +3,14 @@ # License: BSD-3-Clause # Copyright the MNE-Python contributors. +import sys from pathlib import Path import pytest from mne.io import read_raw_fif from mne.preprocessing import find_bad_channels_lof +from mne.utils import check_version base_dir = Path(__file__).parent.parent.parent / "io" / "tests" / "data" raw_fname = base_dir / "test_raw.fif" @@ -26,6 +28,8 @@ def test_lof(n_neighbors, ch_type, n_ch, n_bad): """Test LOF detection.""" pytest.importorskip("sklearn") + if sys.platform == "win32" and check_version("sklearn", "1.5.dev"): + pytest.skip("https://github.com/scikit-learn/scikit-learn/issues/28625") raw = read_raw_fif(raw_fname).load_data() assert raw.info["bads"] == [] bads, scores = find_bad_channels_lof( From 60505e96a7d90a56b48a6e1ea1e9f9f16a3dd87a Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Thu, 14 Mar 2024 16:16:26 -0400 Subject: [PATCH 229/405] BUG: Fix bug with too many legend entries (#12498) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- doc/changes/devel/12498.bugfix.rst | 2 ++ examples/preprocessing/muscle_ica.py | 1 - mne/report/report.py | 4 ++-- mne/viz/ica.py | 14 ++++++++++++-- mne/viz/tests/test_ica.py | 3 +++ .../preprocessing/40_artifact_correction_ica.py | 9 ++++----- 6 files changed, 23 insertions(+), 10 deletions(-) create mode 100644 doc/changes/devel/12498.bugfix.rst diff --git a/doc/changes/devel/12498.bugfix.rst b/doc/changes/devel/12498.bugfix.rst new file mode 100644 index 00000000000..2655cf692d1 --- /dev/null +++ b/doc/changes/devel/12498.bugfix.rst @@ -0,0 +1,2 @@ +Fix bug with :meth:`mne.preprocessing.ICA.plot_sources` for ``evoked`` data where the +legend contained too many entries, by `Eric Larson`_. diff --git a/examples/preprocessing/muscle_ica.py b/examples/preprocessing/muscle_ica.py index f57e24a678b..64c14f5f5af 100644 --- a/examples/preprocessing/muscle_ica.py +++ b/examples/preprocessing/muscle_ica.py @@ -11,7 +11,6 @@ artifact is produced during postural maintenance. This is more appropriately removed by ICA otherwise there wouldn't be any epochs left! Note that muscle artifacts of this kind are much more pronounced in EEG than they are in MEG. - """ # Authors: Alex Rockhill # diff --git a/mne/report/report.py b/mne/report/report.py index 43c3d7c7ac4..6519d5cbb06 100644 --- a/mne/report/report.py +++ b/mne/report/report.py @@ -430,8 +430,8 @@ def _fig_to_img(fig, *, image_format="png", own_figure=True): if pil_kwargs: # matplotlib modifies the passed dict, which is a bug mpl_kwargs["pil_kwargs"] = pil_kwargs.copy() - with warnings.catch_warnings(): - fig.savefig(output, format=image_format, dpi=dpi, **mpl_kwargs) + + fig.savefig(output, format=image_format, dpi=dpi, **mpl_kwargs) if own_figure: plt.close(fig) diff --git a/mne/viz/ica.py b/mne/viz/ica.py index e2eb6273cb3..1ec18fde1da 100644 --- a/mne/viz/ica.py +++ b/mne/viz/ica.py @@ -855,8 +855,18 @@ def _plot_ica_sources_evoked(evoked, picks, exclude, title, show, ica, labels=No lines[-1].set_pickradius(3.0) ax.set(title=title, xlim=times[[0, -1]], xlabel="Time (ms)", ylabel="(NA)") - if len(lines): - ax.legend(lines, exclude_labels, loc="best") + leg_lines_labels = list( + zip( + *[ + (line, label) + for line, label in zip(lines, exclude_labels) + if label is not None + ] + ) + ) + if len(leg_lines_labels): + leg_lines, leg_labels = leg_lines_labels + ax.legend(leg_lines, leg_labels, loc="best") texts.append( ax.text( diff --git a/mne/viz/tests/test_ica.py b/mne/viz/tests/test_ica.py index 7972e4d36b6..39d4b616431 100644 --- a/mne/viz/tests/test_ica.py +++ b/mne/viz/tests/test_ica.py @@ -362,12 +362,15 @@ def test_plot_ica_sources(raw_orig, browser_backend, monkeypatch): ica.plot_sources(epochs) ica.plot_sources(epochs.average()) evoked = epochs.average() + ica.exclude = [0] fig = ica.plot_sources(evoked) # Test a click ax = fig.get_axes()[0] line = ax.lines[0] _fake_click(fig, ax, [line.get_xdata()[0], line.get_ydata()[0]], "data") _fake_click(fig, ax, [ax.get_xlim()[0], ax.get_ylim()[1]], "data") + leg = ax.get_legend() + assert len(leg.get_texts()) == len(ica.exclude) == 1 # plot with bad channels excluded ica.exclude = [0] diff --git a/tutorials/preprocessing/40_artifact_correction_ica.py b/tutorials/preprocessing/40_artifact_correction_ica.py index 3e0698a0efe..7c7c872ff70 100644 --- a/tutorials/preprocessing/40_artifact_correction_ica.py +++ b/tutorials/preprocessing/40_artifact_correction_ica.py @@ -416,11 +416,10 @@ ica.plot_sources(eog_evoked) # %% -# Note that above we used `~mne.preprocessing.ICA.plot_sources` on both -# the original `~mne.io.Raw` instance and also on an -# `~mne.Evoked` instance of the extracted EOG artifacts. This can be -# another way to confirm that `~mne.preprocessing.ICA.find_bads_eog` has -# identified the correct components. +# Note that above we used :meth:`~mne.preprocessing.ICA.plot_sources` on both the +# original :class:`~mne.io.Raw` instance and also on an `~mne.Evoked` instance of the +# extracted EOG artifacts. This can be another way to confirm that +# :meth:`~mne.preprocessing.ICA.find_bads_eog` has identified the correct components. # # # Using a simulated channel to select ICA components From 14ff9483cf92a40479b23f4b17886d3d384f65a5 Mon Sep 17 00:00:00 2001 From: Clemens Brunner Date: Fri, 15 Mar 2024 15:11:15 +0100 Subject: [PATCH 230/405] Remove bundle name hack on macOS (#12499) --- mne/viz/backends/_utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mne/viz/backends/_utils.py b/mne/viz/backends/_utils.py index 6045e9eeac8..25e87fcff22 100644 --- a/mne/viz/backends/_utils.py +++ b/mne/viz/backends/_utils.py @@ -150,7 +150,8 @@ def _init_mne_qtapp(enable_icon=True, pg_app=False, splash=False): bundle = NSBundle.mainBundle() info = bundle.localizedInfoDictionary() or bundle.infoDictionary() - info["CFBundleName"] = app_name + if "CFBundleName" not in info: + info["CFBundleName"] = app_name except ModuleNotFoundError: pass From 6eb4c3f4f94177d9c6e7c40cf941f5a4be9d4c98 Mon Sep 17 00:00:00 2001 From: Clemens Brunner Date: Mon, 18 Mar 2024 14:58:21 +0100 Subject: [PATCH 231/405] Allow exporting STIM channels to EDF (#12332) Co-authored-by: Daniel McCloy --- doc/changes/devel/12332.newfeature.rst | 1 + mne/export/_edf.py | 99 +++++++++----------------- mne/export/tests/test_export.py | 13 ++-- mne/io/edf/edf.py | 3 +- mne/utils/docs.py | 9 +-- 5 files changed, 44 insertions(+), 81 deletions(-) create mode 100644 doc/changes/devel/12332.newfeature.rst diff --git a/doc/changes/devel/12332.newfeature.rst b/doc/changes/devel/12332.newfeature.rst new file mode 100644 index 00000000000..0a7a82227ba --- /dev/null +++ b/doc/changes/devel/12332.newfeature.rst @@ -0,0 +1 @@ +Add ability to export STIM channels to EDF in :meth:`mne.io.Raw.export`, by `Clemens Brunner`_. \ No newline at end of file diff --git a/mne/export/_edf.py b/mne/export/_edf.py index 04590f042da..68905d5ed7d 100644 --- a/mne/export/_edf.py +++ b/mne/export/_edf.py @@ -17,60 +17,28 @@ def _export_raw(fname, raw, physical_range, add_ch_type): """Export Raw objects to EDF files. - TODO: if in future the Info object supports transducer or - technician information, allow writing those here. + TODO: if in future the Info object supports transducer or technician information, + allow writing those here. """ - # get EEG-related data in uV + # get voltage-based data in uV units = dict( eeg="uV", ecog="uV", seeg="uV", eog="uV", ecg="uV", emg="uV", bio="uV", dbs="uV" ) - digital_min = -32767 - digital_max = 32767 + digital_min, digital_max = -32767, 32767 # load data first raw.load_data() - # remove extra STI channels - orig_ch_types = raw.get_channel_types() - drop_chs = [] - if "stim" in orig_ch_types: - stim_index = np.argwhere(np.array(orig_ch_types) == "stim") - stim_index = np.atleast_1d(stim_index.squeeze()).tolist() - drop_chs.extend([raw.ch_names[idx] for idx in stim_index]) - warn(f"Exporting STIM channels is not supported, dropping indices {stim_index}") - - # Add warning if any channel types are not voltage based. - # Users are expected to only export data that is voltage based, - # such as EEG, ECoG, sEEG, etc. - # Non-voltage channels are dropped by the export function. - # Note: we can write these other channels, such as 'misc' - # but these are simply a "catch all" for unknown or undesired - # channels. - voltage_types = list(units) + ["stim", "misc"] - non_voltage_ch = [ch not in voltage_types for ch in orig_ch_types] - if any(non_voltage_ch): - warn( - f"Non-voltage channels detected: {non_voltage_ch}. MNE-Python's " - "EDF exporter only supports voltage-based channels, because the " - "EDF format cannot accommodate much of the accompanying data " - "necessary for channel types like MEG and fNIRS (channel " - "orientations, coordinate frame transforms, etc). You can " - "override this restriction by setting those channel types to " - '"misc" but no guarantees are made of the fidelity of that ' - "approach." - ) - - ch_names = [ch for ch in raw.ch_names if ch not in drop_chs] - ch_types = np.array(raw.get_channel_types(picks=ch_names)) + ch_types = np.array(raw.get_channel_types()) n_times = raw.n_times # get the entire dataset in uV - data = raw.get_data(units=units, picks=ch_names) + data = raw.get_data(units=units) - # Sampling frequency in EDF only supports integers, so to allow for - # float sampling rates from Raw, we adjust the output sampling rate - # for all channels and the data record duration. + # Sampling frequency in EDF only supports integers, so to allow for float sampling + # rates from Raw, we adjust the output sampling rate for all channels and the data + # record duration. sfreq = raw.info["sfreq"] if float(sfreq).is_integer(): out_sfreq = int(sfreq) @@ -78,10 +46,9 @@ def _export_raw(fname, raw, physical_range, add_ch_type): # make non-integer second durations work if (pad_width := int(np.ceil(n_times / sfreq) * sfreq - n_times)) > 0: warn( - f"EDF format requires equal-length data blocks, " - f"so {pad_width / sfreq} seconds of " - "zeros were appended to all channels when writing the " - "final block." + "EDF format requires equal-length data blocks, so " + f"{pad_width / sfreq:.3g} seconds of zeros were appended to all " + "channels when writing the final block." ) data = np.pad(data, (0, int(pad_width))) else: @@ -90,15 +57,17 @@ def _export_raw(fname, raw, physical_range, add_ch_type): ) out_sfreq = np.floor(sfreq) / data_record_duration warn( - f"Data has a non-integer sampling rate of {sfreq}; writing to " - "EDF format may cause a small change to sample times." + f"Data has a non-integer sampling rate of {sfreq}; writing to EDF format " + "may cause a small change to sample times." ) # get any filter information applied to the data lowpass = raw.info["lowpass"] highpass = raw.info["highpass"] linefreq = raw.info["line_freq"] - filter_str_info = f"HP:{highpass}Hz LP:{lowpass}Hz N:{linefreq}Hz" + filter_str_info = f"HP:{highpass}Hz LP:{lowpass}Hz" + if linefreq is not None: + filter_str_info += " N:{linefreq}Hz" if physical_range == "auto": # get max and min for each channel type data @@ -106,43 +75,41 @@ def _export_raw(fname, raw, physical_range, add_ch_type): ch_types_phys_min = dict() for _type in np.unique(ch_types): - _picks = [n for n, t in zip(ch_names, ch_types) if t == _type] + _picks = [n for n, t in zip(raw.ch_names, ch_types) if t == _type] _data = raw.get_data(units=units, picks=_picks) ch_types_phys_max[_type] = _data.max() ch_types_phys_min[_type] = _data.min() else: # get the physical min and max of the data in uV - # Physical ranges of the data in uV is usually set by the manufacturer - # and properties of the electrode. In general, physical max and min - # should be the clipping levels of the ADC input and they should be - # the same for all channels. For example, Nihon Kohden uses +3200 uV - # and -3200 uV for all EEG channels (which are the actual clipping - # levels of their input amplifiers & ADC). - # For full discussion, see: https://github.com/sccn/eeglab/issues/246 + # Physical ranges of the data in uV are usually set by the manufacturer and + # electrode properties. In general, physical min and max should be the clipping + # levels of the ADC input, and they should be the same for all channels. For + # example, Nihon Kohden uses ±3200 uV for all EEG channels (corresponding to the + # actual clipping levels of their input amplifiers & ADC). For a discussion, + # see https://github.com/sccn/eeglab/issues/246 pmin, pmax = physical_range[0], physical_range[1] # check that physical min and max is not exceeded if data.max() > pmax: warn( - f"The maximum μV of the data {data.max()} is " - f"more than the physical max passed in {pmax}.", + f"The maximum μV of the data {data.max()} is more than the physical max" + f" passed in {pmax}." ) if data.min() < pmin: warn( - f"The minimum μV of the data {data.min()} is " - f"less than the physical min passed in {pmin}.", + f"The minimum μV of the data {data.min()} is less than the physical min" + f" passed in {pmin}." ) data = np.clip(data, pmin, pmax) signals = [] - for idx, ch in enumerate(ch_names): + for idx, ch in enumerate(raw.ch_names): ch_type = ch_types[idx] signal_label = f"{ch_type.upper()} {ch}" if add_ch_type else ch if len(signal_label) > 16: raise RuntimeError( - f"Signal label for {ch} ({ch_type}) is " - f"longer than 16 characters, which is not " - f"supported in EDF. Please shorten the " - f"channel name before exporting to EDF." + f"Signal label for {ch} ({ch_type}) is longer than 16 characters, which" + " is not supported by the EDF standard. Please shorten the channel name" + "before exporting to EDF." ) if physical_range == "auto": @@ -156,7 +123,7 @@ def _export_raw(fname, raw, physical_range, add_ch_type): out_sfreq, label=signal_label, transducer_type="", - physical_dimension="uV", + physical_dimension="" if ch_type == "stim" else "uV", physical_range=(pmin, pmax), digital_range=(digital_min, digital_max), prefiltering=filter_str_info, diff --git a/mne/export/tests/test_export.py b/mne/export/tests/test_export.py index 808b020bfb4..62bbe57a87e 100644 --- a/mne/export/tests/test_export.py +++ b/mne/export/tests/test_export.py @@ -169,17 +169,13 @@ def test_double_export_edf(tmp_path): # export once temp_fname = tmp_path / "test.edf" - with pytest.warns(RuntimeWarning, match="Exporting STIM channels"): - raw.export(temp_fname, add_ch_type=True) + raw.export(temp_fname, add_ch_type=True) raw_read = read_raw_edf(temp_fname, infer_types=True, preload=True) # export again raw_read.export(temp_fname, add_ch_type=True, overwrite=True) raw_read = read_raw_edf(temp_fname, infer_types=True, preload=True) - # stim channel should be dropped - raw.drop_channels("2") - assert raw.ch_names == raw_read.ch_names assert_array_almost_equal(raw.get_data(), raw_read.get_data(), decimal=10) assert_array_equal(raw.times, raw_read.times) @@ -257,19 +253,19 @@ def test_rawarray_edf(tmp_path): @edfio_mark() -def test_edf_export_warns_on_non_voltage_channels(tmp_path): +def test_edf_export_non_voltage_channels(tmp_path): """Test saving a Raw array containing a non-voltage channel.""" temp_fname = tmp_path / "test.edf" raw = _create_raw_for_edf_tests() raw.set_channel_types({"9": "hbr"}, on_unit_change="ignore") - with pytest.warns(RuntimeWarning, match="Non-voltage channels"): - raw.export(temp_fname, overwrite=True) + raw.export(temp_fname, overwrite=True) # data should match up to the non-accepted channel raw_read = read_raw_edf(temp_fname, preload=True) assert raw.ch_names == raw_read.ch_names assert_array_almost_equal(raw.get_data()[:-1], raw_read.get_data()[:-1], decimal=10) + assert_array_almost_equal(raw.get_data()[-1], raw_read.get_data()[-1], decimal=5) assert_array_equal(raw.times, raw_read.times) @@ -291,6 +287,7 @@ def test_measurement_date_outside_range_valid_for_edf(tmp_path): raw.export(tmp_path / "test.edf", overwrite=True) +@pytest.mark.filterwarnings("ignore:Data has a non-integer:RuntimeWarning") @pytest.mark.parametrize( ("physical_range", "exceeded_bound"), [ diff --git a/mne/io/edf/edf.py b/mne/io/edf/edf.py index 4c3b2da8e24..7a329b6af64 100644 --- a/mne/io/edf/edf.py +++ b/mne/io/edf/edf.py @@ -40,6 +40,7 @@ "TEMP": FIFF.FIFFV_TEMPERATURE_CH, "MISC": FIFF.FIFFV_MISC_CH, "SAO2": FIFF.FIFFV_BIO_CH, + "STIM": FIFF.FIFFV_STIM_CH, } @@ -369,7 +370,7 @@ def _read_segment_file(data, idx, fi, start, stop, raw_extras, filenames, cals, # We could read this one EDF block at a time, which would be this: ch_offsets = np.cumsum(np.concatenate([[0], n_samps]), dtype=np.int64) - block_start_idx, r_lims, d_lims = _blk_read_lims(start, stop, buf_len) + block_start_idx, r_lims, _ = _blk_read_lims(start, stop, buf_len) # But to speed it up, we really need to read multiple blocks at once, # Otherwise we can end up with e.g. 18,181 chunks for a 20 MB file! # Let's do ~10 MB chunks: diff --git a/mne/utils/docs.py b/mne/utils/docs.py index 02bb6825b6e..73fead5ad1d 100644 --- a/mne/utils/docs.py +++ b/mne/utils/docs.py @@ -1217,12 +1217,9 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): ) docdict["export_edf_note"] = """ -For EDF exports, only channels measured in Volts are allowed; in MNE-Python -this means channel types 'eeg', 'ecog', 'seeg', 'emg', 'eog', 'ecg', 'dbs', -'bio', and 'misc'. 'stim' channels are dropped. Although this function -supports storing channel types in the signal label (e.g. ``EEG Fz`` or -``MISC E``), other software may not support this (optional) feature of -the EDF standard. +Although this function supports storing channel types in the signal label (e.g. +``EEG Fz`` or ``MISC E``), other software may not support this (optional) feature of the +EDF standard. If ``add_ch_type`` is True, then channel types are written based on what they are currently set in MNE-Python. One should double check that all From 5843ad17e2a883b5f029731252587ef9d01a8ab8 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Mon, 18 Mar 2024 11:29:14 -0400 Subject: [PATCH 232/405] MAINT: Work around SciPy dev bug (#12501) --- mne/preprocessing/tests/test_lof.py | 4 ---- tools/azure_dependencies.sh | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/mne/preprocessing/tests/test_lof.py b/mne/preprocessing/tests/test_lof.py index 3ded089235c..858fa0e4432 100644 --- a/mne/preprocessing/tests/test_lof.py +++ b/mne/preprocessing/tests/test_lof.py @@ -3,14 +3,12 @@ # License: BSD-3-Clause # Copyright the MNE-Python contributors. -import sys from pathlib import Path import pytest from mne.io import read_raw_fif from mne.preprocessing import find_bad_channels_lof -from mne.utils import check_version base_dir = Path(__file__).parent.parent.parent / "io" / "tests" / "data" raw_fname = base_dir / "test_raw.fif" @@ -28,8 +26,6 @@ def test_lof(n_neighbors, ch_type, n_ch, n_bad): """Test LOF detection.""" pytest.importorskip("sklearn") - if sys.platform == "win32" and check_version("sklearn", "1.5.dev"): - pytest.skip("https://github.com/scikit-learn/scikit-learn/issues/28625") raw = read_raw_fif(raw_fname).load_data() assert raw.info["bads"] == [] bads, scores = find_bad_channels_lof( diff --git a/tools/azure_dependencies.sh b/tools/azure_dependencies.sh index 00395c8ac67..7a691d25c29 100755 --- a/tools/azure_dependencies.sh +++ b/tools/azure_dependencies.sh @@ -9,7 +9,7 @@ elif [ "${TEST_MODE}" == "pip-pre" ]; then # python -m pip install $STD_ARGS --only-binary ":all:" --extra-index-url "https://www.riverbankcomputing.com/pypi/simple" "PyQt6!=6.6.1,!=6.6.2" PyQt6-sip PyQt6-Qt6 "PyQt6-Qt6!=6.6.1,!=6.6.2" python -m pip install $STD_ARGS --only-binary ":all:" "PyQt6!=6.6.1,!=6.6.2" PyQt6-sip PyQt6-Qt6 "PyQt6-Qt6!=6.6.1,!=6.6.2" echo "Numpy etc." - python -m pip install $STD_ARGS --only-binary ":all:" --default-timeout=60 --extra-index-url "https://pypi.anaconda.org/scientific-python-nightly-wheels/simple" "numpy<2.0.0.dev0" "scipy>=1.12.0.dev0" "scikit-learn>=1.5.dev0" matplotlib pillow statsmodels pyarrow h5py + python -m pip install $STD_ARGS --only-binary ":all:" --default-timeout=60 --extra-index-url "https://pypi.anaconda.org/scientific-python-nightly-wheels/simple" "numpy<2.0.0.dev0" "scipy==1.12.0" "scikit-learn>=1.5.dev0" matplotlib pillow statsmodels pyarrow h5py # echo "dipy" # python -m pip install $STD_ARGS --only-binary ":all:" --extra-index-url "https://pypi.anaconda.org/scipy-wheels-nightly/simple" dipy echo "OpenMEEG" From f65bc00ad6ee586acf101fe21b6ed5848dcd9523 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 19 Mar 2024 00:45:17 +0000 Subject: [PATCH 233/405] [pre-commit.ci] pre-commit autoupdate (#12504) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .github/workflows/tests.yml | 6 ++---- .pre-commit-config.yaml | 2 +- tools/github_actions_dependencies.sh | 2 +- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 908555af797..d491a97029c 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -4,11 +4,9 @@ concurrency: cancel-in-progress: true on: # yamllint disable-line rule:truthy push: - branches: - - '*' + branches: ["main", "maint/*"] pull_request: - branches: - - '*' + branches: ["main", "maint/*"] permissions: contents: read diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 47e6936a83b..f08e4a367c1 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.2 + rev: v0.3.3 hooks: - id: ruff name: ruff lint mne diff --git a/tools/github_actions_dependencies.sh b/tools/github_actions_dependencies.sh index fcb808fd812..d6996472acc 100755 --- a/tools/github_actions_dependencies.sh +++ b/tools/github_actions_dependencies.sh @@ -30,7 +30,7 @@ else # pip install $STD_ARGS --only-binary ":all:" --default-timeout=60 --extra-index-url https://www.riverbankcomputing.com/pypi/simple "PyQt6!=6.6.1,!=6.6.2" "PyQt6-Qt6!=6.6.1,!=6.6.2" pip install $STD_ARGS --only-binary ":all:" --default-timeout=60 "PyQt6!=6.6.1,!=6.6.2" "PyQt6-Qt6!=6.6.1,!=6.6.2" echo "NumPy/SciPy/pandas etc." - pip install $STD_ARGS --only-binary ":all:" --default-timeout=60 --extra-index-url "https://pypi.anaconda.org/scientific-python-nightly-wheels/simple" "numpy<2.0.0.dev0" "scipy>=1.12.0.dev0" "scikit-learn>=1.5.dev0" matplotlib pillow statsmodels pyarrow pandas h5py + pip install $STD_ARGS --only-binary ":all:" --default-timeout=60 --extra-index-url "https://pypi.anaconda.org/scientific-python-nightly-wheels/simple" "numpy<2.0.0.dev0" "scipy==1.12.0" "scikit-learn>=1.5.dev0" matplotlib pillow statsmodels pyarrow pandas h5py # No dipy, python-picard (needs numexpr) until they update to NumPy 2.0 compat INSTALL_KIND="test_extra" echo "OpenMEEG" From b8d9c1713bfef91e7b7bb663ec18a528a9b1691f Mon Sep 17 00:00:00 2001 From: hasrat17 <56307533+hasrat17@users.noreply.github.com> Date: Tue, 19 Mar 2024 22:00:50 +0530 Subject: [PATCH 234/405] Fixes for #12360 Replacing percent format with f-strings (#12464) Co-authored-by: Clemens Brunner Co-authored-by: Daniel McCloy Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Eric Larson --- doc/changes/devel/12464.other.rst | 2 + doc/changes/names.inc | 2 + doc/conf.py | 4 +- doc/sphinxext/flow_diagram.py | 14 +-- doc/sphinxext/mne_substitutions.py | 12 +-- mne/epochs.py | 4 +- mne/fixes.py | 16 ++-- mne/forward/_compute_forward.py | 2 +- mne/forward/_lead_dots.py | 4 +- mne/forward/_make_forward.py | 6 +- mne/forward/forward.py | 15 ++- mne/inverse_sparse/mxne_inverse.py | 8 +- mne/inverse_sparse/mxne_optim.py | 16 ++-- mne/io/artemis123/artemis123.py | 5 +- mne/io/artemis123/tests/test_artemis123.py | 7 +- mne/io/base.py | 50 ++++------ mne/io/brainvision/brainvision.py | 4 +- mne/io/cnt/cnt.py | 2 +- mne/io/ctf/ctf.py | 2 +- mne/io/ctf/info.py | 8 +- mne/io/curry/curry.py | 2 +- mne/io/egi/egimff.py | 8 +- mne/io/eyelink/tests/test_eyelink.py | 13 +-- mne/io/fieldtrip/utils.py | 9 +- mne/io/kit/coreg.py | 10 +- mne/io/kit/kit.py | 5 +- mne/io/nirx/nirx.py | 8 +- mne/label.py | 67 +++++++------ mne/minimum_norm/_eloreta.py | 7 +- mne/minimum_norm/inverse.py | 4 +- mne/minimum_norm/tests/test_inverse.py | 8 +- mne/minimum_norm/time_frequency.py | 12 +-- mne/morph.py | 36 ++++--- mne/morph_map.py | 2 +- mne/preprocessing/_fine_cal.py | 18 ++-- mne/preprocessing/_peak_finder.py | 2 +- mne/preprocessing/artifact_detection.py | 2 +- mne/preprocessing/ecg.py | 2 +- mne/preprocessing/ica.py | 12 +-- mne/preprocessing/infomax_.py | 2 +- mne/preprocessing/interpolate.py | 4 +- mne/preprocessing/maxwell.py | 75 +++++++-------- mne/preprocessing/otp.py | 8 +- .../tests/test_eeglab_infomax.py | 6 +- mne/preprocessing/tests/test_maxwell.py | 12 +-- mne/preprocessing/xdawn.py | 4 +- mne/proj.py | 8 +- mne/rank.py | 4 +- mne/report/report.py | 2 +- mne/simulation/raw.py | 40 ++++---- mne/simulation/source.py | 9 +- mne/simulation/tests/test_raw.py | 2 +- mne/source_estimate.py | 45 ++++----- mne/source_space/_source_space.py | 96 ++++++++----------- mne/stats/cluster_level.py | 18 ++-- mne/stats/regression.py | 4 +- mne/stats/tests/test_parametric.py | 4 +- mne/surface.py | 19 ++-- mne/tests/test_annotations.py | 2 +- mne/tests/test_chpi.py | 16 ++-- mne/tests/test_coreg.py | 4 +- mne/tests/test_cov.py | 4 +- mne/tests/test_dipole.py | 9 +- mne/tests/test_filter.py | 12 +-- mne/tests/test_label.py | 2 +- mne/tests/test_line_endings.py | 3 +- mne/time_frequency/_stockwell.py | 3 +- mne/time_frequency/multitaper.py | 11 +-- mne/time_frequency/tfr.py | 30 +++--- mne/transforms.py | 16 ++-- mne/utils/_bunch.py | 2 +- mne/utils/_testing.py | 15 ++- mne/utils/check.py | 36 ++++--- mne/utils/config.py | 6 +- mne/utils/dataframe.py | 2 +- mne/utils/docs.py | 18 ++-- mne/utils/mixin.py | 4 +- mne/utils/numerics.py | 47 +++++---- mne/utils/tests/test_numerics.py | 2 +- mne/viz/_3d.py | 35 +++---- mne/viz/_brain/_brain.py | 22 ++--- mne/viz/_brain/colormap.py | 4 +- mne/viz/_brain/surface.py | 4 +- mne/viz/backends/tests/test_renderer.py | 2 +- mne/viz/evoked.py | 8 +- mne/viz/misc.py | 12 +-- mne/viz/tests/test_3d_mpl.py | 2 +- mne/viz/topo.py | 8 +- mne/viz/topomap.py | 6 +- mne/viz/utils.py | 10 +- pyproject.toml | 4 +- 91 files changed, 504 insertions(+), 629 deletions(-) create mode 100644 doc/changes/devel/12464.other.rst diff --git a/doc/changes/devel/12464.other.rst b/doc/changes/devel/12464.other.rst new file mode 100644 index 00000000000..6839c4ebe61 --- /dev/null +++ b/doc/changes/devel/12464.other.rst @@ -0,0 +1,2 @@ +Replacing percent format with f-strings format specifiers , by :newcontrib:`Hasrat Ali Arzoo`. + diff --git a/doc/changes/names.inc b/doc/changes/names.inc index d3dfd61b916..076c5933568 100644 --- a/doc/changes/names.inc +++ b/doc/changes/names.inc @@ -206,6 +206,8 @@ .. _Hari Bharadwaj: https://github.com/haribharadwaj +.. _Hasrat Ali Arzoo: https://github.com/hasrat17 + .. _Henrich Kolkhorst: https://github.com/hekolk .. _Hongjiang Ye: https://github.com/hongjiang-ye diff --git a/doc/conf.py b/doc/conf.py index a00a34debc3..b2dbe387f27 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -683,7 +683,9 @@ def append_attr_meth_examples(app, what, name, obj, options, lines): if what in ("attribute", "method"): size = os.path.getsize( os.path.join( - os.path.dirname(__file__), "generated", "%s.examples" % (name,) + os.path.dirname(__file__), + "generated", + f"{name}.examples", ) ) if size > 0: diff --git a/doc/sphinxext/flow_diagram.py b/doc/sphinxext/flow_diagram.py index ba374c60f88..cefe6713a7d 100644 --- a/doc/sphinxext/flow_diagram.py +++ b/doc/sphinxext/flow_diagram.py @@ -12,18 +12,14 @@ sensor_color = "#7bbeca" source_color = "#ff6347" -legend = """ -< +legend = f""" +< - - -
+
Sensor (M/EEG) space
+
Source (brain) space
>""" % ( - edge_size, - sensor_color, - source_color, -) +
>""" legend = "".join(legend.split("\n")) nodes = dict( diff --git a/doc/sphinxext/mne_substitutions.py b/doc/sphinxext/mne_substitutions.py index bd415fc67f9..23196e795f6 100644 --- a/doc/sphinxext/mne_substitutions.py +++ b/doc/sphinxext/mne_substitutions.py @@ -29,18 +29,14 @@ def run(self, **kwargs): # noqa: D102 ): keys.append(key) rst = "- " + "\n- ".join( - "``%r``: **%s** (scaled by %g to plot in *%s*)" - % ( - key, - DEFAULTS["titles"][key], - DEFAULTS["scalings"][key], - DEFAULTS["units"][key], - ) + f"``{repr(key)}``: **{DEFAULTS['titles'][key]}** " + f"(scaled by {DEFAULTS['scalings'][key]} to " + f"plot in *{DEFAULTS['units'][key]}*)" for key in keys ) else: raise self.error( - "MNE directive unknown in %s: %r" + "MNE directive unknown in %s: %r" # noqa: UP031 % ( env.doc2path(env.docname, base=None), self.arguments[0], diff --git a/mne/epochs.py b/mne/epochs.py index 7006fb10f3e..14a0092c07a 100644 --- a/mne/epochs.py +++ b/mne/epochs.py @@ -3860,8 +3860,8 @@ def _is_good( bad_names = [ch_names[idx[i]] for i in idx_deltas] if not has_printed: logger.info( - " Rejecting %s epoch based on %s : " - "%s" % (t, name, bad_names) + f" Rejecting {t} epoch based on {name} : " + f"{bad_names}" ) has_printed = True if not full_report: diff --git a/mne/fixes.py b/mne/fixes.py index 55e56261866..2af4eba73b9 100644 --- a/mne/fixes.py +++ b/mne/fixes.py @@ -98,7 +98,7 @@ def _safe_svd(A, **kwargs): except np.linalg.LinAlgError as exp: from .utils import warn - warn("SVD error (%s), attempting to use GESVD instead of GESDD" % (exp,)) + warn(f"SVD error ({exp}), attempting to use GESVD instead of GESDD") return linalg.svd(A, lapack_driver="gesvd", **kwargs) @@ -192,8 +192,8 @@ def _get_param_names(cls): "scikit-learn estimators should always " "specify their parameters in the signature" " of their __init__ (no varargs)." - " %s with constructor %s doesn't " - " follow this convention." % (cls, init_signature) + f" {cls} with constructor {init_signature} doesn't " + " follow this convention." ) # Extract and sort argument names excluding 'self' return sorted([p.name for p in parameters]) @@ -264,9 +264,9 @@ def set_params(self, **params): name, sub_name = split if name not in valid_params: raise ValueError( - "Invalid parameter %s for estimator %s. " + f"Invalid parameter {name} for estimator {self}. " "Check the list of available parameters " - "with `estimator.get_params().keys()`." % (name, self) + "with `estimator.get_params().keys()`." ) sub_object = valid_params[name] sub_object.set_params(**{sub_name: value}) @@ -274,10 +274,10 @@ def set_params(self, **params): # simple objects case if key not in valid_params: raise ValueError( - "Invalid parameter %s for estimator %s. " + f"Invalid parameter {key} for estimator " + f"{self.__class__.__name__}. " "Check the list of available parameters " "with `estimator.get_params().keys()`." - % (key, self.__class__.__name__) ) setattr(self, key, value) return self @@ -287,7 +287,7 @@ def __repr__(self): # noqa: D105 pprint(self.get_params(deep=False), params) params.seek(0) class_name = self.__class__.__name__ - return "%s(%s)" % (class_name, params.read().strip()) + return f"{class_name}({params.read().strip()})" # __getstate__ and __setstate__ are omitted because they only contain # conditionals that are not satisfied by our objects (e.g., diff --git a/mne/forward/_compute_forward.py b/mne/forward/_compute_forward.py index 6c4e157f7f9..641f315239a 100644 --- a/mne/forward/_compute_forward.py +++ b/mne/forward/_compute_forward.py @@ -661,7 +661,7 @@ def _magnetic_dipole_field_vec(rrs, coils, too_close="raise"): rmags, cosmags, ws, bins = _triage_coils(coils) fwd, min_dist = _compute_mdfv(rrs, rmags, cosmags, ws, bins, too_close) if min_dist < _MIN_DIST_LIMIT: - msg = "Coil too close (dist = %g mm)" % (min_dist * 1000,) + msg = f"Coil too close (dist = {min_dist * 1000:g} mm)" if too_close == "raise": raise RuntimeError(msg) func = warn if too_close == "warning" else logger.info diff --git a/mne/forward/_lead_dots.py b/mne/forward/_lead_dots.py index b158f6db07f..3b2118de409 100644 --- a/mne/forward/_lead_dots.py +++ b/mne/forward/_lead_dots.py @@ -69,12 +69,12 @@ def _get_legen_table( # Updated due to API change (GH 1167) os.makedirs(fname) if ch_type == "meg": - fname = op.join(fname, "legder_%s_%s.bin" % (n_coeff, n_interp)) + fname = op.join(fname, f"legder_{n_coeff}_{n_interp}.bin") leg_fun = _get_legen_der extra_str = " derivative" lut_shape = (n_interp + 1, n_coeff, 3) else: # 'eeg' - fname = op.join(fname, "legval_%s_%s.bin" % (n_coeff, n_interp)) + fname = op.join(fname, f"legval_{n_coeff}_{n_interp}.bin") leg_fun = _get_legen extra_str = "" lut_shape = (n_interp + 1, n_coeff) diff --git a/mne/forward/_make_forward.py b/mne/forward/_make_forward.py index 812be7daf7e..24131ad4a10 100644 --- a/mne/forward/_make_forward.py +++ b/mne/forward/_make_forward.py @@ -299,8 +299,8 @@ def _setup_bem(bem, bem_extra, neeg, mri_head_t, allow_none=False, verbose=None) else: if bem["surfs"][0]["coord_frame"] != FIFF.FIFFV_COORD_MRI: raise RuntimeError( - "BEM is in %s coordinates, should be in MRI" - % (_coord_frame_name(bem["surfs"][0]["coord_frame"]),) + f'BEM is in {_coord_frame_name(bem["surfs"][0]["coord_frame"])} ' + 'coordinates, should be in MRI' ) if neeg > 0 and len(bem["surfs"]) == 1: raise RuntimeError( @@ -693,7 +693,7 @@ def make_forward_solution( logger.info("MRI -> head transform : %s" % trans) logger.info("Measurement data : %s" % info_extra) if isinstance(bem, ConductorModel) and bem["is_sphere"]: - logger.info("Sphere model : origin at %s mm" % (bem["r0"],)) + logger.info(f"Sphere model : origin at {bem['r0']} mm") logger.info("Standard field computations") else: logger.info("Conductor model : %s" % bem_extra) diff --git a/mne/forward/forward.py b/mne/forward/forward.py index dc39a58bd8f..e3ab3d238f4 100644 --- a/mne/forward/forward.py +++ b/mne/forward/forward.py @@ -1258,7 +1258,7 @@ def compute_orient_prior(forward, loose="auto", verbose=None): if any(v > 0.0 for v in loose.values()): raise ValueError( "loose must be 0. with forward operator " - "with fixed orientation, got %s" % (loose,) + f"with fixed orientation, got {loose}" ) return orient_prior if all(v == 1.0 for v in loose.values()): @@ -1269,7 +1269,7 @@ def compute_orient_prior(forward, loose="auto", verbose=None): raise ValueError( "Forward operator is not oriented in surface " "coordinates. loose parameter should be 1. " - "not %s." % (loose,) + f"not {loose}." ) start = 0 logged = dict() @@ -1419,13 +1419,12 @@ def compute_depth_prior( if isinstance(limit_depth_chs, str): if limit_depth_chs != "whiten": raise ValueError( - 'limit_depth_chs, if str, must be "whiten", got ' - "%s" % (limit_depth_chs,) + f'limit_depth_chs, if str, must be "whiten", got {limit_depth_chs}' ) if not isinstance(noise_cov, Covariance): raise ValueError( 'With limit_depth_chs="whiten", noise_cov must be' - " a Covariance, got %s" % (type(noise_cov),) + f" a Covariance, got {type(noise_cov)}" ) if combine_xyz is not False: # private / expert option _check_option("combine_xyz", combine_xyz, ("fro", "spectral")) @@ -1491,7 +1490,7 @@ def compute_depth_prior( " limit = %d/%d = %f" % (n_limit + 1, len(d), np.sqrt(limit / ws[0])) ) scale = 1.0 / limit - logger.info(" scale = %g exp = %g" % (scale, exp)) + logger.info(f" scale = {scale:g} exp = {exp:g}") w = np.minimum(w / limit, 1) depth_prior = w**exp @@ -1513,8 +1512,8 @@ def _stc_src_sel( del stc if not len(src) == len(vertices): raise RuntimeError( - "Mismatch between number of source spaces (%s) and " - "STC vertices (%s)" % (len(src), len(vertices)) + f"Mismatch between number of source spaces ({len(src)}) and " + f"STC vertices ({len(vertices)})" ) src_sels, stc_sels, out_vertices = [], [], [] src_offset = stc_offset = 0 diff --git a/mne/inverse_sparse/mxne_inverse.py b/mne/inverse_sparse/mxne_inverse.py index 9a2d8c4b5c8..703a0d30ca4 100644 --- a/mne/inverse_sparse/mxne_inverse.py +++ b/mne/inverse_sparse/mxne_inverse.py @@ -55,9 +55,7 @@ def _prepare_weights(forward, gain, source_weighting, weights, weights_min): weights = np.max(np.abs(weights.data), axis=1) weights_max = np.max(weights) if weights_min > weights_max: - raise ValueError( - "weights_min > weights_max (%s > %s)" % (weights_min, weights_max) - ) + raise ValueError(f"weights_min > weights_max ({weights_min} > {weights_max})") weights_min = weights_min / weights_max weights = weights / weights_max n_dip_per_pos = 1 if is_fixed_orient(forward) else 3 @@ -813,7 +811,7 @@ def tf_mixed_norm( if len(tstep) != len(wsize): raise ValueError( "The same number of window sizes and steps must be " - "passed. Got tstep = %s and wsize = %s" % (tstep, wsize) + f"passed. Got tstep = {tstep} and wsize = {wsize}" ) forward, gain, gain_info, whitener, source_weighting, mask = _prepare_gain( @@ -1090,7 +1088,7 @@ def _compute_sure_val(coef1, coef2, gain, M, sigma, delta, eps): for i, (coef1, coef2) in enumerate(zip(coefs_grid_1, coefs_grid_2)): sure_path[i] = _compute_sure_val(coef1, coef2, gain, M, sigma, delta, eps) if verbose: - logger.info("alpha %s :: sure %s" % (alpha_grid[i], sure_path[i])) + logger.info(f"alpha {alpha_grid[i]} :: sure {sure_path[i]}") best_alpha_ = alpha_grid[np.argmin(sure_path)] X = coefs_grid_1[np.argmin(sure_path)] diff --git a/mne/inverse_sparse/mxne_optim.py b/mne/inverse_sparse/mxne_optim.py index a4c63a557b5..dbac66a96f9 100644 --- a/mne/inverse_sparse/mxne_optim.py +++ b/mne/inverse_sparse/mxne_optim.py @@ -243,7 +243,7 @@ def _mixed_norm_solver_bcd( ) if gap < tol: - logger.debug("Convergence reached ! (gap: %s < %s)" % (gap, tol)) + logger.debug(f"Convergence reached ! (gap: {gap} < {tol})") break # using Anderson acceleration of the primal variable for faster @@ -525,7 +525,7 @@ def mixed_norm_solver( ) ) if gap < tol: - logger.info("Convergence reached ! (gap: %s < %s)" % (gap, tol)) + logger.info(f"Convergence reached ! (gap: {gap} < {tol})") break # add sources if not last iteration @@ -545,7 +545,7 @@ def mixed_norm_solver( idx = np.searchsorted(idx_active_set, idx_old_active_set) X_init[idx] = X else: - warn("Did NOT converge ! (gap: %s > %s)" % (gap, tol)) + warn(f"Did NOT converge ! (gap: {gap} > {tol})") else: X, active_set, E = l21_solver( M, G, alpha, lc, maxit=maxit, tol=tol, n_orient=n_orient, init=None @@ -640,8 +640,8 @@ def gprime(w): if weight_init is not None and weight_init.shape != (G.shape[1],): raise ValueError( - "Wrong dimension for weight initialization. Got %s. " - "Expected %s." % (weight_init.shape, (G.shape[1],)) + f"Wrong dimension for weight initialization. Got {weight_init.shape}. " + f"Expected {(G.shape[1],)}." ) weights = weight_init if weight_init is not None else np.ones(G.shape[1]) @@ -1270,7 +1270,7 @@ def _tf_mixed_norm_solver_bcd_( "\n Iteration %d :: n_active %d" % (i + 1, np.sum(active_set) / n_orient) ) - logger.info(" dgap %.2e :: p_obj %f :: d_obj %f" % (gap, p_obj, d_obj)) + logger.info(f" dgap {gap:.2e} :: p_obj {p_obj} :: d_obj {d_obj}") if converged: break @@ -1504,7 +1504,7 @@ def tf_mixed_norm_solver( if len(tstep) != len(wsize): raise ValueError( "The same number of window sizes and steps must be " - "passed. Got tstep = %s and wsize = %s" % (tstep, wsize) + f"passed. Got tstep = {tstep} and wsize = {wsize}" ) n_steps = np.ceil(M.shape[1] / tstep.astype(float)).astype(int) @@ -1624,7 +1624,7 @@ def iterative_tf_mixed_norm_solver( if len(tstep) != len(wsize): raise ValueError( "The same number of window sizes and steps must be " - "passed. Got tstep = %s and wsize = %s" % (tstep, wsize) + f"passed. Got tstep = {tstep} and wsize = {wsize}" ) n_steps = np.ceil(n_times / tstep.astype(float)).astype(int) diff --git a/mne/io/artemis123/artemis123.py b/mne/io/artemis123/artemis123.py index 4ecd524f73d..99b00d36f45 100644 --- a/mne/io/artemis123/artemis123.py +++ b/mne/io/artemis123/artemis123.py @@ -131,10 +131,7 @@ def _get_artemis123_info(fname, pos_fname=None): tmp[k] = v header_info["channels"].append(tmp) elif sectionFlag == 3: - header_info["comments"] = "%s%s" % ( - header_info["comments"], - line.strip(), - ) + header_info["comments"] = f"{header_info['comments']}{line.strip()}" elif sectionFlag == 4: header_info["num_samples"] = int(line.strip()) elif sectionFlag == 5: diff --git a/mne/io/artemis123/tests/test_artemis123.py b/mne/io/artemis123/tests/test_artemis123.py index ec4d3d4017f..9b002c7b712 100644 --- a/mne/io/artemis123/tests/test_artemis123.py +++ b/mne/io/artemis123/tests/test_artemis123.py @@ -36,11 +36,10 @@ 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, "%0.3f > %0.3f mm translation" % ( - 1000 * dist, - 1000 * dist_tol, + assert dist <= dist_tol, ( + f"{1000 * dist:0.3f} > {1000 * dist_tol:0.3f} " "mm translation" ) - assert angle <= angle_tol, "%0.3f > %0.3f° rotation" % (angle, angle_tol) + assert angle <= angle_tol, f"{angle:0.3f} > {angle_tol:0.3f}° rotation" @pytest.mark.timeout(60) # ~25 s on Travis Linux OpenBLAS diff --git a/mne/io/base.py b/mne/io/base.py index e68b49af3da..ed909e5658f 100644 --- a/mne/io/base.py +++ b/mne/io/base.py @@ -414,8 +414,8 @@ def _read_segment( if isinstance(data_buffer, np.ndarray): if data_buffer.shape != data_shape: raise ValueError( - "data_buffer has incorrect shape: %s != %s" - % (data_buffer.shape, data_shape) + f"data_buffer has incorrect shape: " + f"{data_buffer.shape} != {data_shape}" ) data = data_buffer else: @@ -1516,13 +1516,13 @@ def crop(self, tmin=0.0, tmax=None, include_tmax=True, *, verbose=None): tmax = max_time if tmin > tmax: - raise ValueError("tmin (%s) must be less than tmax (%s)" % (tmin, tmax)) + raise ValueError(f"tmin ({tmin}) must be less than tmax ({tmax})") if tmin < 0.0: - raise ValueError("tmin (%s) must be >= 0" % (tmin,)) + raise ValueError(f"tmin ({tmin}) must be >= 0") elif tmax - int(not include_tmax) / self.info["sfreq"] > max_time: raise ValueError( - "tmax (%s) must be less than or equal to the max " - "time (%0.4f s)" % (tmax, max_time) + f"tmax ({tmax}) must be less than or equal to the max " + f"time ({max_time:0.4f} s)" ) smin, smax = np.where( @@ -1808,9 +1808,7 @@ def _tmin_tmax_to_start_stop(self, tmin, tmax): stop = self.time_as_index(float(tmax), use_rounding=True)[0] + 1 stop = min(stop, self.last_samp - self.first_samp + 1) if stop <= start or stop <= 0: - raise ValueError( - "tmin (%s) and tmax (%s) yielded no samples" % (tmin, tmax) - ) + raise ValueError(f"tmin ({tmin}) and tmax ({tmax}) yielded no samples") return start, stop @copy_function_doc_to_method_doc(plot_raw) @@ -2096,15 +2094,12 @@ def __repr__(self): # noqa: D105 name = self.filenames[0] name = "" if name is None else op.basename(name) + ", " size_str = str(sizeof_fmt(self._size)) # str in case it fails -> None - size_str += ", data%s loaded" % ("" if self.preload else " not") - s = "%s%s x %s (%0.1f s), ~%s" % ( - name, - len(self.ch_names), - self.n_times, - self.times[-1], - size_str, + size_str += f", data{'' if self.preload else ' not'} loaded" + s = ( + f"{name}{len(self.ch_names)} x {self.n_times} " + f"({self.times[-1]:0.1f} s), ~{size_str}" ) - return "<%s | %s>" % (self.__class__.__name__, s) + return f"<{self.__class__.__name__} | {s}>" @repr_html def _repr_html_(self, caption=None): @@ -2162,8 +2157,8 @@ def add_events(self, events, stim_channel=None, replace=False): idx = events[:, 0].astype(int) if np.any(idx < self.first_samp) or np.any(idx > self.last_samp): raise ValueError( - "event sample numbers must be between %s and %s" - % (self.first_samp, self.last_samp) + f"event sample numbers must be between {self.first_samp} " + f"and {self.last_samp}" ) if not all(idx == events[:, 0]): raise ValueError("event sample numbers must be integers") @@ -2839,17 +2834,12 @@ def _write_raw_data( # This should occur on the first buffer write of the file, so # we should mention the space required for the meas info raise ValueError( - "buffer size (%s) is too large for the given split size (%s) " - "by %s bytes after writing info (%s) and leaving enough space " - 'for end tags (%s): decrease "buffer_size_sec" or increase ' - '"split_size".' - % ( - this_buff_size_bytes, - split_size, - overage, - pos_prev, - _NEXT_FILE_BUFFER, - ) + f"buffer size ({this_buff_size_bytes}) is too large for the " + f"given split size ({split_size}) " + f"by {overage} bytes after writing info ({pos_prev}) and " + "leaving enough space " + f'for end tags ({_NEXT_FILE_BUFFER}): decrease "buffer_size_sec" ' + 'or increase "split_size".' ) new_start = last diff --git a/mne/io/brainvision/brainvision.py b/mne/io/brainvision/brainvision.py index 9a8531a22d1..1942744afe3 100644 --- a/mne/io/brainvision/brainvision.py +++ b/mne/io/brainvision/brainvision.py @@ -867,8 +867,8 @@ def _get_hdr_info(hdr_fname, eog, misc, scale): nyquist = "" warn( "Channels contain different lowpass filters. " - "Highest (weakest) filter setting (%0.2f Hz%s) " - "will be stored." % (info["lowpass"], nyquist) + f"Highest (weakest) filter setting ({info['lowpass']:0.2f} " + f"Hz{nyquist}) will be stored." ) # Creates a list of dicts of eeg channels for raw.info diff --git a/mne/io/cnt/cnt.py b/mne/io/cnt/cnt.py index c695dfb0e86..5e5c60ee1a1 100644 --- a/mne/io/cnt/cnt.py +++ b/mne/io/cnt/cnt.py @@ -292,7 +292,7 @@ def _get_cnt_info(input_fname, eog, ecg, emg, misc, data_format, date_format, he fid.seek(205) session_label = read_str(fid, 20) - session_date = "%s %s" % (read_str(fid, 10), read_str(fid, 12)) + session_date = f"{read_str(fid, 10)} {read_str(fid, 12)}" meas_date = _session_date_2_meas_date(session_date, date_format) fid.seek(370) diff --git a/mne/io/ctf/ctf.py b/mne/io/ctf/ctf.py index c50459e0e0a..1d4887c915a 100644 --- a/mne/io/ctf/ctf.py +++ b/mne/io/ctf/ctf.py @@ -227,7 +227,7 @@ def _clean_names_inst(inst): def _get_sample_info(fname, res4, system_clock): """Determine the number of valid samples.""" - logger.info("Finding samples for %s: " % (fname,)) + logger.info(f"Finding samples for {fname}: ") if CTF.SYSTEM_CLOCK_CH in res4["ch_names"]: clock_ch = res4["ch_names"].index(CTF.SYSTEM_CLOCK_CH) else: diff --git a/mne/io/ctf/info.py b/mne/io/ctf/info.py index d49c9709f9c..791fdceaf51 100644 --- a/mne/io/ctf/info.py +++ b/mne/io/ctf/info.py @@ -171,8 +171,8 @@ def _check_comp_ch(cch, kind, desired=None): desired = cch["grad_order_no"] if cch["grad_order_no"] != desired: raise RuntimeError( - "%s channel with inconsistent compensation " - "grade %s, should be %s" % (kind, cch["grad_order_no"], desired) + f"{kind} channel with inconsistent compensation " + f"grade {cch['grad_order_no']}, should be {desired}" ) return desired @@ -217,8 +217,8 @@ def _convert_channel_info(res4, t, use_eeg_pos): if cch["sensor_type_index"] != CTF.CTFV_MEG_CH: text += " ref" warn( - "%s channel %s did not have position assigned, so " - "it was changed to a MISC channel" % (text, ch["ch_name"]) + f"{text} channel {ch['ch_name']} did not have position " + "assigned, so it was changed to a MISC channel" ) continue ch["unit"] = FIFF.FIFF_UNIT_T diff --git a/mne/io/curry/curry.py b/mne/io/curry/curry.py index 3b3d5e711d3..3d0fb9afbca 100644 --- a/mne/io/curry/curry.py +++ b/mne/io/curry/curry.py @@ -425,7 +425,7 @@ def _make_trans_dig(curry_paths, info, curry_dev_dev_t): ) ) dist = 1000 * np.linalg.norm(unknown_curry_t["trans"][:3, 3]) - logger.info(" Fit a %0.1f° rotation, %0.1f mm translation" % (angle, dist)) + logger.info(f" Fit a {angle:0.1f}° rotation, {dist:0.1f} mm translation") unknown_dev_t = combine_transforms( unknown_curry_t, curry_dev_dev_t, "unknown", "meg" ) diff --git a/mne/io/egi/egimff.py b/mne/io/egi/egimff.py index e241208d1cc..3a039b0c784 100644 --- a/mne/io/egi/egimff.py +++ b/mne/io/egi/egimff.py @@ -64,7 +64,7 @@ def _read_mff_header(filepath): record_time, ) if g is None: - raise RuntimeError("Could not parse recordTime %r" % (record_time,)) + raise RuntimeError(f"Could not parse recordTime {repr(record_time)}") frac = g.groups()[0] assert len(frac) in (6, 9) and all(f.isnumeric() for f in frac) # regex div = 1000 if len(frac) == 6 else 1000000 @@ -72,7 +72,7 @@ def _read_mff_header(filepath): # convert from times in µS to samples for ei, e in enumerate(epochs[key]): if e % div != 0: - raise RuntimeError("Could not parse epoch time %s" % (e,)) + raise RuntimeError(f"Could not parse epoch time {e}") epochs[key][ei] = e // div epochs[key] = np.array(epochs[key], np.uint64) # I guess they refer to times in milliseconds? @@ -104,7 +104,7 @@ def _read_mff_header(filepath): if bad: raise RuntimeError( "EGI epoch first/last samps could not be parsed:\n" - "%s\n%s" % (list(epochs["first_samps"]), list(epochs["last_samps"])) + f'{list(epochs["first_samps"])}\n{list(epochs["last_samps"])}' ) summaryinfo.update(epochs) # index which samples in raw are actually readable from disk (i.e., not @@ -156,7 +156,7 @@ def _read_mff_header(filepath): if not same_blocks: raise RuntimeError( "PNS and signals samples did not match:\n" - "%s\nvs\n%s" % (list(pns_samples), list(signal_samples)) + f"{list(pns_samples)}\nvs\n{list(signal_samples)}" ) pns_file = op.join(filepath, "pnsSet.xml") diff --git a/mne/io/eyelink/tests/test_eyelink.py b/mne/io/eyelink/tests/test_eyelink.py index dd3a32c270d..7f57596ac38 100644 --- a/mne/io/eyelink/tests/test_eyelink.py +++ b/mne/io/eyelink/tests/test_eyelink.py @@ -234,19 +234,16 @@ def _simulate_eye_tracking_data(in_file, out_file): else: fp.write("%s\n" % line) - fp.write("%s\n" % "START\t7452389\tRIGHT\tSAMPLES\tEVENTS") - fp.write("%s\n" % new_samples_line) + fp.write("START\t7452389\tRIGHT\tSAMPLES\tEVENTS\n") + fp.write(f"{new_samples_line}\n") for timestamp in np.arange(7452389, 7453390): # simulate a second block fp.write( - "%s\n" - % ( - f"{timestamp}\t-2434.0\t-1760.0\t840.0\t100\t20\t45\t45\t127.0\t" - "...\t1497\t5189\t512.5\t............." - ) + f"{timestamp}\t-2434.0\t-1760.0\t840.0\t100\t20\t45\t45\t127.0\t" + "...\t1497\t5189\t512.5\t.............\n" ) - fp.write("%s\n" % "END\t7453390\tRIGHT\tSAMPLES\tEVENTS") + fp.write("END\t7453390\tRIGHT\tSAMPLES\tEVENTS\n") @requires_testing_data diff --git a/mne/io/fieldtrip/utils.py b/mne/io/fieldtrip/utils.py index c4950d45bea..9a4274f6a43 100644 --- a/mne/io/fieldtrip/utils.py +++ b/mne/io/fieldtrip/utils.py @@ -54,9 +54,8 @@ def _create_info(ft_struct, raw_info): if missing_channels: warn( "The following channels are present in the FieldTrip data " - "but cannot be found in the provided info: %s.\n" + f"but cannot be found in the provided info: {str(missing_channels)}.\n" "These channels will be removed from the resulting data!" - % (str(missing_channels),) ) missing_chan_idx = [ch_names.index(ch) for ch in missing_channels] @@ -174,8 +173,8 @@ def _create_info_chs_dig(ft_struct): cur_ch["coil_type"] = FIFF.FIFFV_COIL_NONE else: warn( - "Cannot guess the correct type of channel %s. Making " - "it a MISC channel." % (cur_channel_label,) + f"Cannot guess the correct type of channel {cur_channel_label}. " + "Making it a MISC channel." ) cur_ch["kind"] = FIFF.FIFFV_MISC_CH cur_ch["coil_type"] = FIFF.FIFFV_COIL_NONE @@ -363,7 +362,7 @@ def _process_channel_meg(cur_ch, grad): cur_ch["coil_type"] = FIFF.FIFFV_COIL_AXIAL_GRAD_5CM cur_ch["unit"] = FIFF.FIFF_UNIT_T else: - raise RuntimeError("Unexpected coil type: %s." % (chantype,)) + raise RuntimeError(f"Unexpected coil type: {chantype}.") cur_ch["coord_frame"] = FIFF.FIFFV_COORD_HEAD diff --git a/mne/io/kit/coreg.py b/mne/io/kit/coreg.py index 7a113c9f3e6..f58f1e29acf 100644 --- a/mne/io/kit/coreg.py +++ b/mne/io/kit/coreg.py @@ -86,7 +86,7 @@ def read_mrk(fname): # check output mrk_points = np.asarray(mrk_points) if mrk_points.shape != (5, 3): - err = "%r is no marker file, shape is " "%s" % (fname, mrk_points.shape) + err = f"{repr(fname)} is no marker file, shape is {mrk_points.shape}" raise ValueError(err) return mrk_points @@ -163,14 +163,12 @@ def _set_dig_kit(mrk, elp, hsp, eeg): elp_points = _read_dig_kit(elp) if len(elp_points) != 8: raise ValueError( - "File %r should contain 8 points; got shape " - "%s." % (elp, elp_points.shape) + f"File {repr(elp)} should contain 8 points; got shape " + f"{elp_points.shape}." ) elp = elp_points elif len(elp) not in (6, 7, 8): - raise ValueError( - "ELP should contain 6 ~ 8 points; got shape " "%s." % (elp.shape,) - ) + raise ValueError(f"ELP should contain 6 ~ 8 points; got shape {elp.shape}.") if isinstance(mrk, (str, Path, PathLike)): mrk = read_mrk(mrk) diff --git a/mne/io/kit/kit.py b/mne/io/kit/kit.py index c89ee66c253..737222b0090 100644 --- a/mne/io/kit/kit.py +++ b/mne/io/kit/kit.py @@ -535,9 +535,8 @@ def get_kit_info(rawfile, allow_unknown_format, standardize_names=None, verbose= else: raise UnsupportedKITFormat( version_string, - "SQD file format %s is not officially supported. " - "Set allow_unknown_format=True to load it anyways." - % (version_string,), + f"SQD file format {version_string} is not officially supported. " + "Set allow_unknown_format=True to load it anyways.", ) sysid = np.fromfile(fid, INT32, 1)[0] diff --git a/mne/io/nirx/nirx.py b/mne/io/nirx/nirx.py index 0eb2565cb32..52826f266f3 100644 --- a/mne/io/nirx/nirx.py +++ b/mne/io/nirx/nirx.py @@ -101,7 +101,7 @@ def __init__(self, fname, saturated, preload=False, verbose=None): fname = str(_check_fname(fname, "read", True, "fname", need_dir=True)) - json_config = glob.glob("%s/*%s" % (fname, "config.json")) + json_config = glob.glob(f"{fname}/*{'config.json'}") if len(json_config): is_aurora = True else: @@ -130,7 +130,7 @@ def __init__(self, fname, saturated, preload=False, verbose=None): "config.txt", "probeInfo.mat", ) - n_dat = len(glob.glob("%s/*%s" % (fname, "dat"))) + n_dat = len(glob.glob(f"{fname}/*{'dat'}")) if n_dat != 1: warn( "A single dat file was expected in the specified path, " @@ -143,7 +143,7 @@ def __init__(self, fname, saturated, preload=False, verbose=None): files = dict() nan_mask = dict() for key in keys: - files[key] = glob.glob("%s/*%s" % (fname, key)) + files[key] = glob.glob(f"{fname}/*{key}") fidx = 0 if len(files[key]) != 1: if key not in ("wl1", "wl2"): @@ -202,7 +202,7 @@ def __init__(self, fname, saturated, preload=False, verbose=None): if hdr["GeneralInfo"]["NIRStar"] not in ['"15.0"', '"15.2"', '"15.3"']: raise RuntimeError( "MNE does not support this NIRStar version" - " (%s)" % (hdr["GeneralInfo"]["NIRStar"],) + f" ({hdr['GeneralInfo']['NIRStar']})" ) if ( "NIRScout" not in hdr["GeneralInfo"]["Device"] diff --git a/mne/label.py b/mne/label.py index 0fba226d4a4..5c8a1b8ca30 100644 --- a/mne/label.py +++ b/mne/label.py @@ -335,13 +335,13 @@ def __add__(self, other): if self.subject != other.subject: raise ValueError( "Label subject parameters must match, got " - '"%s" and "%s". Consider setting the ' + f'"{self.subject}" and "{other.subject}". Consider setting the ' "subject parameter on initialization, or " "setting label.subject manually before " - "combining labels." % (self.subject, other.subject) + "combining labels." ) if self.hemi != other.hemi: - name = "%s + %s" % (self.name, other.name) + name = f"{self.name} + {other.name}" if self.hemi == "lh": lh, rh = self.copy(), other.copy() else: @@ -357,8 +357,8 @@ def __add__(self, other): other_dup = [np.where(other.vertices == d)[0][0] for d in duplicates] if not np.all(self.pos[self_dup] == other.pos[other_dup]): err = ( - "Labels %r and %r: vertices overlap but differ in " - "position values" % (self.name, other.name) + f"Labels {repr(self.name)} and {repr(other.name)}: vertices " + "overlap but differ in position values" ) raise ValueError(err) @@ -383,11 +383,11 @@ def __add__(self, other): indcs = np.argsort(vertices) vertices, pos, values = vertices[indcs], pos[indcs, :], values[indcs] - comment = "%s + %s" % (self.comment, other.comment) + comment = f"{self.comment} + {other.comment}" name0 = self.name if self.name else "unnamed" name1 = other.name if other.name else "unnamed" - name = "%s + %s" % (name0, name1) + name = f"{name0} + {name1}" color = _blend_colors(self.color, other.color) @@ -408,10 +408,10 @@ def __sub__(self, other): if self.subject != other.subject: raise ValueError( "Label subject parameters must match, got " - '"%s" and "%s". Consider setting the ' + f'"{self.subject}" and "{other.subject}". Consider setting the ' "subject parameter on initialization, or " "setting label.subject manually before " - "combining labels." % (self.subject, other.subject) + "combining labels." ) if self.hemi == other.hemi: @@ -419,7 +419,7 @@ def __sub__(self, other): else: keep = np.arange(len(self.vertices)) - name = "%s - %s" % (self.name or "unnamed", other.name or "unnamed") + name = f'{self.name or "unnamed"} - {other.name or "unnamed"}' return Label( self.vertices[keep], self.pos[keep], @@ -870,7 +870,7 @@ def center_of_mass( .. footbibliography:: """ if not isinstance(surf, str): - raise TypeError("surf must be a string, got %s" % (type(surf),)) + raise TypeError(f"surf must be a string, got {type(surf)}") subject = _check_subject(self.subject, subject) if np.any(self.values < 0): raise ValueError("Cannot compute COM with negative values") @@ -980,7 +980,7 @@ def _get_label_src(label, src): if src.kind != "surface": raise RuntimeError( "Cannot operate on SourceSpaces that are not " - "surface type, got %s" % (src.kind,) + f"surface type, got {src.kind}" ) if label.hemi == "lh": hemi_src = src[0] @@ -1020,8 +1020,7 @@ class BiHemiLabel: def __init__(self, lh, rh, name=None, color=None): if lh.subject != rh.subject: raise ValueError( - "lh.subject (%s) and rh.subject (%s) must " - "agree" % (lh.subject, rh.subject) + f"lh.subject ({lh.subject}) and rh.subject ({rh.subject}) must agree" ) self.lh = lh self.rh = rh @@ -1061,7 +1060,7 @@ def __add__(self, other): else: raise TypeError("Need: Label or BiHemiLabel. Got: %r" % other) - name = "%s + %s" % (self.name, other.name) + name = f"{self.name} + {other.name}" color = _blend_colors(self.color, other.color) return BiHemiLabel(lh, rh, name, color) @@ -1084,7 +1083,7 @@ def __sub__(self, other): elif len(rh.vertices) == 0: return lh else: - name = "%s - %s" % (self.name, other.name) + name = f"{self.name} - {other.name}" return BiHemiLabel(lh, rh, name, self.color) @@ -1144,7 +1143,7 @@ def read_label(filename, subject=None, color=None, *, verbose=None): basename_ = basename[:-6] else: basename_ = basename[:-9] - name = "%s-%s" % (basename_, hemi) + name = f"{basename_}-{hemi}" # read the file with open(filename) as fid: @@ -1240,9 +1239,8 @@ def _prep_label_split(label, subject=None, subjects_dir=None): pass elif subject != label.subject: raise ValueError( - "The label specifies a different subject (%r) from " - "the subject parameter (%r)." % label.subject, - subject, + f"The label specifies a different subject ({repr(label.subject)}) from " + f"the subject parameter ({repr(subject)})." ) return label, subject, subjects_dir @@ -1296,7 +1294,7 @@ def _split_label_contig(label_to_split, subject=None, subjects_dir=None): else: basename = label_to_split.name name_ext = "" - name_pattern = "%s_div%%i%s" % (basename, name_ext) + name_pattern = f"{basename}_div%i{name_ext}" names = tuple(name_pattern % i for i in range(1, n_parts + 1)) # Colors @@ -1368,7 +1366,7 @@ def split_label(label, parts=2, subject=None, subjects_dir=None, freesurfer=Fals else: basename = label.name name_ext = "" - name_pattern = "%s_div%%i%s" % (basename, name_ext) + name_pattern = f"{basename}_div%i{name_ext}" names = tuple(name_pattern % i for i in range(1, n_parts + 1)) else: names = parts @@ -1482,7 +1480,7 @@ def label_sign_flip(label, src): vertno_sel = np.intersect1d(rh_vertno, vertices) ori.append(src[1]["nn"][vertno_sel]) if len(ori) == 0: - raise Exception('Unknown hemisphere type "%s"' % (label.hemi,)) + raise Exception(f'Unknown hemisphere type "{label.hemi}"') ori = np.concatenate(ori, axis=0) if len(ori) == 0: return np.array([], int) @@ -1707,7 +1705,7 @@ def _grow_labels(seeds, extents, hemis, names, dist, vert, subject): seed_repr = str(seed) else: seed_repr = ",".join(map(str, seed)) - comment = "Circular label: seed=%s, extent=%0.1fmm" % (seed_repr, extent) + comment = f"Circular label: seed={seed_repr}, extent={extent:0.1f}mm" label = Label( vertices=label_verts, pos=vert[hemi][label_verts], @@ -2159,8 +2157,8 @@ def _read_annot(fname): ) else: raise OSError( - "No such file %s, candidate parcellations in " - "that directory:\n%s" % (fname, "\n".join(cands)) + f"No such file {fname}, candidate parcellations in " + "that directory:\n" + "\n".join(cands) ) with open(fname, "rb") as fid: n_verts = np.fromfile(fid, ">i4", 1)[0] @@ -2238,14 +2236,14 @@ def _get_annot_fname(annot_fname, subject, hemi, parc, subjects_dir): def _load_vert_pos(subject, subjects_dir, surf_name, hemi, n_expected, extra=""): - fname_surf = op.join(subjects_dir, subject, "surf", "%s.%s" % (hemi, surf_name)) + fname_surf = op.join(subjects_dir, subject, "surf", f"{hemi}.{surf_name}") vert_pos, _ = read_surface(fname_surf) vert_pos /= 1e3 # the positions in labels are in meters if len(vert_pos) != n_expected: raise RuntimeError( - "Number of surface vertices (%s) for subject %s" + f"Number of surface vertices ({len(vert_pos)}) for subject {subject}" " does not match the expected number of vertices" - "(%s)%s" % (len(vert_pos), subject, n_expected, extra) + f"({n_expected}){extra}" ) return vert_pos @@ -2388,8 +2386,7 @@ def _check_labels_subject(labels, subject, name): if subject is not None: # label.subject can be None, depending on init if subject != label.subject: raise ValueError( - "Got multiple values of %s: %s and %s" - % (name, subject, label.subject) + f"Got multiple values of {name}: {subject} and {label.subject}" ) if subject is None: raise ValueError( @@ -2521,7 +2518,7 @@ def labels_to_stc( if values.ndim == 1: values = values[:, np.newaxis] if values.ndim != 2: - raise ValueError("values must have 1 or 2 dimensions, got %s" % (values.ndim,)) + raise ValueError(f"values must have 1 or 2 dimensions, got {values.ndim}") _validate_type(src, (SourceSpaces, None)) if src is None: data, vertices, subject = _labels_to_stc_surf( @@ -2748,11 +2745,11 @@ def write_labels_to_annot( ) if any(i > 255 for i in color): - msg = "%s: %s (%s)" % (color, ", ".join(names), hemi) + msg = f"{color}: {', '.join(names)} ({hemi})" invalid_colors.append(msg) if len(names) > 1: - msg = "%s: %s (%s)" % (color, ", ".join(names), hemi) + msg = f"{color}: {', '.join(names)} ({hemi})" duplicate_colors.append(msg) # replace None values (labels with unspecified color) @@ -2801,7 +2798,7 @@ def write_labels_to_annot( other_indices = (annot_ids.index(i) for i in other_ids) other_names = (hemi_labels[i].name for i in other_indices) other_repr = ", ".join(other_names) - msg = "%s: %s overlaps %s" % (hemi, label.name, other_repr) + msg = f"{hemi}: {label.name} overlaps {other_repr}" overlap.append(msg) annot[label.vertices] = annot_id diff --git a/mne/minimum_norm/_eloreta.py b/mne/minimum_norm/_eloreta.py index 8f15365e5b4..b49b0a4a338 100644 --- a/mne/minimum_norm/_eloreta.py +++ b/mne/minimum_norm/_eloreta.py @@ -60,8 +60,8 @@ def _compute_eloreta(inv, lambda2, options): logger.info(" Computing optimized source covariance (eLORETA)...") if n_orient == 3: logger.info( - " Using %s orientation weights" - % ("uniform" if force_equal else "independent",) + f" Using {'uniform' if force_equal else 'independent'} " + "orientation weights" ) # src, sens, 3 G_3 = _get_G_3(G, n_orient) @@ -120,8 +120,7 @@ def _compute_eloreta(inv, lambda2, options): R_last.ravel() ) logger.debug( - " Iteration %s / %s ...%s (%0.1e)" - % (kk + 1, max_iter, extra, delta) + f" Iteration {kk + 1} / {max_iter} ...{extra} ({delta:0.1e})" ) if delta < eps: logger.info( diff --git a/mne/minimum_norm/inverse.py b/mne/minimum_norm/inverse.py index 87122fdb6e1..440ed3735f2 100644 --- a/mne/minimum_norm/inverse.py +++ b/mne/minimum_norm/inverse.py @@ -1079,7 +1079,7 @@ def _apply_inverse( # Pick the correct channels from the data # sel = _pick_channels_inverse_operator(evoked.ch_names, inv) - logger.info('Applying inverse operator to "%s"...' % (evoked.comment,)) + logger.info(f'Applying inverse operator to "{evoked.comment}"...') logger.info(" Picked %d channels from the data" % len(sel)) logger.info(" Computing inverse...") K, noise_norm, vertno, source_nn = _assemble_kernel( @@ -1108,7 +1108,7 @@ def _apply_inverse( sol = combine_xyz(sol) if noise_norm is not None: - logger.info(" %s..." % (method,)) + logger.info(f" {method}...") if is_free_ori and pick_ori == "vector": noise_norm = noise_norm.repeat(3, axis=0) sol *= noise_norm diff --git a/mne/minimum_norm/tests/test_inverse.py b/mne/minimum_norm/tests/test_inverse.py index 58722a19fd5..a620fdbbf29 100644 --- a/mne/minimum_norm/tests/test_inverse.py +++ b/mne/minimum_norm/tests/test_inverse.py @@ -130,7 +130,7 @@ def _compare(a, b): if k not in b and k not in skip_types: raise ValueError( "First one had one second one didn't:\n" - "%s not in %s" % (k, b.keys()) + f"{k} not in {b.keys()}" ) if k not in skip_types: last_keys.pop() @@ -140,7 +140,7 @@ def _compare(a, b): if k not in a and k not in skip_types: raise ValueError( "Second one had one first one didn't:\n" - "%s not in %s" % (k, sorted(a.keys())) + f"{k} not in {sorted(a.keys())}" ) elif isinstance(a, list): assert len(a) == len(b) @@ -225,9 +225,7 @@ def _compare_inverses_approx( stc_2 /= norms corr = np.corrcoef(stc_1.ravel(), stc_2.ravel())[0, 1] assert corr > ctol - assert_allclose( - stc_1, stc_2, rtol=rtol, atol=atol, err_msg="%s: %s" % (method, corr) - ) + assert_allclose(stc_1, stc_2, rtol=rtol, atol=atol, err_msg=f"{method}: {corr}") def _compare_io(inv_op, *, out_file_ext=".fif", tmp_path): diff --git a/mne/minimum_norm/time_frequency.py b/mne/minimum_norm/time_frequency.py index 9561e3cd53a..16b76875941 100644 --- a/mne/minimum_norm/time_frequency.py +++ b/mne/minimum_norm/time_frequency.py @@ -861,9 +861,7 @@ def compute_source_psd( tmin = 0.0 if tmin is None else float(tmin) overlap = float(overlap) if not 0 <= overlap < 1: - raise ValueError( - "Overlap must be at least 0 and less than 1, got %s" % (overlap,) - ) + raise ValueError(f"Overlap must be at least 0 and less than 1, got {overlap}") n_fft = int(n_fft) duration = ((1.0 - overlap) * n_fft) / raw.info["sfreq"] events = make_fixed_length_events(raw, 1, tmin, tmax, duration) @@ -935,7 +933,7 @@ def _compute_source_psd_epochs( use_cps=True, ): """Generate compute_source_psd_epochs.""" - logger.info("Considering frequencies %g ... %g Hz" % (fmin, fmax)) + logger.info(f"Considering frequencies {fmin} ... {fmax} Hz") if label: # TODO: add multi-label support @@ -987,10 +985,10 @@ def _compute_source_psd_epochs( else: extra = "on %d epochs" % (n_epochs,) if isinstance(bandwidth, str): - bandwidth = "%s windowing" % (bandwidth,) + bandwidth = f"{bandwidth} windowing" else: - bandwidth = "%d tapers with bandwidth %0.1f Hz" % (n_tapers, bandwidth) - logger.info("Using %s %s" % (bandwidth, extra)) + bandwidth = f"{n_tapers} tapers with bandwidth {bandwidth:0.1f} Hz" + logger.info(f"Using {bandwidth} {extra}") if adaptive: parallel, my_psd_from_mt_adaptive, n_jobs = parallel_func( diff --git a/mne/morph.py b/mne/morph.py index e3ac5dd1589..812ba23e095 100644 --- a/mne/morph.py +++ b/mne/morph.py @@ -556,8 +556,8 @@ def apply( if stc.subject != self.subject_from: raise ValueError( "stc_from.subject and " - "morph.subject_from must match. (%s != %s)" - % (stc.subject, self.subject_from) + "morph.subject_from " + f"must match. ({stc.subject} != {self.subject_from})" ) out = _apply_morph_data(self, stc) if output != "stc": # convert to volume @@ -736,7 +736,7 @@ def _morph_vols(self, vols, mesg, subselect=True): def __repr__(self): # noqa: D105 s = "%s" % self.kind - s += ", %s -> %s" % (self.subject_from, self.subject_to) + s += f", {self.subject_from} -> {self.subject_to}" if self.kind == "volume": s += f", zooms : {self.zooms}" s += f", niter_affine : {self.niter_affine}" @@ -802,7 +802,7 @@ def _check_zooms(mri_from, zooms, zooms_src_to): if zooms.shape != (3,): raise ValueError( "zooms must be None, a singleton, or have shape (3,)," - " got shape %s" % (zooms.shape,) + f" got shape {zooms.shape}" ) zooms = tuple(zooms) return zooms @@ -840,13 +840,11 @@ def _check_subject_src(subject, src, name="subject_from", src_name="src"): subject = subject_check elif subject_check is not None and subject != subject_check: raise ValueError( - "%s does not match %s subject (%s != %s)" - % (name, src_name, subject, subject_check) + f"{name} does not match {src_name} subject ({subject} != {subject_check})" ) if subject is None: raise ValueError( - "%s could not be inferred from %s, it must be " - "specified" % (name, src_name) + f"{name} could not be inferred from {src_name}, it must be specified" ) return subject @@ -898,8 +896,8 @@ def _check_dep(nibabel="2.1.0", dipy="0.10.1"): if not passed: raise ImportError( - "%s %s or higher must be correctly " - "installed and accessible from Python" % (lib, ver) + f"{lib} {ver} or higher must be correctly " + "installed and accessible from Python" ) @@ -1321,11 +1319,11 @@ def grade_to_vertices(subject, grade, subjects_dir=None, n_jobs=None, verbose=No for verts in vertices: if (np.diff(verts) == 0).any(): raise ValueError( - "Cannot use icosahedral grade %s with subject %s, " - "mapping %s vertices onto the high-resolution mesh " + f"Cannot use icosahedral grade {grade} with subject " + f"{subject}, mapping {len(verts)} vertices onto the " + "high-resolution mesh " "yields repeated vertices, use a lower grade or a " "list of vertices from an existing source space" - % (grade, subject, len(verts)) ) else: # potentially fill the surface vertices = [np.arange(lhs.shape[0]), np.arange(rhs.shape[0])] @@ -1449,9 +1447,9 @@ def _check_vertices_match(v1, v2, name): if np.isin(v2, v1).all(): ext = " Vertices were likely excluded during forward computation." raise ValueError( - "vertices do not match between morph (%s) and stc (%s) for %s:\n%s" - '\n%s\nPerhaps src_to=fwd["src"] needs to be passed when calling ' - "compute_source_morph.%s" % (len(v1), len(v2), name, v1, v2, ext) + f"vertices do not match between morph ({len(v1)}) and stc ({len(v2)}) " + 'for {name}:\n{v1}\n{v2}\nPerhaps src_to=fwd["src"] needs to be passed ' + f"when calling compute_source_morph.{ext}" ) @@ -1462,8 +1460,8 @@ def _apply_morph_data(morph, stc_from): """Morph a source estimate from one subject to another.""" if stc_from.subject is not None and stc_from.subject != morph.subject_from: raise ValueError( - "stc.subject (%s) != morph.subject_from (%s)" - % (stc_from.subject, morph.subject_from) + f"stc.subject ({stc_from.subject}) != morph.subject_from " + f"({morph.subject_from})" ) _check_option("morph.kind", morph.kind, ("surface", "volume", "mixed")) if morph.kind == "surface": @@ -1540,7 +1538,7 @@ def _apply_morph_data(morph, stc_from): for hemi, v1, v2 in zip( ("left", "right"), morph.src_data["vertices_from"], stc_from.vertices[:2] ): - _check_vertices_match(v1, v2, "%s hemisphere" % (hemi,)) + _check_vertices_match(v1, v2, f"{hemi} hemisphere") from_sl = slice(0, from_surf_stop) assert not from_used[from_sl].any() from_used[from_sl] = True diff --git a/mne/morph_map.py b/mne/morph_map.py index 64eb537b181..643cacf8dea 100644 --- a/mne/morph_map.py +++ b/mne/morph_map.py @@ -155,7 +155,7 @@ def _write_morph_map(fname, subject_from, subject_to, mmap_1, mmap_2): with start_and_end_file(fname) as fid: _write_morph_map_(fid, subject_from, subject_to, mmap_1, mmap_2) except Exception as exp: - warn('Could not write morph-map file "%s" (error: %s)' % (fname, exp)) + warn(f'Could not write morph-map file "{fname}" (error: {exp})') def _write_morph_map_(fid, subject_from, subject_to, mmap_1, mmap_2): diff --git a/mne/preprocessing/_fine_cal.py b/mne/preprocessing/_fine_cal.py index 7b0492cdb2b..585b03fa10c 100644 --- a/mne/preprocessing/_fine_cal.py +++ b/mne/preprocessing/_fine_cal.py @@ -154,13 +154,13 @@ def compute_fine_calibration( cal_list = list() z_list = list() logger.info( - "Adjusting normals for %s magnetometers " - "(averaging over %s time intervals)" % (len(mag_picks), len(time_idxs) - 1) + f"Adjusting normals for {len(mag_picks)} magnetometers " + f"(averaging over {len(time_idxs) - 1} time intervals)" ) for start, stop in zip(time_idxs[:-1], time_idxs[1:]): logger.info( - " Processing interval %0.3f - %0.3f s" - % (start / info["sfreq"], stop / info["sfreq"]) + f" Processing interval {start / info['sfreq']:0.3f} - " + f"{stop / info['sfreq']:0.3f} s" ) data = raw[picks, start:stop][0] if ctc is not None: @@ -190,14 +190,12 @@ def compute_fine_calibration( # if len(grad_picks) > 0: extra = "X direction" if n_imbalance == 1 else ("XYZ directions") - logger.info( - "Computing imbalance for %s gradimeters (%s)" % (len(grad_picks), extra) - ) + logger.info(f"Computing imbalance for {len(grad_picks)} gradimeters ({extra})") imb_list = list() for start, stop in zip(time_idxs[:-1], time_idxs[1:]): logger.info( - " Processing interval %0.3f - %0.3f s" - % (start / info["sfreq"], stop / info["sfreq"]) + f" Processing interval {start / info['sfreq']:0.3f} - " + f"{stop / info['sfreq']:0.3f} s" ) data = raw[picks, start:stop][0] if ctc is not None: @@ -521,7 +519,7 @@ def read_fine_calibration(fname): raise RuntimeError( "Error parsing fine calibration file, " "should have 14 or 16 entries per line " - "but found %s on line:\n%s" % (len(vals), line) + f"but found {len(vals)} on line:\n{line}" ) # `vals` contains channel number ch_name = vals[0] diff --git a/mne/preprocessing/_peak_finder.py b/mne/preprocessing/_peak_finder.py index c1808397991..99272ae0fda 100644 --- a/mne/preprocessing/_peak_finder.py +++ b/mne/preprocessing/_peak_finder.py @@ -56,7 +56,7 @@ def peak_finder(x0, thresh=None, extrema=1, verbose=None): if thresh is None: thresh = (np.max(x0) - np.min(x0)) / 4 - logger.debug("Peak finder automatic threshold: %0.2g" % (thresh,)) + logger.debug(f"Peak finder automatic threshold: {thresh:0.2g}") assert extrema in [-1, 1] diff --git a/mne/preprocessing/artifact_detection.py b/mne/preprocessing/artifact_detection.py index 514eadb00a9..6b69bc9abca 100644 --- a/mne/preprocessing/artifact_detection.py +++ b/mne/preprocessing/artifact_detection.py @@ -250,7 +250,7 @@ def annotate_movement( if use_dev_head_trans not in ["average", "info"]: raise ValueError( "use_dev_head_trans must be either" - + " 'average' or 'info': got '%s'" % (use_dev_head_trans,) + f" 'average' or 'info': got '{use_dev_head_trans}'" ) if use_dev_head_trans == "average": diff --git a/mne/preprocessing/ecg.py b/mne/preprocessing/ecg.py index d773f72ba41..e36319316b1 100644 --- a/mne/preprocessing/ecg.py +++ b/mne/preprocessing/ecg.py @@ -322,7 +322,7 @@ def _get_ecg_channel_index(ch_name, inst): ) else: if ch_name not in inst.ch_names: - raise ValueError("%s not in channel list (%s)" % (ch_name, inst.ch_names)) + raise ValueError(f"{ch_name} not in channel list ({inst.ch_names})") ecg_idx = pick_channels(inst.ch_names, include=[ch_name]) if len(ecg_idx) == 0: diff --git a/mne/preprocessing/ica.py b/mne/preprocessing/ica.py index 86dfbbf6793..85bd312f3b2 100644 --- a/mne/preprocessing/ica.py +++ b/mne/preprocessing/ica.py @@ -189,8 +189,8 @@ def _check_for_unsupported_ica_channels(picks, info, allow_ref_meg=False): check = all([ch in types for ch in chs]) if not check: raise ValueError( - "Invalid channel type%s passed for ICA: %s." - "Only the following types are supported: %s" % (_pl(chs), chs, types) + f"Invalid channel type{_pl(chs)} passed for ICA: {chs}." + f"Only the following types are supported: {types}" ) @@ -935,7 +935,7 @@ def _fit(self, data, fit_type): f"n_pca_components ({self.n_pca_components}) results in " f"only {n_pca} components (EV={evs[1]:0.1f}%)" ) - logger.info("%s: %s components" % (msg, self.n_components_)) + logger.info(f"{msg}: {self.n_components_} components") # the things to store for PCA self.pca_mean_ = pca.mean_ @@ -2784,7 +2784,7 @@ def _get_target_ch(container, target): picks = list(set(picks) - set(ref_picks)) if len(picks) == 0: - raise ValueError("%s not in channel list (%s)" % (target, container.ch_names)) + raise ValueError(f"{target} not in channel list ({container.ch_names})") return picks @@ -3376,8 +3376,8 @@ def corrmap( threshold = np.atleast_1d(np.array(threshold, float)).ravel() threshold_err = ( "No component detected using when z-scoring " - "threshold%s %s, consider using a more lenient " - "threshold" % (threshold_extra, threshold) + f"threshold{threshold_extra} {threshold}, consider using a more lenient " + "threshold" ) if len(all_maps) == 0: raise RuntimeError(threshold_err) diff --git a/mne/preprocessing/infomax_.py b/mne/preprocessing/infomax_.py index 9b2841caa20..0f873c9d0bd 100644 --- a/mne/preprocessing/infomax_.py +++ b/mne/preprocessing/infomax_.py @@ -145,7 +145,7 @@ def infomax( if block is None: block = int(math.floor(math.sqrt(n_samples / 3.0))) - logger.info("Computing%sInfomax ICA" % " Extended " if extended else " ") + logger.info(f"Computing{' Extended ' if extended else ' '}Infomax ICA") # collect parameters nblock = n_samples // block diff --git a/mne/preprocessing/interpolate.py b/mne/preprocessing/interpolate.py index 0cbe8b73ce4..fc9b3c0fdec 100644 --- a/mne/preprocessing/interpolate.py +++ b/mne/preprocessing/interpolate.py @@ -45,9 +45,7 @@ def equalize_bads(insts, interp_thresh=1.0, copy=True): them, possibly with some formerly bad channels interpolated. """ if not 0 <= interp_thresh <= 1: - raise ValueError( - "interp_thresh must be between 0 and 1, got %s" % (interp_thresh,) - ) + raise ValueError(f"interp_thresh must be between 0 and 1, got {interp_thresh}") all_bads = list(set(chain.from_iterable([inst.info["bads"] for inst in insts]))) if isinstance(insts[0], BaseEpochs): diff --git a/mne/preprocessing/maxwell.py b/mne/preprocessing/maxwell.py index 5620f300ff1..8f4f5c64521 100644 --- a/mne/preprocessing/maxwell.py +++ b/mne/preprocessing/maxwell.py @@ -503,8 +503,8 @@ def _prep_maxwell_filter( missing = sorted(set(good_names) - set(got_names)) if missing: raise ValueError( - "%s channel names were missing some " - "good MEG channel names:\n%s" % (item, ", ".join(missing)) + f"{item} channel names were missing some " + f"good MEG channel names:\n{', '.join(missing)}" ) idx = [got_names.index(name) for name in good_names] extended_proj_.append(proj["data"]["data"][:, idx]) @@ -569,8 +569,8 @@ def _prep_maxwell_filter( dist = np.sqrt(np.sum(_sq(diff))) if dist > 25.0: warn( - "Head position change is over 25 mm (%s) = %0.1f mm" - % (", ".join("%0.1f" % x for x in diff), dist) + f'Head position change is over 25 mm ' + f'({", ".join("%0.1f" % x for x in diff)}) = {dist:0.1f} mm' ) # Reconstruct raw file object with spatiotemporal processed data @@ -704,9 +704,9 @@ def _run_maxwell_filter( max_samps = (ends - onsets).max() if not 0.0 < st_duration <= max_samps + 1.0: raise ValueError( - "st_duration (%0.1fs) must be between 0 and the " + f"st_duration ({st_duration / sfreq:0.1f}s) must be between 0 and the " "longest contiguous duration of the data " - "(%0.1fs)." % (st_duration / sfreq, max_samps / sfreq) + "({max_samps / sfreq:0.1f}s)." ) # Generate time points to break up data into equal-length windows starts, stops = list(), list() @@ -722,16 +722,16 @@ def _run_maxwell_filter( if n_last_buf >= st_duration: logger.info( " Spatiotemporal window did not fit evenly into" - "contiguous data segment. %0.2f seconds were lumped " - "into the previous window." - % ((n_last_buf - st_duration) / sfreq,) + "contiguous data segment. " + f"{(n_last_buf - st_duration) / sfreq:0.2f} seconds " + "were lumped into the previous window." ) else: logger.info( - " Contiguous data segment of duration %0.2f " + f" Contiguous data segment of duration " + f"{n_last_buf / sfreq:0.2f} " "seconds is too short to be processed with tSSS " - "using duration %0.2f" - % (n_last_buf / sfreq, st_duration / sfreq) + f"using duration {st_duration / sfreq:0.2f}" ) assert len(read_lims) >= 2 assert read_lims[0] == onset and read_lims[-1] == end @@ -742,13 +742,13 @@ def _run_maxwell_filter( # Loop through buffer windows of data n_sig = int(np.floor(np.log10(max(len(starts), 0)))) + 1 - logger.info(" Processing %s data chunk%s" % (len(starts), _pl(starts))) + logger.info(f" Processing {len(starts)} data chunk{_pl(starts)}") for ii, (start, stop) in enumerate(zip(starts, stops)): if start == stop: continue # Skip zero-length annotations tsss_valid = (stop - start) >= st_duration rel_times = raw_sss.times[start:stop] - t_str = "%8.3f - %8.3f s" % tuple(rel_times[[0, -1]]) + t_str = f"{rel_times[[0, -1]][0]:8.3f} - {rel_times[[0, -1]][1]:8.3f} s" t_str += ("(#%d/%d)" % (ii + 1, len(starts))).rjust(2 * n_sig + 5) # Get original data @@ -904,8 +904,8 @@ def _get_coil_scale(meg_picks, mag_picks, grad_picks, mag_scale, info): grad_base = list(grad_base)[0] mag_scale = 1.0 / grad_base logger.info( - " Setting mag_scale=%0.2f based on gradiometer " - "distance %0.2f mm" % (mag_scale, 1000 * grad_base) + f" Setting mag_scale={mag_scale:0.2f} based on gradiometer " + f"distance {1000 * grad_base:0.2f} mm" ) mag_scale = float(mag_scale) coil_scale = np.ones((len(meg_picks), 1)) @@ -962,7 +962,7 @@ def _check_destination(destination, info, head_frame): if recon_trans.to_str != "head" or recon_trans.from_str != "MEG device": raise RuntimeError( "Destination transform is not MEG device -> head, " - "got %s -> %s" % (recon_trans.from_str, recon_trans.to_str) + f"got {recon_trans.from_str} -> {recon_trans.to_str}" ) return recon_trans @@ -1154,14 +1154,14 @@ def _check_pos(pos, head_frame, raw, st_fixed, sfreq): if not _time_mask(t, tmin=raw._first_time - 1e-3, tmax=None, sfreq=sfreq).all(): raise ValueError( "Head position time points must be greater than " - "first sample offset, but found %0.4f < %0.4f" % (t[0], raw._first_time) + f"first sample offset, but found {t[0]:0.4f} < {raw._first_time:0.4f}" ) max_dist = np.sqrt(np.sum(pos[:, 4:7] ** 2, axis=1)).max() if max_dist > 1.0: warn( - "Found a distance greater than 1 m (%0.3g m) from the device " + f"Found a distance greater than 1 m ({max_dist:0.3g} m) from the device " "origin, positions may be invalid and Maxwell filtering could " - "fail" % (max_dist,) + "fail" ) dev_head_ts = np.zeros((len(t), 4, 4)) dev_head_ts[:, 3, 3] = 1.0 @@ -1316,17 +1316,8 @@ def _regularize( S_decomp = S_decomp.take(reg_moments, axis=1) if regularize is not None or n_use_out != n_out: logger.info( - " Using %s/%s harmonic components for %s " - "(%s/%s in, %s/%s out)" - % ( - n_use_in + n_use_out, - n_in + n_out, - t_str, - n_use_in, - n_in, - n_use_out, - n_out, - ) + f" Using {n_use_in + n_use_out}/{n_in + n_out} harmonic components " + f"for {t_str} ({n_use_in}/{n_in} in, {n_use_out}/{n_out} out)" ) return S_decomp, reg_moments, n_use_in @@ -1353,8 +1344,8 @@ 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( - "Number of requested bases (%s) exceeds number of " - "good sensors (%s)" % (str(n_bases), good_mask.sum()) + f"Number of requested bases ({str(n_bases)}) exceeds number of " + f"good sensors ({good_mask.sum()})" ) recons = [ch for ch in meg_info["bads"]] if len(recons) > 0: @@ -1382,9 +1373,9 @@ def _get_mf_picks_fix_mags(info, int_order, ext_order, ignore_ref=False, verbose FIFF.FIFFV_COIL_CTF_OFFDIAG_REF_GRAD, ] mag_or_fine[np.isin(coil_types, ctf_grads)] = False - msg = " Processing %s gradiometers and %s magnetometers" % ( - len(grad_picks), - len(mag_picks), + msg = ( + f" Processing {len(grad_picks)} gradiometers " + f"and {len(mag_picks)} magnetometers" ) n_kit = len(mag_picks) - mag_or_fine.sum() if n_kit > 0: @@ -2118,7 +2109,7 @@ def _prep_fine_cal(info, fine_cal): ) ) if len(missing): - warn("Found cal channel%s not in data: %s" % (_pl(missing), missing)) + warn(f"Found cal channel{_pl(missing)} not in data: {missing}") return info_to_cal, fine_cal, ch_names @@ -2209,8 +2200,8 @@ def _update_sensor_geometry(info, fine_cal, ignore_ref): np.rad2deg(np.arccos(ang_shift), ang_shift) # Convert to degrees logger.info( " Adjusted coil positions by (μ ± σ): " - "%0.1f° ± %0.1f° (max: %0.1f°)" - % (np.mean(ang_shift), np.std(ang_shift), np.max(np.abs(ang_shift))) + f"{np.mean(ang_shift):0.1f}° ± {np.std(ang_shift):0.1f}° " + f"(max: {np.max(np.abs(ang_shift)):0.1f}°)" ) return calibration, sss_cal @@ -2764,7 +2755,7 @@ def find_bad_channels_maxwell( break name = raw.ch_names[these_picks[idx]] - logger.debug(" Bad: %s %0.1f" % (name, max_)) + logger.debug(f" Bad: {name} {max_:0.1f}") these_picks.pop(idx) chunk_noisy.append(name) noisy_chs.update(chunk_noisy) @@ -2785,8 +2776,8 @@ def find_bad_channels_maxwell( scores_noisy = scores_noisy[params["meg_picks"]] thresh_noisy = thresh_noisy[params["meg_picks"]] - logger.info(" Static bad channels: %s" % (noisy_chs,)) - logger.info(" Static flat channels: %s" % (flat_chs,)) + logger.info(f" Static bad channels: {noisy_chs}") + logger.info(f" Static flat channels: {flat_chs}") logger.info("[done]") if return_scores: diff --git a/mne/preprocessing/otp.py b/mne/preprocessing/otp.py index 6cbd3822641..572e99ec7e2 100644 --- a/mne/preprocessing/otp.py +++ b/mne/preprocessing/otp.py @@ -88,9 +88,8 @@ def oversampled_temporal_projection(raw, duration=10.0, picks=None, verbose=None n_samples = int(round(float(duration) * raw.info["sfreq"])) if n_samples < len(picks_good) - 1: raise ValueError( - "duration (%s) yielded %s samples, which is fewer " - "than the number of channels -1 (%s)" - % (n_samples / raw.info["sfreq"], n_samples, len(picks_good) - 1) + f"duration ({n_samples / raw.info['sfreq']}) yielded {n_samples} samples, " + f"which is fewer than the number of channels -1 ({len(picks_good) - 1})" ) n_overlap = n_samples // 2 raw_otp = raw.copy().load_data(verbose=False) @@ -105,7 +104,8 @@ def oversampled_temporal_projection(raw, duration=10.0, picks=None, verbose=None read_lims = list(range(0, len(raw.times), n_samples)) + [len(raw.times)] for start, stop in zip(read_lims[:-1], read_lims[1:]): logger.info( - " Denoising % 8.2f – % 8.2f s" % tuple(raw.times[[start, stop - 1]]) + f" Denoising {raw.times[[start, stop - 1]][0]: 8.2f} – " + f"{raw.times[[start, stop - 1]][1]: 8.2f} s" ) otp.feed(raw[picks, start:stop][0]) return raw_otp diff --git a/mne/preprocessing/tests/test_eeglab_infomax.py b/mne/preprocessing/tests/test_eeglab_infomax.py index f4f4d1d68dc..584406820a7 100644 --- a/mne/preprocessing/tests/test_eeglab_infomax.py +++ b/mne/preprocessing/tests/test_eeglab_infomax.py @@ -77,9 +77,9 @@ def test_mne_python_vs_eeglab(): Y = generate_data_for_comparing_against_eeglab_infomax(ch_type, random_state) N, T = Y.shape for method in methods: - eeglab_results_file = "eeglab_%s_results_%s_data.mat" % ( - method, - dict(eeg="eeg", mag="meg")[ch_type], + eeglab_results_file = ( + f"eeglab_{method}_results_" + f"{dict(eeg='eeg', mag='meg')[ch_type]}_data.mat" ) # For comparison against eeglab, make sure the following diff --git a/mne/preprocessing/tests/test_maxwell.py b/mne/preprocessing/tests/test_maxwell.py index bb7bea8ef84..8f497178408 100644 --- a/mne/preprocessing/tests/test_maxwell.py +++ b/mne/preprocessing/tests/test_maxwell.py @@ -173,11 +173,7 @@ def _assert_n_free(raw_sss, lower, upper=None): """Check the DOF.""" upper = lower if upper is None else upper n_free = raw_sss.info["proc_history"][0]["max_info"]["sss_info"]["nfree"] - assert lower <= n_free <= upper, "nfree fail: %s <= %s <= %s" % ( - lower, - n_free, - upper, - ) + assert lower <= n_free <= upper, f"nfree fail: {lower} <= {n_free} <= {upper}" def _assert_mag_coil_type(info, coil_type): @@ -985,9 +981,9 @@ def _assert_shielding(raw_sss, erm_power, min_factor, max_factor=np.inf, meg="ma sss_power = raw_sss[picks][0].ravel() sss_power = np.sqrt(np.sum(sss_power * sss_power)) factor = erm_power / sss_power - assert min_factor <= factor < max_factor, ( - "Shielding factor not %0.3f <= %0.3f < %0.3f" % (min_factor, factor, max_factor) - ) + assert ( + min_factor <= factor < max_factor + ), f"Shielding factor not {min_factor:0.3f} <= {factor:0.3f} < {max_factor:0.3f}" @buggy_mkl_svd diff --git a/mne/preprocessing/xdawn.py b/mne/preprocessing/xdawn.py index d2e39a1f5ed..c0a0bb88cb3 100644 --- a/mne/preprocessing/xdawn.py +++ b/mne/preprocessing/xdawn.py @@ -202,7 +202,7 @@ def _fit_xdawn( except np.linalg.LinAlgError as exp: raise ValueError( "Could not compute eigenvalues, ensure " - "proper regularization (%s)" % (exp,) + f"proper regularization ({exp})" ) evecs = evecs[:, np.argsort(evals)[::-1]] # sort eigenvectors evecs /= np.apply_along_axis(np.linalg.norm, 0, evecs) @@ -530,7 +530,7 @@ def transform(self, inst): elif isinstance(inst, np.ndarray): X = inst if X.ndim not in (2, 3): - raise ValueError("X must be 2D or 3D, got %s" % (X.ndim,)) + raise ValueError(f"X must be 2D or 3D, got {X.ndim}") else: raise ValueError("Data input must be of Epoch type or numpy array") diff --git a/mne/proj.py b/mne/proj.py index 6395a187a54..71cd3de85bf 100644 --- a/mne/proj.py +++ b/mne/proj.py @@ -217,7 +217,7 @@ def compute_proj_epochs( else: event_id = "Multiple-events" if desc_prefix is None: - desc_prefix = "%s-%-.3f-%-.3f" % (event_id, epochs.tmin, epochs.tmax) + desc_prefix = f"{event_id}-{epochs.tmin:<.3f}-{epochs.tmax:<.3f}" return _compute_proj(data, epochs.info, n_grad, n_mag, n_eeg, desc_prefix, meg=meg) @@ -273,7 +273,7 @@ def compute_proj_evoked( """ data = np.dot(evoked.data, evoked.data.T) # compute data covariance if desc_prefix is None: - desc_prefix = "%-.3f-%-.3f" % (evoked.times[0], evoked.times[-1]) + desc_prefix = f"{evoked.times[0]:<.3f}-{evoked.times[-1]:<.3f}" return _compute_proj(data, evoked.info, n_grad, n_mag, n_eeg, desc_prefix, meg=meg) @@ -368,7 +368,7 @@ def compute_proj_raw( start = start / raw.info["sfreq"] stop = stop / raw.info["sfreq"] - desc_prefix = "Raw-%-.3f-%-.3f" % (start, stop) + desc_prefix = f"Raw-{start:<.3f}-{stop:<.3f}" projs = _compute_proj(data, info, n_grad, n_mag, n_eeg, desc_prefix, meg=meg) return projs @@ -456,7 +456,7 @@ def sensitivity_map( elif ncomp == 0: raise RuntimeError( "No valid projectors found for channel type " - "%s, cannot compute %s" % (ch_type, mode) + f"{ch_type}, cannot compute {mode}" ) # can only run the last couple methods if there are projectors elif mode in residual_types: diff --git a/mne/rank.py b/mne/rank.py index 539f897a253..ae5b6057e56 100644 --- a/mne/rank.py +++ b/mne/rank.py @@ -100,7 +100,7 @@ def _estimate_rank_from_s(s, tol="auto", tol_kind="absolute"): max_s = np.amax(s, axis=-1) if isinstance(tol, str): if tol not in ("auto", "float32"): - raise ValueError('tol must be "auto" or float, got %r' % (tol,)) + raise ValueError(f'tol must be "auto" or float, got {repr(tol)}') # XXX this should be float32 probably due to how we save and # load data, but it breaks test_make_inverse_operator (!) # The factor of 2 gets test_compute_covariance_auto_reg[None] @@ -377,7 +377,7 @@ def compute_rank( else: info = inst.info inst_type = "data" - logger.info("Computing rank from %s with rank=%r" % (inst_type, rank)) + logger.info(f"Computing rank from {inst_type} with rank={repr(rank)}") _validate_type(rank, (str, dict, None), "rank") if isinstance(rank, str): # string, either 'info' or 'full' diff --git a/mne/report/report.py b/mne/report/report.py index 6519d5cbb06..30fd2e691fd 100644 --- a/mne/report/report.py +++ b/mne/report/report.py @@ -846,7 +846,7 @@ def __init__( self.include = [] self.lang = "en-us" # language setting for the HTML file if not isinstance(raw_psd, bool) and not isinstance(raw_psd, dict): - raise TypeError("raw_psd must be bool or dict, got %s" % (type(raw_psd),)) + raise TypeError(f"raw_psd must be bool or dict, got {type(raw_psd)}") self.raw_psd = raw_psd self._init_render() # Initialize the renderer diff --git a/mne/simulation/raw.py b/mne/simulation/raw.py index 5e2a00c060f..b1c3428f9df 100644 --- a/mne/simulation/raw.py +++ b/mne/simulation/raw.py @@ -123,15 +123,15 @@ def _check_head_pos(head_pos, info, first_samp, times=None): bad = ts < 0 if bad.any(): raise RuntimeError( - "All position times must be >= 0, found %s/%s" "< 0" % (bad.sum(), len(bad)) + f"All position times must be >= 0, found {bad.sum()}/{len(bad)}" "< 0" ) if times is not None: bad = ts > times[-1] if bad.any(): raise RuntimeError( - "All position times must be <= t_end (%0.1f " - "s), found %s/%s bad values (is this a split " - "file?)" % (times[-1], bad.sum(), len(bad)) + f"All position times must be <= t_end ({times[-1]:0.1f} " + f"s), found {bad.sum()}/{len(bad)} bad values (is this a split " + "file?)" ) # If it starts close to zero, make it zero (else unique(offset) fails) if len(ts) > 0 and ts[0] < (0.5 / info["sfreq"]): @@ -313,8 +313,8 @@ def simulate_raw( # Extract necessary info meeg_picks = pick_types(info, meg=True, eeg=True, exclude=[]) logger.info( - 'Setting up raw simulation: %s position%s, "%s" interpolation' - % (len(dev_head_ts), _pl(dev_head_ts), interp) + f"Setting up raw simulation: {len(dev_head_ts)} " + f'position{_pl(dev_head_ts)}, "{interp}" interpolation' ) if isinstance(stc, SourceSimulator) and stc.first_samp != first_samp: @@ -356,8 +356,8 @@ def simulate_raw( this_n = stc_counted[1].data.shape[1] this_stop = this_start + this_n logger.info( - " Interval %0.3f–%0.3f s" - % (this_start / info["sfreq"], this_stop / info["sfreq"]) + f" Interval {this_start / info['sfreq']:0.3f}–" + f"{this_stop / info['sfreq']:0.3f} s" ) n_doing = this_stop - this_start assert n_doing > 0 @@ -498,7 +498,7 @@ def add_ecg( def _add_exg(raw, kind, head_pos, interp, n_jobs, random_state): assert isinstance(kind, str) and kind in ("ecg", "blink") _validate_type(raw, BaseRaw, "raw") - _check_preload(raw, "Adding %s noise " % (kind,)) + _check_preload(raw, f"Adding {kind} noise ") rng = check_random_state(random_state) info, times, first_samp = raw.info, raw.times, raw.first_samp data = raw._data @@ -686,7 +686,7 @@ def _stc_data_event(stc_counted, head_idx, sfreq, src=None, verts=None): stc_idx, stc = stc_counted if isinstance(stc, (list, tuple)): if len(stc) != 2: - raise ValueError("stc, if tuple, must be length 2, got %s" % (len(stc),)) + raise ValueError(f"stc, if tuple, must be length 2, got {len(stc)}") stc, stim_data = stc else: stim_data = None @@ -705,22 +705,22 @@ def _stc_data_event(stc_counted, head_idx, sfreq, src=None, verts=None): if stim_data.dtype.kind != "i": raise ValueError( "stim_data in a stc tuple must be an integer ndarray," - " got dtype %s" % (stim_data.dtype,) + f" got dtype {stim_data.dtype}" ) if stim_data.shape != (len(stc.times),): raise ValueError( - "event data had shape %s but needed to be (%s,) to" - "match stc" % (stim_data.shape, len(stc.times)) + f"event data had shape {stim_data.shape} but needed to " + f"be ({len(stc.times)},) tomatch stc" ) # Validate STC if not np.allclose(sfreq, 1.0 / stc.tstep): raise ValueError( - "stc and info must have same sample rate, " - "got %s and %s" % (1.0 / stc.tstep, sfreq) + f"stc and info must have same sample rate, " + f"got {1.0 / stc.tstep} and {sfreq}" ) if len(stc.times) <= 2: # to ensure event encoding works raise ValueError( - "stc must have at least three time points, got %s" % (len(stc.times),) + f"stc must have at least three time points, got {len(stc.times)}" ) verts_ = stc.vertices if verts is None: @@ -844,9 +844,7 @@ def _iter_forward_solutions( for ti, dev_head_t in enumerate(dev_head_ts): # Could be *slightly* more efficient not to do this N times, # but the cost here is tiny compared to actual fwd calculation - logger.info( - "Computing gain matrix for transform #%s/%s" % (ti + 1, len(dev_head_ts)) - ) + logger.info(f"Computing gain matrix for transform #{ti + 1}/{len(dev_head_ts)}") _transform_orig_meg_coils(megcoils, dev_head_t) # Make sure our sensors are all outside our BEM @@ -863,8 +861,8 @@ def _iter_forward_solutions( outside = np.ones(len(coil_rr), bool) if not outside.all(): raise RuntimeError( - "%s MEG sensors collided with inner skull " - "surface for transform %s" % (np.sum(~outside), ti) + f"{np.sum(~outside)} MEG sensors collided with inner skull " + f"surface for transform {ti}" ) megfwd = _compute_forwards( rr, sensors=sensors, bem=bem, n_jobs=n_jobs, verbose=False diff --git a/mne/simulation/source.py b/mne/simulation/source.py index f87c9b420de..42c88c47a46 100644 --- a/mne/simulation/source.py +++ b/mne/simulation/source.py @@ -177,8 +177,8 @@ def simulate_sparse_stc( subject = subject_src elif subject_src is not None and subject != subject_src: raise ValueError( - "subject argument (%s) did not match the source " - "space subject_his_id (%s)" % (subject, subject_src) + f"subject argument ({subject}) did not match the source " + f"space subject_his_id ({subject_src})" ) data = np.zeros((n_dipoles, len(times))) for i_dip in range(n_dipoles): @@ -328,9 +328,8 @@ def simulate_stc( d = len(v) - len(np.unique(v)) if d > 0: raise RuntimeError( - "Labels had %s overlaps in the %s " - "hemisphere, " - "they must be non-overlapping" % (d, hemi) + f"Labels had {d} overlaps in the {hemi} " + "hemisphere, they must be non-overlapping" ) # the data is in the order left, right data = list() diff --git a/mne/simulation/tests/test_raw.py b/mne/simulation/tests/test_raw.py index 97700ce9f00..2b047f758dd 100644 --- a/mne/simulation/tests/test_raw.py +++ b/mne/simulation/tests/test_raw.py @@ -398,7 +398,7 @@ def test_simulate_raw_bem(raw_data): fits = fit_dipole(evoked, cov, bem, trans, min_dist=1.0)[0].pos diffs = np.sqrt(np.sum((locs - fits) ** 2, axis=-1)) * 1000 med_diff = np.median(diffs) - assert med_diff < tol, "%s: %s" % (bem, med_diff) + assert med_diff < tol, f"{bem}: {med_diff}" # also test event timings with SourceSimulator first_samp = raw.first_samp events = find_events(raw, initial_event=True, verbose=False) diff --git a/mne/source_estimate.py b/mne/source_estimate.py index ccf4f8f7d19..481ae84efab 100644 --- a/mne/source_estimate.py +++ b/mne/source_estimate.py @@ -384,8 +384,8 @@ def read_source_estimate(fname, subject=None): kwargs["subject"] = subject if subject is not None and subject != kwargs["subject"]: raise RuntimeError( - 'provided subject name "%s" does not match ' - 'subject name from the file "%s' % (subject, kwargs["subject"]) + f'provided subject name "{subject}" does not match ' + f'subject name from the file "{kwargs["subject"]}' ) if ftype in ("volume", "discrete"): @@ -480,7 +480,7 @@ def _verify_source_estimate_compat(a, b): """Make sure two SourceEstimates are compatible for arith. operations.""" compat = False if type(a) != type(b): - raise ValueError("Cannot combine %s and %s." % (type(a), type(b))) + raise ValueError(f"Cannot combine {type(a)} and {type(b)}.") if len(a.vertices) == len(b.vertices): if all(np.array_equal(av, vv) for av, vv in zip(a.vertices, b.vertices)): compat = True @@ -492,7 +492,7 @@ def _verify_source_estimate_compat(a, b): if a.subject != b.subject: raise ValueError( "source estimates do not have the same subject " - "names, %r and %r" % (a.subject, b.subject) + f"names, {repr(a.subject)} and {repr(b.subject)}" ) @@ -512,13 +512,12 @@ def __init__(self, data, vertices, tmin, tstep, subject=None, verbose=None): data = None if kernel.shape[1] != sens_data.shape[0]: raise ValueError( - "kernel (%s) and sens_data (%s) have invalid " - "dimensions" % (kernel.shape, sens_data.shape) + f"kernel ({kernel.shape}) and sens_data ({sens_data.shape}) " + "have invalid dimensions" ) if sens_data.ndim != 2: raise ValueError( - "The sensor data must have 2 dimensions, got " - "%s" % (sens_data.ndim,) + "The sensor data must have 2 dimensions, got {sens_data.ndim}" ) _validate_type(vertices, list, "vertices") @@ -538,8 +537,8 @@ def __init__(self, data, vertices, tmin, tstep, subject=None, verbose=None): if data is not None: if data.ndim not in (self._data_ndim, self._data_ndim - 1): raise ValueError( - "Data (shape %s) must have %s dimensions for " - "%s" % (data.shape, self._data_ndim, self.__class__.__name__) + f"Data (shape {data.shape}) must have {self._data_ndim} " + f"dimensions for {self.__class__.__name__}" ) if data.shape[0] != n_src: raise ValueError( @@ -550,7 +549,7 @@ def __init__(self, data, vertices, tmin, tstep, subject=None, verbose=None): if data.shape[1] != 3: raise ValueError( "Data for VectorSourceEstimate must have " - "shape[1] == 3, got shape %s" % (data.shape,) + f"shape[1] == 3, got shape {data.shape}" ) if data.ndim == self._data_ndim - 1: # allow upbroadcasting data = data[..., np.newaxis] @@ -573,10 +572,10 @@ def __repr__(self): # noqa: D105 s += ", tmin : %s (ms)" % (1e3 * self.tmin) s += ", tmax : %s (ms)" % (1e3 * self.times[-1]) s += ", tstep : %s (ms)" % (1e3 * self.tstep) - s += ", data shape : %s" % (self.shape,) + s += f", data shape : {self.shape}" sz = sum(object_size(x) for x in (self.vertices + [self.data])) s += f", ~{sizeof_fmt(sz)}" - return "<%s | %s>" % (type(self).__name__, s) + return f"<{type(self).__name__} | {s}>" @fill_doc def get_peak( @@ -737,8 +736,7 @@ def save(self, fname, ftype="h5", *, overwrite=False, verbose=None): fname = _check_fname(fname=fname, overwrite=True) # check below if ftype != "h5": raise ValueError( - "%s objects can only be written as HDF5 files." - % (self.__class__.__name__,) + f"{self.__class__.__name__} objects can only be written as HDF5 files." ) _, write_hdf5 = _import_h5io_funcs() if fname.suffix != ".h5": @@ -1610,7 +1608,7 @@ def in_label(self, label): ): raise RuntimeError( "label and stc must have same subject names, " - 'currently "%s" and "%s"' % (label.subject, self.subject) + f'currently "{label.subject}" and "{self.subject}"' ) if label.hemi == "both": @@ -2103,7 +2101,7 @@ def center_of_mass( .. footbibliography:: """ if not isinstance(surf, str): - raise TypeError("surf must be a string, got %s" % (type(surf),)) + raise TypeError(f"surf must be a string, got {type(surf)}") subject = _check_subject(self.subject, subject) if np.any(self.data < 0): raise ValueError("Cannot compute COM with negative values") @@ -3565,13 +3563,12 @@ def _volume_labels(src, labels, mri_resolution): else: if len(labels) != 2: raise ValueError( - "labels, if list or tuple, must have length 2, " - "got %s" % (len(labels),) + "labels, if list or tuple, must have length 2, got {len(labels)}" ) mri, labels = labels infer_labels = False _validate_type(mri, "path-like", "labels[0]" + extra) - logger.info("Reading atlas %s" % (mri,)) + logger.info(f"Reading atlas {mri}") vol_info = _get_mri_info_data(str(mri), data=True) atlas_data = vol_info["data"] atlas_values = np.unique(atlas_data) @@ -3606,8 +3603,8 @@ def _volume_labels(src, labels, mri_resolution): atlas_shape = atlas_data.shape if atlas_shape != src_shape: raise RuntimeError( - "atlas shape %s does not match source space MRI " - "shape %s" % (atlas_shape, src_shape) + f"atlas shape {atlas_shape} does not match source space MRI " + f"shape {src_shape}" ) atlas_data = atlas_data.ravel(order="F") if mri_resolution: @@ -3709,10 +3706,10 @@ def _gen_extract_label_time_course( if len(vn) != len(svn): raise ValueError( "stc not compatible with source space. " - "stc has %s time series but there are %s " + f"stc has {len(svn)} time series but there are {len(vn)} " "vertices in source space. Ensure you used " "src from the forward or inverse operator, " - "as forward computation can exclude vertices." % (len(svn), len(vn)) + "as forward computation can exclude vertices." ) if not np.array_equal(svn, vn): raise ValueError("stc not compatible with source space") diff --git a/mne/source_space/_source_space.py b/mne/source_space/_source_space.py index 11834cc7631..471c4182afa 100644 --- a/mne/source_space/_source_space.py +++ b/mne/source_space/_source_space.py @@ -320,7 +320,7 @@ def kind(self): else: kind = "volume" if any(k == "surf" for k in types[surf_check:]): - raise RuntimeError("Invalid source space with kinds %s" % (types,)) + raise RuntimeError(f"Invalid source space with kinds {types}") return kind @verbose @@ -446,9 +446,9 @@ def __repr__(self): # noqa: D105 r = _src_kind_dict[ss_type] if ss_type == "vol": if "seg_name" in ss: - r += " (%s)" % (ss["seg_name"],) + r += f" ({ss['seg_name']})" else: - r += ", shape=%s" % (ss["shape"],) + r += f", shape={ss['shape']}" elif ss_type == "surf": r += " (%s), n_vertices=%i" % (_get_hemi(ss)[0], ss["np"]) r += ", n_used=%i" % (ss["nuse"],) @@ -457,11 +457,11 @@ def __repr__(self): # noqa: D105 ss_repr.append("<%s>" % r) subj = self._subject if subj is not None: - extra += ["subject %r" % (subj,)] + extra += [f"subject {repr(subj)}"] sz = object_size(self) if sz is not None: extra += [f"~{sizeof_fmt(sz)}"] - return "" % (", ".join(ss_repr), ", ".join(extra)) + return f"" @property def _subject(self): @@ -1425,11 +1425,8 @@ def _check_spacing(spacing, verbose=None): """Check spacing parameter.""" # check to make sure our parameters are good, parse 'spacing' types = 'a string with values "ico#", "oct#", "all", or an int >= 2' - space_err = '"spacing" must be %s, got type %s (%r)' % ( - types, - type(spacing), - spacing, - ) + space_err = f'"spacing" must be {types}, got type {type(spacing)} ({repr(spacing)})' + if isinstance(spacing, str): if spacing == "all": stype = "all" @@ -1441,13 +1438,11 @@ def _check_spacing(spacing, verbose=None): sval = int(sval) except Exception: raise ValueError( - "%s subdivision must be an integer, got %r" % (stype, sval) + f"{stype} subdivision must be an integer, got {repr(sval)}" ) lim = 0 if stype == "ico" else 1 if sval < lim: - raise ValueError( - "%s subdivision must be >= %s, got %s" % (stype, lim, sval) - ) + raise ValueError(f"{stype} subdivision must be >= {lim}, got {sval}") else: raise ValueError(space_err) else: @@ -1460,7 +1455,7 @@ def _check_spacing(spacing, verbose=None): ico_surf = None src_type_str = "all" else: - src_type_str = "%s = %s" % (stype, sval) + src_type_str = f"{stype} = {sval}" if stype == "ico": logger.info("Icosahedron subdivision grade %s" % sval) ico_surf = _get_ico_surface(sval) @@ -1522,9 +1517,8 @@ def setup_source_space( setup_volume_source_space """ cmd = ( - "setup_source_space(%s, spacing=%s, surface=%s, " - "subjects_dir=%s, add_dist=%s, verbose=%s)" - % (subject, spacing, surface, subjects_dir, add_dist, verbose) + f"setup_source_space({subject}, spacing={spacing}, surface={surface}, " + f"subjects_dir={subjects_dir}, add_dist={add_dist}, verbose={verbose})" ) subjects_dir = get_subjects_dir(subjects_dir, raise_error=True) @@ -1533,7 +1527,7 @@ def setup_source_space( ] for surf, hemi in zip(surfs, ["LH", "RH"]): if surf is not None and not op.isfile(surf): - raise OSError("Could not find the %s surface %s" % (hemi, surf)) + 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) @@ -1551,8 +1545,7 @@ def setup_source_space( # pre-load ico/oct surf (once) for speed, if necessary if stype not in ("spacing", "all"): logger.info( - "Doing the %shedral vertex picking..." - % (dict(ico="icosa", oct="octa")[stype],) + 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) @@ -1605,7 +1598,7 @@ def setup_source_space( def _check_volume_labels(volume_label, mri, name="volume_label"): - _validate_type(mri, "path-like", "mri when %s is not None" % (name,)) + _validate_type(mri, "path-like", f"mri when {name} is not None") mri = str(_check_fname(mri, overwrite="read", must_exist=True)) if isinstance(volume_label, str): volume_label = [volume_label] @@ -1614,22 +1607,22 @@ def _check_volume_labels(volume_label, mri, name="volume_label"): # Turn it into a dict if not mri.endswith("aseg.mgz"): raise RuntimeError( - "Must use a *aseg.mgz file unless %s is a dict, got %s" - % (name, op.basename(mri)) + f"Must use a *aseg.mgz file unless {name} is a dict, " + f"got {op.basename(mri)}" ) lut, _ = read_freesurfer_lut() use_volume_label = dict() for label in volume_label: if label not in lut: raise ValueError( - "Volume %r not found in file %s. Double check " - "FreeSurfer lookup table.%s" % (label, mri, _suggest(label, lut)) + f"Volume {repr(label)} not found in file {mri}. Double check " + f"FreeSurfer lookup table.{_suggest(label, lut)}" ) use_volume_label[label] = lut[label] volume_label = use_volume_label for label, id_ in volume_label.items(): _validate_type(label, str, "volume_label keys") - _validate_type(id_, "int-like", "volume_labels[%r]" % (label,)) + _validate_type(id_, "int-like", f"volume_labels[{repr(label)}]") volume_label = {k: _ensure_int(v) for k, v in volume_label.items()} return volume_label @@ -1825,10 +1818,10 @@ def setup_volume_source_space( logger.info("Boundary surface file : %s", surf_extra) else: logger.info( - "Sphere : origin at (%.1f %.1f %.1f) mm" - % (1000 * sphere[0], 1000 * sphere[1], 1000 * sphere[2]) + f"Sphere : origin at ({1000 * sphere[0]:.1f} " + f"{1000 * sphere[1]:.1f} {1000 * sphere[2]:.1f}) mm" ) - logger.info(" radius : %.1f mm" % (1000 * sphere[3],)) + logger.info(f" radius : {1000 * sphere[3]:.1f} mm") # triage pos argument if isinstance(pos, dict): @@ -1886,8 +1879,8 @@ def setup_volume_source_space( assert surf["id"] == FIFF.FIFFV_BEM_SURF_ID_BRAIN if surf["coord_frame"] != FIFF.FIFFV_COORD_MRI: raise ValueError( - "BEM is not in MRI coordinates, got %s" - % (_coord_frame_name(surf["coord_frame"]),) + f"BEM is not in MRI coordinates, got " + f"{_coord_frame_name(surf['coord_frame'])}" ) logger.info("Taking inner skull from %s" % bem) elif surface is not None: @@ -1996,8 +1989,8 @@ def _make_discrete_source_space(pos, coord_frame="mri"): # Check that coordinate frame is valid if coord_frame not in _str_to_frame: # will fail if coord_frame not string raise KeyError( - 'coord_frame must be one of %s, not "%s"' - % (list(_str_to_frame.keys()), coord_frame) + f"coord_frame must be one of {list(_str_to_frame.keys())}, " + f'not "{coord_frame}"' ) coord_frame = _str_to_frame[coord_frame] # now an int @@ -2066,13 +2059,12 @@ def _make_volume_source_space( # Define the sphere which fits the surface logger.info( - "Surface CM = (%6.1f %6.1f %6.1f) mm" - % (1000 * cm[0], 1000 * cm[1], 1000 * cm[2]) + f"Surface CM = ({1000 * cm[0]:6.1f} {1000 * cm[1]:6.1f} {1000 * cm[2]:6.1f}) mm" ) logger.info("Surface fits inside a sphere with radius %6.1f mm" % (1000 * maxdist)) logger.info("Surface extent:") for c, mi, ma in zip("xyz", mins, maxs): - logger.info(" %s = %6.1f ... %6.1f mm" % (c, 1000 * mi, 1000 * ma)) + logger.info(f" {c} = {1000 * mi:6.1f} ... {1000 * ma:6.1f} mm") maxn = np.array( [ np.floor(np.abs(m) / grid) + 1 if m > 0 else -np.floor(np.abs(m) / grid) - 1 @@ -2089,9 +2081,7 @@ def _make_volume_source_space( ) logger.info("Grid extent:") for c, mi, ma in zip("xyz", minn, maxn): - logger.info( - " %s = %6.1f ... %6.1f mm" % (c, 1000 * mi * grid, 1000 * ma * grid) - ) + logger.info(f" {c} = {1000 * mi * grid:6.1f} ... {1000 * ma * grid:6.1f} mm") # Now make the initial grid ns = tuple(maxn - minn + 1) @@ -2630,7 +2620,7 @@ def _adjust_patch_info(s, verbose=None): def _ensure_src(src, kind=None, extra="", verbose=None): """Ensure we have a source space.""" _check_option("kind", kind, (None, "surface", "volume", "mixed", "discrete")) - msg = "src must be a string or instance of SourceSpaces%s" % (extra,) + msg = f"src must be a string or instance of SourceSpaces{extra}" if _path_like(src): src = str(src) if not op.isfile(src): @@ -2638,7 +2628,7 @@ def _ensure_src(src, kind=None, extra="", verbose=None): logger.info("Reading %s..." % src) src = read_source_spaces(src, verbose=False) if not isinstance(src, SourceSpaces): - raise ValueError("%s, got %s (type %s)" % (msg, src, type(src))) + raise ValueError(f"{msg}, got {src} (type {type(src)})") if kind is not None: if src.kind != kind and src.kind == "mixed": if kind == "surface": @@ -2646,9 +2636,7 @@ def _ensure_src(src, kind=None, extra="", verbose=None): elif kind == "volume": src = src[2:] if src.kind != kind: - raise ValueError( - "Source space must contain %s type, got " "%s" % (kind, src.kind) - ) + raise ValueError(f"Source space must contain {kind} type, got {src.kind}") return src @@ -2660,8 +2648,8 @@ def _ensure_src_subject(src, subject): raise ValueError("source space is too old, subject must be " "provided") elif src_subject is not None and subject != src_subject: raise ValueError( - 'Mismatch between provided subject "%s" and subject ' - 'name "%s" in the source space' % (subject, src_subject) + f'Mismatch between provided subject "{subject}" and subject ' + f'name "{src_subject}" in the source space' ) return subject @@ -2712,7 +2700,7 @@ def add_source_space_distances(src, dist_limit=np.inf, n_jobs=None, *, verbose=N src = _ensure_src(src) dist_limit = float(dist_limit) if dist_limit < 0: - raise ValueError("dist_limit must be non-negative, got %s" % (dist_limit,)) + 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") @@ -2721,7 +2709,7 @@ def add_source_space_distances(src, dist_limit=np.inf, n_jobs=None, *, verbose=N min_dists = list() min_idxs = list() msg = "patch information" if patch_only else "source space distances" - logger.info("Calculating %s (limit=%s mm)..." % (msg, 1000 * dist_limit)) + logger.info(f"Calculating {msg} (limit={1000 * dist_limit} mm)...") max_n = max(s["nuse"] for s in src) if not patch_only and max_n > _DIST_WARN_LIMIT: warn( @@ -2891,9 +2879,7 @@ def _get_vertex_map_nn( """ # adapted from mne_make_source_space.c, knowing accurate=False (i.e. # nearest-neighbor mode should be used) - logger.info( - "Mapping %s %s -> %s (nearest neighbor)..." % (hemi, subject_from, subject_to) - ) + logger.info(f"Mapping {hemi} {subject_from} -> {subject_to} (nearest neighbor)...") regs = [ subjects_dir / s / "surf" / f"{hemi}.sphere.reg" for s in (subject_from, subject_to) @@ -2976,7 +2962,7 @@ def morph_source_spaces( for fro in src_from: hemi, idx, id_ = _get_hemi(fro) to = subjects_dir / subject_to / "surf" / f"{hemi}.{surf}" - logger.info("Reading destination surface %s" % (to,)) + logger.info(f"Reading destination surface {to}") to = read_surface(to, return_dict=True, verbose=False)[-1] complete_surface_info(to, copy=False) # Now we morph the vertices to the destination @@ -3170,8 +3156,8 @@ def _compare_source_spaces(src0, src1, mode="exact", nearest=True, dist_tol=1.5e assert_array_equal( s["vertno"], np.where(s["inuse"])[0], - 'src%s[%s]["vertno"] != ' - 'np.where(src%s[%s]["inuse"])[0]' % (ii, si, ii, si), + f'src{ii}[{si}]["vertno"] != ' + f'np.where(src{ii}[{si}]["inuse"])[0]', ) assert_equal(len(s0["vertno"]), len(s1["vertno"])) agreement = np.mean(s0["inuse"] == s1["inuse"]) diff --git a/mne/stats/cluster_level.py b/mne/stats/cluster_level.py index cca48ebdfee..32243eeeff0 100644 --- a/mne/stats/cluster_level.py +++ b/mne/stats/cluster_level.py @@ -419,8 +419,8 @@ def _find_clusters( if show_info is True: if len(thresholds) == 0: warn( - 'threshold["start"] (%s) is more extreme than data ' - "statistics with most extreme value %s" % (threshold["start"], stop) + f'threshold["start"] ({threshold["start"]}) is more extreme ' + f"than data statistics with most extreme value {stop}" ) else: logger.info( @@ -928,8 +928,7 @@ def _permutation_cluster_test( and threshold < 0 ): raise ValueError( - "incompatible tail and threshold signs, got " - "%s and %s" % (tail, threshold) + f"incompatible tail and threshold signs, got {tail} and {threshold}" ) # check dimensions for each group in X (a list at this stage). @@ -956,7 +955,7 @@ def _permutation_cluster_test( # ------------------------------------------------------------- t_obs = stat_fun(*X) _validate_type(t_obs, np.ndarray, "return value of stat_fun") - logger.info("stat_fun(H1): min=%f max=%f" % (np.min(t_obs), np.max(t_obs))) + logger.info(f"stat_fun(H1): min={np.min(t_obs)} max={np.max(t_obs)}") # test if stat_fun treats variables independently if buffer_size is not None: @@ -976,9 +975,8 @@ def _permutation_cluster_test( # The stat should have the same shape as the samples for no adj. if t_obs.size != np.prod(sample_shape): raise ValueError( - "t_obs.shape %s provided by stat_fun %s is not " - "compatible with the sample shape %s" - % (t_obs.shape, stat_fun, sample_shape) + f"t_obs.shape {t_obs.shape} provided by stat_fun {stat_fun} is not " + f"compatible with the sample shape {sample_shape}" ) if adjacency is None or adjacency is False: t_obs.shape = sample_shape @@ -1138,7 +1136,7 @@ def _check_fun(X, stat_fun, threshold, tail=0, kind="within"): if stat_fun is not None and stat_fun is not ttest_1samp_no_p: warn( "Automatic threshold is only valid for stat_fun=None " - "(or ttest_1samp_no_p), got %s" % (stat_fun,) + f"(or ttest_1samp_no_p), got {stat_fun}" ) p_thresh = 0.05 / (1 + (tail == 0)) n_samples = len(X) @@ -1153,7 +1151,7 @@ def _check_fun(X, stat_fun, threshold, tail=0, kind="within"): if stat_fun is not None and stat_fun is not f_oneway: warn( "Automatic threshold is only valid for stat_fun=None " - "(or f_oneway), got %s" % (stat_fun,) + f"(or f_oneway), got {stat_fun}" ) elif tail != 1: warn('Ignoring argument "tail", performing 1-tailed F-test') diff --git a/mne/stats/regression.py b/mne/stats/regression.py index 762a250bc3b..c9c6c63a5dc 100644 --- a/mne/stats/regression.py +++ b/mne/stats/regression.py @@ -89,9 +89,7 @@ def linear_regression(inst, design_matrix, names=None): data = np.array([i.data for i in inst]) else: raise ValueError("Input must be epochs or iterable of source " "estimates") - logger.info( - msg + ", (%s targets, %s regressors)" % (np.prod(data.shape[1:]), len(names)) - ) + 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") lm_fits = {} diff --git a/mne/stats/tests/test_parametric.py b/mne/stats/tests/test_parametric.py index e1d64583777..de7aa237c40 100644 --- a/mne/stats/tests/test_parametric.py +++ b/mne/stats/tests/test_parametric.py @@ -148,14 +148,14 @@ def test_ttest_equiv(kind, kwargs, sigma, seed): rng = np.random.RandomState(seed) def theirs(*a, **kw): - f = getattr(scipy.stats, "ttest_%s" % (kind,)) + f = getattr(scipy.stats, f"ttest_{kind}") if kind == "1samp": func = partial(f, popmean=0, **kwargs) else: func = partial(f, **kwargs) return func(*a, **kw)[0] - ours = partial(getattr(mne.stats, "ttest_%s_no_p" % (kind,)), sigma=sigma, **kwargs) + ours = partial(getattr(mne.stats, f"ttest_{kind}_no_p"), sigma=sigma, **kwargs) X = rng.randn(3, 4, 5) if kind == "ind": diff --git a/mne/surface.py b/mne/surface.py index d203a9ce00b..0334ee12ab0 100644 --- a/mne/surface.py +++ b/mne/surface.py @@ -122,7 +122,7 @@ def _get_head_surface(subject, source, subjects_dir, on_defects, raise_error=Tru surf = None for this_source in source: this_head = op.realpath( - op.join(subjects_dir, subject, "bem", "%s-%s.fif" % (subject, this_source)) + op.join(subjects_dir, subject, "bem", f"{subject}-{this_source}.fif") ) if op.exists(this_head): surf = read_bem_surfaces( @@ -137,7 +137,7 @@ def _get_head_surface(subject, source, subjects_dir, on_defects, raise_error=Tru path = op.join(subjects_dir, subject, "bem") if not op.isdir(path): raise OSError('Subject bem directory "%s" does not exist.' % path) - files = sorted(glob(op.join(path, "%s*%s.fif" % (subject, this_source)))) + files = sorted(glob(op.join(path, f"{subject}*{this_source}.fif"))) for this_head in files: try: surf = read_bem_surfaces( @@ -157,8 +157,8 @@ def _get_head_surface(subject, source, subjects_dir, on_defects, raise_error=Tru if surf is None: if raise_error: raise OSError( - 'No file matching "%s*%s" and containing a head ' - "surface found." % (subject, this_source) + f'No file matching "{subject}*{this_source}" and containing a head ' + "surface found." ) else: return surf @@ -1454,9 +1454,7 @@ def _decimate_surface_sphere(rr, tris, n_triangles): ) func_map = dict(ico=_get_ico_surface, oct=_tessellate_sphere_surf) kind, level = map_[n_triangles] - logger.info( - "Decimating using Freesurfer spherical %s%s downsampling" % (kind, level) - ) + logger.info(f"Decimating using Freesurfer spherical {kind}{level} downsampling") ico_surf = func_map[kind](level) assert len(ico_surf["tris"]) == n_triangles tempdir = _TempDir() @@ -1539,8 +1537,8 @@ def decimate_surface(points, triangles, n_triangles, method="quadric", *, verbos _check_option("method", method, sorted(method_map)) if n_triangles > len(triangles): raise ValueError( - "Requested n_triangles (%s) exceeds number of " - "original triangles (%s)" % (n_triangles, len(triangles)) + f"Requested n_triangles ({n_triangles}) exceeds number of " + f"original triangles ({len(triangles)})" ) return method_map[method](points, triangles, n_triangles) @@ -1829,8 +1827,7 @@ def read_tri(fname_in, swap=False, verbose=None): tris[:, [2, 1]] = tris[:, [1, 2]] tris -= 1 logger.info( - "Loaded surface from %s with %s nodes and %s triangles." - % (fname_in, n_nodes, n_tris) + f"Loaded surface from {fname_in} with {n_nodes} nodes and {n_tris} triangles." ) if n_items in [3, 4]: logger.info("Node normals were not included in the source file.") diff --git a/mne/tests/test_annotations.py b/mne/tests/test_annotations.py index 4868f5dc5df..c968f639e22 100644 --- a/mne/tests/test_annotations.py +++ b/mne/tests/test_annotations.py @@ -236,7 +236,7 @@ def test_crop(tmp_path): assert_allclose( getattr(raw_concat.annotations, attr), getattr(raw.annotations, attr), - err_msg="Failed for %s:" % (attr,), + err_msg=f"Failed for {attr}:", ) raw.set_annotations(None) # undo diff --git a/mne/tests/test_chpi.py b/mne/tests/test_chpi.py index 5801e374b3b..cb9ccc60c26 100644 --- a/mne/tests/test_chpi.py +++ b/mne/tests/test_chpi.py @@ -210,7 +210,7 @@ def _assert_quats( # maxfilter produces some times that are implausibly large (weird) if not np.isclose(t[0], t_est[0], atol=1e-1): # within 100 ms raise AssertionError( - "Start times not within 100 ms: %0.3f != %0.3f" % (t[0], t_est[0]) + f"Start times not within 100 ms: {t[0]:0.3f} != {t_est[0]:0.3f}" ) use_mask = (t >= t_est[0]) & (t <= t_est[-1]) t = t[use_mask] @@ -229,10 +229,9 @@ def _assert_quats( distances = np.sqrt(np.sum((trans - trans_est_interp) ** 2, axis=1)) assert np.isfinite(distances).all() arg_worst = np.argmax(distances) - assert distances[arg_worst] <= dist_tol, "@ %0.3f seconds: %0.3f > %0.3f mm" % ( - t[arg_worst], - 1000 * distances[arg_worst], - 1000 * dist_tol, + assert distances[arg_worst] <= dist_tol, ( + f"@ {t[arg_worst]:0.3f} seconds: " + f"{1000 * distances[arg_worst]:0.3f} > {1000 * dist_tol:0.3f} mm" ) # limit rotation difference between MF and our estimation @@ -240,10 +239,9 @@ def _assert_quats( quats_est_interp = interp1d(t_est, quats_est, axis=0)(t) angles = 180 * _angle_between_quats(quats_est_interp, quats) / np.pi arg_worst = np.argmax(angles) - assert angles[arg_worst] <= angle_tol, "@ %0.3f seconds: %0.3f > %0.3f deg" % ( - t[arg_worst], - angles[arg_worst], - angle_tol, + assert angles[arg_worst] <= angle_tol, ( + f"@ {t[arg_worst]:0.3f} seconds: " + f"{angles[arg_worst]:0.3f} > {angle_tol:0.3f} deg" ) # error calculation difference diff --git a/mne/tests/test_coreg.py b/mne/tests/test_coreg.py index af5801114a9..5f4c58fa8a5 100644 --- a/mne/tests/test_coreg.py +++ b/mne/tests/test_coreg.py @@ -218,7 +218,7 @@ def test_scale_mri_xfm(tmp_path, few_surfaces, subjects_dir_tmp_few): subjects_dir_tmp_few / subject_from / "bem" - / ("%s-%s-src.fif" % (subject_from, spacing)) + / (f"{subject_from}-{spacing}-src.fif") ) src_from = mne.setup_source_space( subject_from, @@ -273,7 +273,7 @@ def test_scale_mri_xfm(tmp_path, few_surfaces, subjects_dir_tmp_few): subjects_dir_tmp_few / subject_to / "bem" - / ("%s-%s-src.fif" % (subject_to, spacing)) + / (f"{subject_to}-{spacing}-src.fif") ) assert src_to_fname.exists(), "Source space was not scaled" # Check MRI scaling diff --git a/mne/tests/test_cov.py b/mne/tests/test_cov.py index cd817dcfceb..d23452a6a0b 100644 --- a/mne/tests/test_cov.py +++ b/mne/tests/test_cov.py @@ -294,7 +294,7 @@ def test_cov_estimation_on_raw(method, tmp_path): try: import sklearn # noqa: F401 except Exception as exp: - pytest.skip("sklearn is required, got %s" % (exp,)) + pytest.skip(f"sklearn is required, got {exp}") raw = read_raw_fif(raw_fname, preload=True) cov_mne = read_cov(erm_cov_fname) method_params = dict(shrunk=dict(shrinkage=[0])) @@ -393,7 +393,7 @@ def test_cov_estimation_on_raw_reg(): def _assert_cov(cov, cov_desired, tol=0.005, nfree=True): assert_equal(cov.ch_names, cov_desired.ch_names) err = np.linalg.norm(cov.data - cov_desired.data) / np.linalg.norm(cov.data) - assert err < tol, "%s >= %s" % (err, tol) + assert err < tol, f"{err} >= {tol}" if nfree: assert_equal(cov.nfree, cov_desired.nfree) diff --git a/mne/tests/test_dipole.py b/mne/tests/test_dipole.py index 73aaeb7ad68..8f7c9508024 100644 --- a/mne/tests/test_dipole.py +++ b/mne/tests/test_dipole.py @@ -215,9 +215,8 @@ 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(), "%s (factor: %s)" % ( - (data_rms / resi_rms).min(), - 0.95, + assert (data_rms > resi_rms * 0.95).all(), ( + f"{(data_rms / resi_rms).min()} " f"(factor: {0.95})" ) # Compare to original points @@ -560,7 +559,7 @@ def test_bdip(fname_dip_, fname_bdip_, tmp_path): b = getattr(this_bdip, key) if key == "khi2" and dip_has_conf: if d is not None: - assert_allclose(d, b, atol=atol, err_msg="%s: %s" % (kind, key)) + assert_allclose(d, b, atol=atol, err_msg=f"{kind}: {key}") else: assert b is None if dip_has_conf: @@ -574,7 +573,7 @@ def test_bdip(fname_dip_, fname_bdip_, tmp_path): d, b, rtol=0.12, # no so great, text I/O - err_msg="%s: %s" % (kind, key), + err_msg=f"{kind}: {key}", ) # Not stored assert this_bdip.name is None diff --git a/mne/tests/test_filter.py b/mne/tests/test_filter.py index 36f2da736c3..23ff37b8591 100644 --- a/mne/tests/test_filter.py +++ b/mne/tests/test_filter.py @@ -88,12 +88,8 @@ 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], "%s %s: %s <= %s <= %s" % ( - kind, - thresh, - lims[0], - n_ring, - lims[1], + assert lims[0] <= n_ring <= lims[1], ( + f"{kind} {thresh}: {lims[0]} " f"<= {n_ring} <= {lims[1]}" ) with pytest.warns(RuntimeWarning, match="properly estimate"): assert estimate_ringing_samples(butter(4, 0.00001)) == 100000 @@ -407,7 +403,7 @@ def test_resample_scipy(): for window in ("boxcar", "hann"): for N in (100, 101, 102, 103): x = np.arange(N).astype(float) - err_msg = "%s: %s" % (N, window) + err_msg = f"{N}: {window}" x_2_sp = sp_resample(x, 2 * N, window=window) for n_jobs in n_jobs_test: x_2 = resample(x, 2, 1, npad=0, window=window, n_jobs=n_jobs) @@ -911,7 +907,7 @@ def test_reporting_iir(phase, ftype, btype, order, output): dB_cutoff = -7.58 dB_cutoff *= order_mult if btype == "lowpass": - keys += ["%0.2f dB" % (dB_cutoff,)] + keys += [f"{dB_cutoff:0.2f} dB"] for key in keys: assert key.lower() in log.lower() # Verify some of the filter properties diff --git a/mne/tests/test_label.py b/mne/tests/test_label.py index ff28eaa1423..01d934417e2 100644 --- a/mne/tests/test_label.py +++ b/mne/tests/test_label.py @@ -182,7 +182,7 @@ def assert_labels_equal(l0, l1, decimal=5, comment=True, color=True): for attr in ["hemi", "subject"]: attr0 = getattr(l0, attr) attr1 = getattr(l1, attr) - msg = "label.%s: %r != %r" % (attr, attr0, attr1) + msg = f"label.{attr}: {repr(attr0)} != {repr(attr1)}" assert_equal(attr0, attr1, msg) for attr in ["vertices", "pos", "values"]: a0 = getattr(l0, attr) diff --git a/mne/tests/test_line_endings.py b/mne/tests/test_line_endings.py index 5c91c29fd9a..8ee4f604c9f 100644 --- a/mne/tests/test_line_endings.py +++ b/mne/tests/test_line_endings.py @@ -74,8 +74,7 @@ def _assert_line_endings(dir_): ) if len(report) > 0: raise AssertionError( - "Found %s files with incorrect endings:\n%s" - % (len(report), "\n".join(report)) + f"Found {len(report)} files with incorrect endings:\n" + "\n".join(report) ) diff --git a/mne/time_frequency/_stockwell.py b/mne/time_frequency/_stockwell.py index 26b25444abb..d1108f8057b 100644 --- a/mne/time_frequency/_stockwell.py +++ b/mne/time_frequency/_stockwell.py @@ -29,8 +29,7 @@ def _is_power_of_two(n): n_fft = 2 ** int(np.ceil(np.log2(n_times))) elif n_fft < n_times: raise ValueError( - "n_fft cannot be smaller than signal size. " - "Got %s < %s." % (n_fft, n_times) + f"n_fft cannot be smaller than signal size. Got {n_fft} < {n_times}." ) if n_times < n_fft: logger.info( diff --git a/mne/time_frequency/multitaper.py b/mne/time_frequency/multitaper.py index 1709d6c16d1..00e3c1c1e17 100644 --- a/mne/time_frequency/multitaper.py +++ b/mne/time_frequency/multitaper.py @@ -285,9 +285,7 @@ def _compute_mt_params(n_times, sfreq, bandwidth, low_bias, adaptive, verbose=No """Triage windowing and multitaper parameters.""" # Compute standardized half-bandwidth if isinstance(bandwidth, str): - logger.info( - ' Using standard spectrum estimation with "%s" window' % (bandwidth,) - ) + logger.info(f' Using standard spectrum estimation with "{bandwidth}" window') window_fun = get_window(bandwidth, n_times)[np.newaxis] return window_fun, np.ones(1), False @@ -297,9 +295,8 @@ def _compute_mt_params(n_times, sfreq, bandwidth, low_bias, adaptive, verbose=No half_nbw = 4.0 if half_nbw < 0.5: raise ValueError( - "bandwidth value %s yields a normalized half-bandwidth of " - "%s < 0.5, use a value of at least %s" - % (bandwidth, half_nbw, sfreq / n_times) + f"bandwidth value {bandwidth} yields a normalized half-bandwidth of " + f"{half_nbw} < 0.5, use a value of at least {sfreq / n_times}" ) # Compute DPSS windows @@ -315,7 +312,7 @@ def _compute_mt_params(n_times, sfreq, bandwidth, low_bias, adaptive, verbose=No if adaptive and len(eigvals) < 3: warn( "Not adaptively combining the spectral estimators due to a " - "low number of tapers (%s < 3)." % (len(eigvals),) + f"low number of tapers ({len(eigvals)} < 3)." ) adaptive = False diff --git a/mne/time_frequency/tfr.py b/mne/time_frequency/tfr.py index 0233f82edac..d7df408b564 100644 --- a/mne/time_frequency/tfr.py +++ b/mne/time_frequency/tfr.py @@ -498,7 +498,7 @@ def _compute_tfr( if epoch_data.ndim != 3: raise ValueError( "epoch_data must be of shape (n_epochs, n_chans, " - "n_times), got %s" % (epoch_data.shape,) + f"n_times), got {epoch_data.shape}" ) # Check params @@ -518,7 +518,7 @@ def _compute_tfr( if (freqs > sfreq / 2.0).any(): raise ValueError( "Cannot compute freq above Nyquist freq of the data " - "(%0.1f Hz), got %0.1f Hz" % (sfreq / 2.0, freqs.max()) + f"({sfreq / 2.0:0.1f} Hz), got {freqs.max():0.1f} Hz" ) # We decimate *after* decomposition, so we need to create our kernels @@ -2097,13 +2097,11 @@ def plot_joint( freq = tfr.freqs[np.argmin(np.abs(tfr.freqs - freq))] if (time_half_range == 0) and (freq_half_range == 0): - sub_map_title = "(%.2f s,\n%.1f Hz)" % (time, freq) + sub_map_title = f"({time:.2f} s,\n{freq:.1f} Hz)" else: - sub_map_title = "(%.1f \u00b1 %.1f s,\n%.1f \u00b1 %.1f Hz)" % ( - time, - time_half_range, - freq, - freq_half_range, + sub_map_title = ( + f"({time:.1f} \u00b1 {time_half_range:.1f} " + f"s,\n{freq:.1f} \u00b1 {freq_half_range:.1f} Hz)" ) tmin = time - time_half_range @@ -2600,11 +2598,11 @@ def __imul__(self, a): # noqa: D105 return self def __repr__(self): # noqa: D105 - s = "time : [%f, %f]" % (self.times[0], self.times[-1]) - s += ", freq : [%f, %f]" % (self.freqs[0], self.freqs[-1]) + s = f"time : [{self.times[0]}, {self.times[-1]}]" + s += f", freq : [{self.freqs[0]}, {self.freqs[-1]}]" s += ", nave : %d" % self.nave s += ", channels : %d" % self.data.shape[0] - s += ", ~%s" % (sizeof_fmt(self._size),) + s += f", ~{sizeof_fmt(self._size)}" return "" % s @@ -2772,11 +2770,11 @@ def _detrend_picks(self): return list() def __repr__(self): # noqa: D105 - s = "time : [%f, %f]" % (self.times[0], self.times[-1]) - s += ", freq : [%f, %f]" % (self.freqs[0], self.freqs[-1]) + s = f"time : [{self.times[0]}, {self.times[-1]}]" + s += f", freq : [{self.freqs[0]}, {self.freqs[-1]}]" s += ", epochs : %d" % self.data.shape[0] s += ", channels : %d" % self.data.shape[1] - s += ", ~%s" % (sizeof_fmt(self._size),) + s += f", ~{sizeof_fmt(self._size)}" return "" % s def __abs__(self): @@ -2955,10 +2953,10 @@ def combine_tfr(all_tfr, weights="nave"): ch_names = tfr.ch_names for t_ in all_tfr[1:]: assert t_.ch_names == ch_names, ValueError( - "%s and %s do not contain " "the same channels" % (tfr, t_) + f"{tfr} and {t_} do not contain the same channels" ) assert np.max(np.abs(t_.times - tfr.times)) < 1e-7, ValueError( - "%s and %s do not contain the same time instants" % (tfr, t_) + f"{tfr} and {t_} do not contain the same time instants" ) # use union of bad channels diff --git a/mne/transforms.py b/mne/transforms.py index 975a4818910..7a3875ef56c 100644 --- a/mne/transforms.py +++ b/mne/transforms.py @@ -223,8 +223,7 @@ def _print_coord_trans( scale = 1000.0 if (ti != 3 and units != "mm") else 1.0 text = " mm" if ti != 3 else "" log_func( - " % 8.6f % 8.6f % 8.6f %7.2f%s" - % (tt[0], tt[1], tt[2], scale * tt[3], text) + f" {tt[0]:8.6f} {tt[1]:8.6f} {tt[2]:8.6f} {scale * tt[3]:7.2f}{text}" ) @@ -662,7 +661,7 @@ def transform_surface_to(surf, dest, trans, copy=False): if isinstance(dest, str): if dest not in _str_to_frame: raise KeyError( - 'dest must be one of %s, not "%s"' % (list(_str_to_frame.keys()), dest) + f'dest must be one of {list(_str_to_frame.keys())}, not "{dest}"' ) dest = _str_to_frame[dest] # convert to integer if surf["coord_frame"] == dest: @@ -1018,7 +1017,7 @@ def transform(self, pts, verbose=None): dest : shape (n_transform, 3) The transformed points. """ - logger.info("Transforming %s points" % (len(pts),)) + logger.info(f"Transforming {len(pts)} points") assert pts.shape[1] == 3 # for memory reasons, we should do this in ~100 MB chunks out = np.zeros_like(pts) @@ -1139,11 +1138,8 @@ def fit( dest_center = _fit_sphere(hsp, disp=False)[1] destination = destination - dest_center logger.info( - " Using centers %s -> %s" - % ( - np.array_str(src_center, None, 3), - np.array_str(dest_center, None, 3), - ) + " Using centers {np.array_str(src_center, None, 3)} -> " + "{np.array_str(dest_center, None, 3)}" ) self._fit_params = dict( n_src=len(source), @@ -1581,7 +1577,7 @@ def _read_fs_xfm(fname): for li, line in enumerate(fid): if li == 0: kind = line.strip() - logger.debug("Found: %r" % (kind,)) + logger.debug(f"Found: {repr(kind)}") if line[: len(comp)] == comp: # we have the right line, so don't read any more break diff --git a/mne/utils/_bunch.py b/mne/utils/_bunch.py index ff04fcec91a..26cc4e6b17a 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 "%s (%s)" % (str(self.__class__.mro()[-2](self)), self._name) + return f"{str(self.__class__.mro()[-2](self))} ({self._name})" __repr__ = __str__ diff --git a/mne/utils/_testing.py b/mne/utils/_testing.py index d767e25711c..b2829917f59 100644 --- a/mne/utils/_testing.py +++ b/mne/utils/_testing.py @@ -243,16 +243,13 @@ def _check_snr(actual, desired, picks, min_tol, med_tol, msg, kind="MEG"): snr = snrs.min() bad_count = (snrs < min_tol).sum() msg = " (%s)" % msg if msg != "" else msg - assert bad_count == 0, "SNR (worst %0.2f) < %0.2f for %s/%s " "channels%s" % ( - snr, - min_tol, - bad_count, - len(picks), - msg, + assert bad_count == 0, ( + f"SNR (worst {snr:0.2f}) < {min_tol:0.2f} " + f"for {bad_count}/{len(picks)} channels{msg}" ) # median tol snr = np.median(snrs) - assert snr >= med_tol, "%s SNR median %0.2f < %0.2f%s" % (kind, snr, med_tol, msg) + assert snr >= med_tol, f"{kind} SNR median {snr:0.2f} < {med_tol:0.2f}{msg}" def assert_meg_snr( @@ -296,7 +293,7 @@ def assert_snr(actual, desired, tol): """Assert actual and desired arrays are within some SNR tolerance.""" with np.errstate(divide="ignore"): # allow infinite snr = linalg.norm(desired, ord="fro") / linalg.norm(desired - actual, ord="fro") - assert snr >= tol, "%f < %f" % (snr, tol) + assert snr >= tol, f"{snr} < {tol}" def assert_stcs_equal(stc1, stc2): @@ -344,7 +341,7 @@ def assert_dig_allclose(info_py, info_bin, limit=None): d_bin["r"], rtol=1e-5, atol=1e-5, - err_msg="Failure on %s:\n%s\n%s" % (ii, d_py["r"], d_bin["r"]), + err_msg=f"Failure on {ii}:\n{d_py['r']}\n{d_bin['r']}", ) if any(d["kind"] == FIFF.FIFFV_POINT_EXTRA for d in dig_py) and info_py is not None: r_bin, o_head_bin, o_dev_bin = fit_sphere_to_headshape( diff --git a/mne/utils/check.py b/mne/utils/check.py index e73faf0a2e3..b703317f9d0 100644 --- a/mne/utils/check.py +++ b/mne/utils/check.py @@ -65,14 +65,14 @@ def check_fname(fname, filetype, endings, endings_err=()): if len(endings_err) > 0 and not fname.endswith(endings_err): print_endings = " or ".join([", ".join(endings_err[:-1]), endings_err[-1]]) raise OSError( - "The filename (%s) for file type %s must end with %s" - % (fname, filetype, print_endings) + f"The filename ({fname}) for file type {filetype} must end " + f"with {print_endings}" ) print_endings = " or ".join([", ".join(endings[:-1]), endings[-1]]) if not fname.endswith(endings): warn( - "This filename (%s) does not conform to MNE naming conventions. " - "All %s files should end with %s" % (fname, filetype, print_endings) + f"This filename ({fname}) does not conform to MNE naming conventions. " + f"All {filetype} files should end with {print_endings}" ) @@ -323,9 +323,9 @@ def _check_preload(inst, msg): if not inst.preload: raise RuntimeError( "By default, MNE does not load data into main memory to " - "conserve resources. " + msg + " requires %s data to be " + "conserve resources. " + msg + f" requires {name} data to be " "loaded. Use preload=True (or string) in the constructor or " - "%s.load_data()." % (name, name) + f"{name}.load_data()." ) if name == "epochs": inst._handle_empty("raise", msg) @@ -359,8 +359,8 @@ def _check_compensation_grade(info1, info2, name1, name2="data", ch_names=None): # perform check if grade1 != grade2: raise RuntimeError( - "Compensation grade of %s (%s) and %s (%s) do not match" - % (name1, grade1, name2, grade2) + f"Compensation grade of {name1} ({grade1}) and {name2} ({grade2}) " + "do not match" ) @@ -764,9 +764,7 @@ def _check_rank(rank): _validate_type(rank, (None, dict, str), "rank") if isinstance(rank, str): if rank not in ["full", "info"]: - raise ValueError( - 'rank, if str, must be "full" or "info", ' "got %s" % (rank,) - ) + raise ValueError(f'rank, if str, must be "full" or "info", got {rank}') return rank @@ -937,7 +935,7 @@ def fun(data): raise ValueError( "Combine option must be " + ", ".join(valid) - + " or callable, got %s (type %s)." % (mode, type(mode)) + + f" or callable, got {mode} (type {type(mode)})." ) return fun @@ -950,7 +948,7 @@ def _check_src_normal(pick_ori, src): raise RuntimeError( "Normal source orientation is supported only for " "surface or discrete SourceSpaces, got type " - "%s" % (src.kind,) + f"{src.kind}" ) @@ -1092,7 +1090,7 @@ def _check_sphere(sphere, info=None, sphere_units="m"): raise ValueError( "sphere, if a ConductorModel, must be spherical " "with multiple layers, not a BEM or single-layer " - "sphere (got %s)" % (sphere,) + f"sphere (got {sphere})" ) sphere = tuple(sphere["r0"]) + (sphere["layers"][0]["rad"],) sphere_units = "m" @@ -1102,7 +1100,7 @@ def _check_sphere(sphere, info=None, sphere_units="m"): if sphere.shape != (4,): raise ValueError( "sphere must be float or 1D array of shape (4,), got " - "array-like of shape %s" % (sphere.shape,) + f"array-like of shape {sphere.shape}" ) _check_option("sphere_units", sphere_units, ("m", "mm")) if sphere_units == "mm": @@ -1171,9 +1169,9 @@ def _suggest(val, options, cutoff=0.66): if len(options) == 0: return "" elif len(options) == 1: - return " Did you mean %r?" % (options[0],) + return f" Did you mean {repr(options[0])}?" else: - return " Did you mean one of %r?" % (options,) + return f" Did you mean one of {repr(options)}?" def _check_on_missing(on_missing, name="on_missing", *, extras=()): @@ -1244,7 +1242,5 @@ def _import_nibabel(why="use MRI files"): try: import nibabel as nib except ImportError as exp: - raise exp.__class__( - "nibabel is required to %s, got:\n%s" % (why, exp) - ) from None + raise exp.__class__(f"nibabel is required to {why}, got:\n{exp}") from None return nib diff --git a/mne/utils/config.py b/mne/utils/config.py index 37a9f1aafd6..549f2d9547a 100644 --- a/mne/utils/config.py +++ b/mne/utils/config.py @@ -328,9 +328,9 @@ def get_config(key=None, default=None, raise_error=False, home_dir=None, use_env "for a permanent one" % key ) raise KeyError( - 'Key "%s" not found in %s' - "the mne-python config file (%s). " - "Try %s%s.%s" % (key, loc_env, config_path, meth_env, meth_file, extra_env) + f'Key "{key}" not found in {loc_env}' + f"the mne-python config file ({config_path}). " + f"Try {meth_env}{meth_file}.{extra_env}" ) else: return config.get(key, default) diff --git a/mne/utils/dataframe.py b/mne/utils/dataframe.py index 2f70c57c6e7..e4012c4d45d 100644 --- a/mne/utils/dataframe.py +++ b/mne/utils/dataframe.py @@ -17,7 +17,7 @@ def _set_pandas_dtype(df, columns, dtype, verbose=None): """Try to set the right columns to dtype.""" for column in columns: df[column] = df[column].astype(dtype) - logger.info('Converting "%s" to "%s"...' % (column, dtype)) + logger.info(f'Converting "{column}" to "{dtype}"...') def _scale_dataframe_data(inst, data, picks, scalings): diff --git a/mne/utils/docs.py b/mne/utils/docs.py index 73fead5ad1d..e8c30716d48 100644 --- a/mne/utils/docs.py +++ b/mne/utils/docs.py @@ -4211,10 +4211,10 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): If trans is None, an identity matrix is assumed. """ -docdict["trans_not_none"] = """ +docdict["trans_not_none"] = f""" trans : str | dict | instance of Transform - %s -""" % (_trans_base,) + {_trans_base} +""" docdict["transparent"] = """ transparent : bool | None @@ -4519,7 +4519,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("Error documenting %s:\n%s" % (funcname, str(exp))) + raise RuntimeError(f"Error documenting {funcname}:\n{str(exp)}") return f @@ -4821,11 +4821,7 @@ def linkcode_resolve(domain, info): kind = "main" else: kind = "maint/%s" % (".".join(mne.__version__.split(".")[:2])) - return "http://github.com/mne-tools/mne-python/blob/%s/mne/%s%s" % ( - kind, - fn, - linespec, - ) + return f"http://github.com/mne-tools/mne-python/blob/{kind}/mne/{fn}{linespec}" def open_docs(kind=None, version=None): @@ -4857,7 +4853,7 @@ def open_docs(kind=None, version=None): if version is None: version = get_config("MNE_DOCS_VERSION", "stable") _check_option("version", version, ["stable", "dev"]) - webbrowser.open_new_tab("https://mne.tools/%s/%s" % (version, kind)) + webbrowser.open_new_tab(f"https://mne.tools/{version}/{kind}") class _decorator: @@ -5051,7 +5047,7 @@ def _docformat(docstring, docdict=None, funcname=None): try: return docstring % indented except (TypeError, ValueError, KeyError) as exp: - raise RuntimeError("Error documenting %s:\n%s" % (funcname, str(exp))) + raise RuntimeError(f"Error documenting {funcname}:\n{str(exp)}") def _indentcount_lines(lines): diff --git a/mne/utils/mixin.py b/mne/utils/mixin.py index 87e86aaa315..f0fcf94de14 100644 --- a/mne/utils/mixin.py +++ b/mne/utils/mixin.py @@ -288,7 +288,7 @@ def _keys_to_idx(self, keys): except Exception as exp: msg += ( " The epochs.metadata Pandas query did not " - "yield any results: %s" % (exp.args[0],) + f"yield any results: {exp.args[0]}" ) else: return vals @@ -453,7 +453,7 @@ def metadata(self, metadata, verbose=None): action += " existing" else: action = "Not setting" if metadata is None else "Adding" - logger.info("%s metadata%s" % (action, n_col)) + logger.info(f"{action} metadata{n_col}") self._metadata = metadata diff --git a/mne/utils/numerics.py b/mne/utils/numerics.py index 33e313f362f..9a7524505e7 100644 --- a/mne/utils/numerics.py +++ b/mne/utils/numerics.py @@ -321,7 +321,7 @@ def _apply_scaling_array(data, picks_list, scalings, verbose=None): """Scale data type-dependently for estimation.""" scalings = _check_scaling_inputs(data, picks_list, scalings) if isinstance(scalings, dict): - logger.debug(" Scaling using mapping %s." % (scalings,)) + logger.debug(f" Scaling using mapping {scalings}.") picks_dict = dict(picks_list) scalings = [(picks_dict[k], v) for k, v in scalings.items() if k in picks_dict] for idx, scaling in scalings: @@ -493,16 +493,15 @@ def _time_mask( assert include_tmax # can only be used when sfreq is known if raise_error and tmin > tmax: raise ValueError( - "tmin (%s) must be less than or equal to tmax (%s)" % (orig_tmin, orig_tmax) + f"tmin ({orig_tmin}) must be less than or equal to tmax ({orig_tmax})" ) mask = times >= tmin mask &= times <= tmax if raise_error and not mask.any(): extra = "" if include_tmax else "when include_tmax=False " raise ValueError( - "No samples remain when using tmin=%s and tmax=%s %s" - "(original time bounds are [%s, %s])" - % (orig_tmin, orig_tmax, extra, times[0], times[-1]) + f"No samples remain when using tmin={orig_tmin} and tmax={orig_tmax} " + f"{extra}(original time bounds are [{times[0]}, {times[-1]}])" ) return mask @@ -525,15 +524,14 @@ def _freq_mask(freqs, sfreq, fmin=None, fmax=None, raise_error=True): fmax = int(round(fmax * sfreq)) / sfreq + 0.5 / sfreq if raise_error and fmin > fmax: raise ValueError( - "fmin (%s) must be less than or equal to fmax (%s)" % (orig_fmin, orig_fmax) + f"fmin ({orig_fmin}) must be less than or equal to fmax ({orig_fmax})" ) mask = freqs >= fmin mask &= freqs <= fmax if raise_error and not mask.any(): raise ValueError( - "No frequencies remain when using fmin=%s and " - "fmax=%s (original frequency bounds are [%s, %s])" - % (orig_fmin, orig_fmax, freqs[0], freqs[-1]) + f"No frequencies remain when using fmin={orig_fmin} and fmax={orig_fmax} " + f"(original frequency bounds are [{freqs[0]}, {freqs[-1]}])" ) return mask @@ -683,7 +681,7 @@ def object_hash(x, h=None): for xx in x: object_hash(xx, h) else: - raise RuntimeError("unsupported type: %s (%s)" % (type(x), x)) + raise RuntimeError(f"unsupported type: {type(x)} ({x})") return int(h.hexdigest(), 16) @@ -733,7 +731,7 @@ def object_size(x, memo=None): elif sparse.isspmatrix_csc(x) or sparse.isspmatrix_csr(x): size = sum(sys.getsizeof(xx) for xx in [x, x.data, x.indices, x.indptr]) else: - raise RuntimeError("unsupported type: %s (%s)" % (type(x), x)) + raise RuntimeError(f"unsupported type: {type(x)} ({x})") memo[id_] = size return size @@ -804,16 +802,16 @@ def object_diff(a, b, pre="", *, allclose=False): ) elif isinstance(a, (list, tuple)): if len(a) != len(b): - out += pre + " length mismatch (%s, %s)\n" % (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) elif isinstance(a, float): if not _array_equal_nan(a, b, allclose): - out += pre + " value mismatch (%s, %s)\n" % (a, b) + out += pre + f" value mismatch ({a}, {b})\n" elif isinstance(a, (str, int, bytes, np.generic)): if a != b: - out += pre + " value mismatch (%s, %s)\n" % (a, b) + 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) @@ -830,8 +828,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" - "(%s vs %s)" % (a.shape, b.shape) + " sparse matrix a and b shape mismatch" f"({a.shape} vs {b.shape})" ) else: c = a - b @@ -841,7 +838,7 @@ def object_diff(a, b, pre="", *, allclose=False): elif hasattr(a, "__getstate__") and a.__getstate__() is not None: out += object_diff(a.__getstate__(), b.__getstate__(), pre, allclose=allclose) else: - raise RuntimeError(pre + ": unsupported type %s (%s)" % (type(a), a)) + raise RuntimeError(pre + f": unsupported type {type(a)} ({a})") return out @@ -883,16 +880,16 @@ def _fit(self, X): ) elif not 0 <= n_components <= min(n_samples, n_features): raise ValueError( - "n_components=%r must be between 0 and " - "min(n_samples, n_features)=%r with " - "svd_solver='full'" % (n_components, min(n_samples, n_features)) + f"n_components={repr(n_components)} must be between 0 and " + f"min(n_samples, n_features)={repr(min(n_samples, n_features))} with " + "svd_solver='full'" ) elif n_components >= 1: if not isinstance(n_components, (numbers.Integral, np.integer)): raise ValueError( - "n_components=%r must be of type int " - "when greater than or equal to 1, " - "was of type=%r" % (n_components, type(n_components)) + f"n_components={repr(n_components)} must be of type int " + f"when greater than or equal to 1, " + f"was of type={repr(type(n_components))}" ) self.mean_ = np.mean(X, axis=0) @@ -1051,7 +1048,7 @@ def _check_dt(dt): or dt.tzinfo is None or dt.tzinfo is not timezone.utc ): - raise ValueError("Date must be datetime object in UTC: %r" % (dt,)) + raise ValueError(f"Date must be datetime object in UTC: {repr(dt)}") def _dt_to_stamp(inp_date): @@ -1102,7 +1099,7 @@ def restore(self, val): try: idx = self.popped.pop(val) except KeyError: - warn("Could not find value: %s" % (val,)) + warn(f"Could not find value: {val}") else: loc = np.searchsorted(self.indices, idx) self.indices.insert(loc, idx) diff --git a/mne/utils/tests/test_numerics.py b/mne/utils/tests/test_numerics.py index 645623fa6a0..e66082581a4 100644 --- a/mne/utils/tests/test_numerics.py +++ b/mne/utils/tests/test_numerics.py @@ -318,7 +318,7 @@ def test_object_size(): (200, 900, sparse.eye(20, format="csr")), ): size = object_size(obj) - assert lower < size < upper, "%s < %s < %s:\n%s" % (lower, size, upper, obj) + assert lower < size < upper, f"{lower} < {size} < {upper}:\n{obj}" # views work properly x = dict(a=1) assert object_size(x) < 1000 diff --git a/mne/viz/_3d.py b/mne/viz/_3d.py index a9d44ce9ad5..3bee0c4fd29 100644 --- a/mne/viz/_3d.py +++ b/mne/viz/_3d.py @@ -198,7 +198,7 @@ def plot_head_positions( if p.ndim != 2 or p.shape[1] != 10: raise ValueError( "pos (or each entry in pos if a list) must be " - "dimension (N, 10), got %s" % (p.shape,) + f"dimension (N, 10), got {p.shape}" ) if ii > 0: # concatenation p[:, 0] += pos[ii - 1][-1, 0] - p[0, 0] @@ -233,7 +233,7 @@ def plot_head_positions( else: axes = np.array(axes) if axes.shape != (3, 2): - raise ValueError("axes must have shape (3, 2), got %s" % (axes.shape,)) + raise ValueError(f"axes must have shape (3, 2), got {axes.shape}") fig = axes[0, 0].figure labels = ["xyz", ("$q_1$", "$q_2$", "$q_3$")] @@ -1793,12 +1793,11 @@ def _process_clim(clim, colormap, transparent, data=0.0, allow_pos_lims=True): key = "lims" clim = {"kind": "percent", key: [96, 97.5, 99.95]} if not isinstance(clim, dict): - raise ValueError('"clim" must be "auto" or dict, got %s' % (clim,)) + raise ValueError(f'"clim" must be "auto" or dict, got {clim}') if ("lims" in clim) + ("pos_lims" in clim) != 1: raise ValueError( - "Exactly one of lims and pos_lims must be specified " - "in clim, got %s" % (clim,) + "Exactly one of lims and pos_lims must be specified " f"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") @@ -1806,17 +1805,17 @@ def _process_clim(clim, colormap, transparent, data=0.0, allow_pos_lims=True): ctrl_pts = np.array(clim["pos_lims" if diverging else "lims"], float) ctrl_pts = np.array(ctrl_pts, float) if ctrl_pts.shape != (3,): - raise ValueError("clim has shape %s, it must be (3,)" % (ctrl_pts.shape,)) + raise ValueError(f"clim has shape {ctrl_pts.shape}, it must be (3,)") if (np.diff(ctrl_pts) < 0).any(): raise ValueError( - "colormap limits must be monotonically " "increasing, got %s" % (ctrl_pts,) + f"colormap limits must be monotonically increasing, got {ctrl_pts}" ) clim_kind = clim.get("kind", "percent") _check_option("clim['kind']", clim_kind, ["value", "values", "percent"]) if clim_kind == "percent": perc_data = np.abs(data) if diverging else data ctrl_pts = np.percentile(perc_data, ctrl_pts) - logger.info("Using control points %s" % (ctrl_pts,)) + logger.info(f"Using control points {ctrl_pts}") assert len(ctrl_pts) == 3 clim = dict(kind="value") clim["pos_lims" if diverging else "lims"] = ctrl_pts @@ -2517,7 +2516,7 @@ def _plot_stc( if overlay_alpha == 0: smoothing_steps = 1 # Disable smoothing to save time. - title = subject if len(hemis) > 1 else "%s - %s" % (subject, hemis[0]) + title = subject if len(hemis) > 1 else f"{subject} - {hemis[0]}" kwargs = { "subject": subject, "hemi": hemi, @@ -2933,7 +2932,7 @@ def _onclick(event, params, verbose=None): time_sl = slice(0, None) else: initial_time = float(initial_time) - logger.info("Fixing initial time: %s s" % (initial_time,)) + logger.info(f"Fixing initial time: {initial_time} s") initial_time = np.argmin(np.abs(stc.times - initial_time)) time_sl = slice(initial_time, initial_time + 1) if initial_pos is None: # find max pos and (maybe) time @@ -2946,10 +2945,10 @@ def _onclick(event, params, verbose=None): if initial_pos.shape != (3,): raise ValueError( "initial_pos must be float ndarray with shape " - "(3,), got shape %s" % (initial_pos.shape,) + f"(3,), got shape {initial_pos.shape}" ) initial_pos *= 1000 - logger.info("Fixing initial position: %s mm" % (initial_pos.tolist(),)) + logger.info(f"Fixing initial position: {initial_pos.tolist()} mm") loc_idx = _cut_coords_to_idx(initial_pos, img) if initial_time is not None: # time also specified time_idx = time_sl.start @@ -3997,14 +3996,10 @@ def _plot_dipole( coord_frame_name = "Head" if coord_frame == "head" else "MRI" if title is None: - title = "Dipole #%s / %s @ %.3fs, GOF: %.1f%%, %.1fnAm\n%s: " % ( - idx + 1, - len(dipole.times), - dipole.times[idx], - dipole.gof[idx], - dipole.amplitude[idx] * 1e9, - coord_frame_name, - ) + "(%0.1f, %0.1f, %0.1f) mm" % tuple(xyz[idx]) + 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" ax.get_figure().suptitle(title) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 432621eadc4..da5ca5c3cd1 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -1880,8 +1880,8 @@ def add_data( time = np.asarray(time) if time.shape != (array.shape[-1],): raise ValueError( - "time has shape %s, but need shape %s " - "(array.shape[-1])" % (time.shape, (array.shape[-1],)) + f"time has shape {time.shape}, but need shape " + f"{(array.shape[-1],)} (array.shape[-1])" ) self._data["time"] = time @@ -3462,9 +3462,9 @@ def set_data_smoothing(self, n_steps): vertices = hemi_data["vertices"] if vertices is None: raise ValueError( - "len(data) < nvtx (%s < %s): the vertices " + f"len(data) < nvtx ({len(hemi_data)} < " + f"{self.geo[hemi].x.shape[0]}): the vertices " "parameter must not be None" - % (len(hemi_data), self.geo[hemi].x.shape[0]) ) morph_n_steps = "nearest" if n_steps == -1 else n_steps with use_log_level(False): @@ -3934,8 +3934,8 @@ def _make_movie_frames( tmin = self._times[0] elif tmin < self._times[0]: raise ValueError( - "tmin=%r is smaller than the first time point " - "(%r)" % (tmin, self._times[0]) + f"tmin={repr(tmin)} is smaller than the first time point " + f"({repr(self._times[0])})" ) # find indexes at which to create frames @@ -3943,8 +3943,8 @@ def _make_movie_frames( tmax = self._times[-1] elif tmax > self._times[-1]: raise ValueError( - "tmax=%r is greater than the latest time point " - "(%r)" % (tmax, self._times[-1]) + f"tmax={repr(tmax)} is greater than the latest time point " + f"({repr(self._times[-1])})" ) n_frames = floor((tmax - tmin) * time_dilation * framerate) times = np.arange(n_frames, dtype=float) @@ -3956,7 +3956,7 @@ def _make_movie_frames( if n_times == 0: raise ValueError("No time points selected") - logger.debug("Save movie for time points/samples\n%s\n%s" % (times, time_idx)) + logger.debug(f"Save movie for time points/samples\n{times}\n{time_idx}") # Sometimes the first screenshot is rendered with a different # resolution on OS X self.screenshot(time_viewer=time_viewer) @@ -4127,9 +4127,9 @@ def _update_limits(fmin, fmid, fmax, center, array): fmid = (fmin + fmax) / 2.0 if fmin >= fmid: - raise RuntimeError("min must be < mid, got %0.4g >= %0.4g" % (fmin, fmid)) + raise RuntimeError(f"min must be < mid, got {fmin:0.4g} >= {fmid:0.4g}") if fmid >= fmax: - raise RuntimeError("mid must be < max, got %0.4g >= %0.4g" % (fmid, fmax)) + raise RuntimeError(f"mid must be < max, got {fmid:0.4g} >= {fmax:0.4g}") return fmin, fmid, fmax diff --git a/mne/viz/_brain/colormap.py b/mne/viz/_brain/colormap.py index 0567e352252..31c42456995 100644 --- a/mne/viz/_brain/colormap.py +++ b/mne/viz/_brain/colormap.py @@ -117,9 +117,7 @@ def calculate_lut(lut_table, alpha, fmin, fmid, fmax, center=None, transparent=T Color map with transparency channel. """ if not fmin <= fmid <= fmax: - raise ValueError( - "Must have fmin (%s) <= fmid (%s) <= fmax (%s)" % (fmin, fmid, fmax) - ) + raise ValueError(f"Must have fmin ({fmin}) <= fmid ({fmid}) <= fmax ({fmax})") lut_table = create_lut(lut_table) assert lut_table.dtype.kind == "i" divergent = center is not None diff --git a/mne/viz/_brain/surface.py b/mne/viz/_brain/surface.py index b2b79f2bf7c..7f17cebf718 100644 --- a/mne/viz/_brain/surface.py +++ b/mne/viz/_brain/surface.py @@ -120,9 +120,7 @@ def load_geometry(self): None """ if self.surf == "flat": # special case - fname = path.join( - self.data_path, "surf", "%s.%s" % (self.hemi, "cortex.patch.flat") - ) + fname = path.join(self.data_path, "surf", f"{self.hemi}.cortex.patch.flat") _check_fname( fname, overwrite="read", must_exist=True, name="flatmap surface file" ) diff --git a/mne/viz/backends/tests/test_renderer.py b/mne/viz/backends/tests/test_renderer.py index ec62a1b6748..b20bb6e4865 100644 --- a/mne/viz/backends/tests/test_renderer.py +++ b/mne/viz/backends/tests/test_renderer.py @@ -194,7 +194,7 @@ def test_renderer(renderer, monkeypatch): "-uc", "import mne; mne.viz.create_3d_figure((800, 600), show=True); " "backend = mne.viz.get_3d_backend(); " - "assert backend == %r, backend" % (backend,), + f"assert backend == {repr(backend)}, backend", ] monkeypatch.setenv("MNE_3D_BACKEND", backend) run_subprocess(cmd) diff --git a/mne/viz/evoked.py b/mne/viz/evoked.py index bbbede964a8..5883dfaf5f5 100644 --- a/mne/viz/evoked.py +++ b/mne/viz/evoked.py @@ -193,7 +193,7 @@ def _line_plot_onselect( method = "mean" if psd else "rms" this_data, _ = _merge_ch_data(this_data, ch_type, [], method=method) - title = "%s %s" % (ch_type, method.upper()) + title = f"{ch_type} {method.upper()}" else: title = ch_type this_data = np.average(this_data, axis=1) @@ -213,7 +213,7 @@ def _line_plot_onselect( ) unit = "Hz" if psd else time_unit - fig.suptitle("Average over %.2f%s - %.2f%s" % (xmin, unit, xmax, unit), y=0.1) + fig.suptitle(f"Average over {xmin:.2f}{unit} - {xmax:.2f}{unit}", y=0.1) plt_show() if text is not None: text.set_visible(False) @@ -628,7 +628,7 @@ def _plot_lines( if this_type in _DATA_CH_TYPES_SPLIT: logger.info( "Need more than one channel to make " - "topography for %s. Disabling interactivity." % (this_type,) + f"topography for {this_type}. Disabling interactivity." ) selectables[type_idx] = False @@ -1868,7 +1868,7 @@ def plot_evoked_joint( from matplotlib.patches import ConnectionPatch if ts_args is not None and not isinstance(ts_args, dict): - raise TypeError("ts_args must be dict or None, got type %s" % (type(ts_args),)) + raise TypeError(f"ts_args must be dict or None, got type {type(ts_args)}") ts_args = dict() if ts_args is None else ts_args.copy() ts_args["time_unit"], _ = _check_time_unit( ts_args.get("time_unit", "s"), evoked.times diff --git a/mne/viz/misc.py b/mne/viz/misc.py index c9be85f8f9e..49b01ed6b16 100644 --- a/mne/viz/misc.py +++ b/mne/viz/misc.py @@ -869,7 +869,7 @@ def plot_events( continue y = np.full(count, idx + 1 if equal_spacing else events[ev_mask, 2][0]) if event_id is not None: - event_label = "%s (%s)" % (event_id_rev[ev], count) + event_label = f"{event_id_rev[ev]} ({count})" else: event_label = "N=%d" % (count,) labels.append(event_label) @@ -1025,7 +1025,7 @@ def _get_flim(flim, fscale, freq, sfreq=None): def _check_fscale(fscale): """Check for valid fscale.""" if not isinstance(fscale, str) or fscale not in ("log", "linear"): - raise ValueError('fscale must be "log" or "linear", got %s' % (fscale,)) + raise ValueError(f'fscale must be "log" or "linear", got {fscale}') _DEFAULT_ALIM = (-80, 10) @@ -1340,7 +1340,7 @@ def plot_ideal_filter( if freq[0] != 0: raise ValueError( "freq should start with DC (zero) and end with " - "Nyquist, but got %s for DC" % (freq[0],) + f"Nyquist, but got {freq[0]} for DC" ) freq = np.array(freq) # deal with semilogx problems @ x=0 @@ -1411,8 +1411,8 @@ def _handle_event_colors(color_dict, unique_events, event_id): if len(unassigned): unassigned_str = ", ".join(str(e) for e in unassigned) warn( - "Color was not assigned for event%s %s. Default colors will " - "be used." % (_pl(unassigned), unassigned_str) + f"Color was not assigned for event{_pl(unassigned)} {unassigned_str}. " + "Default colors will be used." ) default_colors.update(custom_colors) return default_colors @@ -1535,7 +1535,7 @@ def plot_csd( ax.set_xticks([]) ax.set_yticks([]) if csd._is_sum: - ax.set_title("%.1f-%.1f Hz." % (np.min(freq), np.max(freq))) + ax.set_title(f"{np.min(freq):.1f}-{np.max(freq):.1f} Hz.") else: ax.set_title("%.1f Hz." % freq) diff --git a/mne/viz/tests/test_3d_mpl.py b/mne/viz/tests/test_3d_mpl.py index 2b46a688a13..b006a421494 100644 --- a/mne/viz/tests/test_3d_mpl.py +++ b/mne/viz/tests/test_3d_mpl.py @@ -89,7 +89,7 @@ def test_plot_volume_source_estimates( log = log.getvalue() want_str = "t = %0.3f s" % want_t assert want_str in log, (want_str, init_t) - want_str = "(%0.1f, %0.1f, %0.1f) mm" % want_p + 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) for ax_idx in [0, 2, 3, 4]: _fake_click(fig, fig.axes[ax_idx], (0.3, 0.5)) diff --git a/mne/viz/topo.py b/mne/viz/topo.py index 1751c7efa57..e23e60b9bca 100644 --- a/mne/viz/topo.py +++ b/mne/viz/topo.py @@ -555,7 +555,7 @@ def _format_coord(x, y, labels, ax): if "(" in xlabel and ")" in xlabel else "s" ) - timestr = "%6.3f %s: " % (x, xunit) + timestr = f"{x:6.3f} {xunit}: " if not nearby: return "%s Nothing here" % timestr labels = [""] * len(nearby) if labels is None else labels @@ -574,11 +574,9 @@ def _format_coord(x, y, labels, ax): s = timestr for data_, label, tvec in nearby_data: idx = np.abs(tvec - x).argmin() - s += "%7.2f %s" % (data_[ch_idx, idx], yunit) + s += f"{data_[ch_idx, idx]:7.2f} {yunit}" if trunc_labels: - label = ( - label if len(label) <= 10 else "%s..%s" % (label[:6], label[-2:]) - ) + label = label if len(label) <= 10 else f"{label[:6]}..{label[-2:]}" s += " [%s] " % label if label else " " return s diff --git a/mne/viz/topomap.py b/mne/viz/topomap.py index a531bb7e866..cca239f844d 100644 --- a/mne/viz/topomap.py +++ b/mne/viz/topomap.py @@ -1271,7 +1271,7 @@ def _plot_topomap( if len(data) != len(pos): raise ValueError( "Data and pos need to be of same length. Got data of " - "length %s, pos of length %s" % (len(data), len(pos)) + f"length {len(data)}, pos of length { len(pos)}" ) norm = min(data) >= 0 @@ -3156,9 +3156,9 @@ def _animate(frame, ax, ax_line, params): time_idx = params["frames"][frame] if params["time_unit"] == "ms": - title = "%6.0f ms" % (params["times"][frame] * 1e3,) + title = f"{params['times'][frame] * 1e3:6.0f} ms" else: - title = "%6.3f s" % (params["times"][frame],) + title = f"{params['times'][frame]:6.3f} s" if params["blit"]: text = params["text"] else: diff --git a/mne/viz/utils.py b/mne/viz/utils.py index d325c474a16..9f622a2dd87 100644 --- a/mne/viz/utils.py +++ b/mne/viz/utils.py @@ -1980,9 +1980,7 @@ def _handle_decim(info, decim, lowpass): decim = max(int(info["sfreq"] / (lp * 3) + 1e-6), 1) decim = _ensure_int(decim, "decim", must_be='an int or "auto"') if decim <= 0: - raise ValueError( - 'decim must be "auto" or a positive integer, got %s' % (decim,) - ) + raise ValueError(f'decim must be "auto" or a positive integer, got {decim}') decim = _check_decim(info, decim, 0)[0] data_picks = _pick_data_channels(info, exclude=()) return decim, data_picks @@ -2153,7 +2151,7 @@ def _set_title_multiple_electrodes( def _check_time_unit(time_unit, times): if not isinstance(time_unit, str): - raise TypeError("time_unit must be str, got %s" % (type(time_unit),)) + raise TypeError(f"time_unit must be str, got {type(time_unit)}") if time_unit == "s": pass elif time_unit == "ms": @@ -2215,7 +2213,7 @@ def _plot_masked_image( if mask.shape != data.shape: raise ValueError( "The mask must have the same shape as the data, " - "i.e., %s, not %s" % (data.shape, mask.shape) + f"i.e., {data.shape}, not {mask.shape}" ) if draw_contour and yscale == "log": warn("Cannot draw contours with linear yscale yet ...") @@ -2322,7 +2320,7 @@ def _plot_masked_image( t_end = ", all points masked)" else: fraction = 1 - (np.float64(mask.sum()) / np.float64(mask.size)) - t_end = ", %0.3g%% of points masked)" % (fraction * 100,) + t_end = f", {fraction * 100:0.3g}% of points masked)" else: t_end = ")" diff --git a/pyproject.toml b/pyproject.toml index f0f3402e460..7bf34bf3fc8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -208,12 +208,12 @@ skip = "doc/references.bib" exclude = ["__init__.py", "constants.py", "resources.py"] [tool.ruff.lint] -select = ["A", "B006", "D", "E", "F", "I", "W", "UP"] +select = ["A", "B006", "D", "E", "F", "I", "W", "UP", "UP031"] ignore = [ "D100", # Missing docstring in public module "D104", # Missing docstring in public package "D413", # Missing blank line after last section - "UP031", # Use format specifiers instead of percent format + ] [tool.ruff.lint.pydocstyle] From 415e7f68ed71135baff0ea857ca4fab5a3690bf8 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Tue, 19 Mar 2024 13:27:08 -0400 Subject: [PATCH 235/405] BUG: Fix bug with minimum phase filters (#12507) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Daniel McCloy --- doc/changes/devel/12507.bugfix.rst | 5 ++ mne/filter.py | 73 ++----------------- mne/fixes.py | 55 ++++++++++++++ mne/tests/test_filter.py | 62 ++++++++++++++-- mne/utils/docs.py | 39 +++++++--- .../preprocessing/25_background_filtering.py | 26 +++---- 6 files changed, 162 insertions(+), 98 deletions(-) create mode 100644 doc/changes/devel/12507.bugfix.rst diff --git a/doc/changes/devel/12507.bugfix.rst b/doc/changes/devel/12507.bugfix.rst new file mode 100644 index 00000000000..c172701bb93 --- /dev/null +++ b/doc/changes/devel/12507.bugfix.rst @@ -0,0 +1,5 @@ +Fix bug where using ``phase="minimum"`` in filtering functions like +:meth:`mne.io.Raw.filter` constructed a filter half the desired length with +compromised attenuation. Now ``phase="minimum"`` has the same length and comparable +suppression as ``phase="zero"``, and the old (incorrect) behavior can be achieved +with ``phase="minimum-half"``, by `Eric Larson`_. diff --git a/mne/filter.py b/mne/filter.py index 290ddf7f7d7..82b77a17a7c 100644 --- a/mne/filter.py +++ b/mne/filter.py @@ -20,6 +20,7 @@ _setup_cuda_fft_resample, _smart_pad, ) +from .fixes import minimum_phase from .parallel import parallel_func from .utils import ( _check_option, @@ -307,39 +308,7 @@ def _overlap_add_filter( copy=True, pad="reflect_limited", ): - """Filter the signal x using h with overlap-add FFTs. - - Parameters - ---------- - x : array, shape (n_signals, n_times) - Signals to filter. - h : 1d array - Filter impulse response (FIR filter coefficients). Must be odd length - if ``phase='linear'``. - n_fft : int - Length of the FFT. If None, the best size is determined automatically. - phase : str - If ``'zero'``, the delay for the filter is compensated (and it must be - an odd-length symmetric filter). If ``'linear'``, the response is - uncompensated. If ``'zero-double'``, the filter is applied in the - forward and reverse directions. If 'minimum', a minimum-phase - filter will be used. - picks : list | None - See calling functions. - n_jobs : int | str - Number of jobs to run in parallel. Can be ``'cuda'`` if ``cupy`` - is installed properly. - copy : bool - If True, a copy of x, filtered, is returned. Otherwise, it operates - on x in place. - pad : str - Padding type for ``_smart_pad``. - - Returns - ------- - x : array, shape (n_signals, n_times) - x filtered. - """ + """Filter the signal x using h with overlap-add FFTs.""" # set up array for filtering, reshape to 2D, operate on last axis x, orig_shape, picks = _prep_for_filtering(x, copy, picks) # Extend the signal by mirroring the edges to reduce transient filter @@ -526,34 +495,6 @@ def _construct_fir_filter( (windowing is a smoothing in frequency domain). If x is multi-dimensional, this operates along the last dimension. - - Parameters - ---------- - sfreq : float - Sampling rate in Hz. - freq : 1d array - Frequency sampling points in Hz. - gain : 1d array - Filter gain at frequency sampling points. - Must be all 0 and 1 for fir_design=="firwin". - filter_length : int - Length of the filter to use. Must be odd length if phase == "zero". - phase : str - If 'zero', the delay for the filter is compensated (and it must be - an odd-length symmetric filter). If 'linear', the response is - uncompensated. If 'zero-double', the filter is applied in the - forward and reverse directions. If 'minimum', a minimum-phase - filter will be used. - fir_window : str - The window to use in FIR design, can be "hamming" (default), - "hann", or "blackman". - fir_design : str - Can be "firwin2" or "firwin". - - Returns - ------- - h : array - Filter coefficients. """ assert freq[0] == 0 if fir_design == "firwin2": @@ -562,7 +503,7 @@ def _construct_fir_filter( assert fir_design == "firwin" fir_design = partial(_firwin_design, sfreq=sfreq) # issue a warning if attenuation is less than this - min_att_db = 12 if phase == "minimum" else 20 + min_att_db = 12 if phase == "minimum-half" else 20 # normalize frequencies freq = np.array(freq) / (sfreq / 2.0) @@ -575,11 +516,13 @@ def _construct_fir_filter( # Use overlap-add filter with a fixed length N = _check_zero_phase_length(filter_length, phase, gain[-1]) # construct symmetric (linear phase) filter - if phase == "minimum": + if phase == "minimum-half": h = fir_design(N * 2 - 1, freq, gain, window=fir_window) - h = signal.minimum_phase(h) + h = minimum_phase(h) else: h = fir_design(N, freq, gain, window=fir_window) + if phase == "minimum": + h = minimum_phase(h, half=False) assert h.size == N att_db, att_freq = _filter_attenuation(h, freq, gain) if phase == "zero-double": @@ -2162,7 +2105,7 @@ def detrend(x, order=1, axis=-1): "blackman": dict(name="Blackman", ripple=0.0017, attenuation=74), } _known_fir_windows = tuple(sorted(_fir_window_dict.keys())) -_known_phases_fir = ("linear", "zero", "zero-double", "minimum") +_known_phases_fir = ("linear", "zero", "zero-double", "minimum", "minimum-half") _known_phases_iir = ("zero", "zero-double", "forward") _known_fir_designs = ("firwin", "firwin2") _fir_design_dict = { diff --git a/mne/fixes.py b/mne/fixes.py index 2af4eba73b9..6d874be8805 100644 --- a/mne/fixes.py +++ b/mne/fixes.py @@ -889,3 +889,58 @@ def _numpy_h5py_dep(): "ignore", "`product` is deprecated.*", DeprecationWarning ) yield + + +def minimum_phase(h, method="homomorphic", n_fft=None, *, half=True): + """Wrap scipy.signal.minimum_phase with half option.""" + # Can be removed once + from scipy.fft import fft, ifft + from scipy.signal import minimum_phase as sp_minimum_phase + + assert isinstance(method, str) and method == "homomorphic" + + if "half" in inspect.getfullargspec(sp_minimum_phase).kwonlyargs: + return sp_minimum_phase(h, method=method, n_fft=n_fft, half=half) + h = np.asarray(h) + if np.iscomplexobj(h): + raise ValueError("Complex filters not supported") + if h.ndim != 1 or h.size <= 2: + raise ValueError("h must be 1-D and at least 2 samples long") + n_half = len(h) // 2 + if not np.allclose(h[-n_half:][::-1], h[:n_half]): + warnings.warn( + "h does not appear to by symmetric, conversion may fail", + RuntimeWarning, + stacklevel=2, + ) + if n_fft is None: + 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)) + + # zero-pad; calculate the DFT + h_temp = np.abs(fft(h, n_fft)) + # take 0.25*log(|H|**2) = 0.5*log(|H|) + h_temp += 1e-7 * h_temp[h_temp > 0].min() # don't let log blow up + np.log(h_temp, out=h_temp) + if half: # halving of magnitude spectrum optional + h_temp *= 0.5 + # IDFT + h_temp = ifft(h_temp).real + # multiply pointwise by the homomorphic filter + # lmin[n] = 2u[n] - d[n] + # i.e., double the positive frequencies and zero out the negative ones; + # Oppenheim+Shafer 3rd ed p991 eq13.42b and p1004 fig13.7 + win = np.zeros(n_fft) + win[0] = 1 + stop = n_fft // 2 + win[1:stop] = 2 + if n_fft % 2: + win[stop] = 1 + h_temp *= win + h_temp = ifft(np.exp(fft(h_temp))) + h_minimum = h_temp.real + + n_out = (n_half + len(h) % 2) if half else len(h) + return h_minimum[:n_out] diff --git a/mne/tests/test_filter.py b/mne/tests/test_filter.py index 23ff37b8591..00dce484a08 100644 --- a/mne/tests/test_filter.py +++ b/mne/tests/test_filter.py @@ -606,12 +606,12 @@ def test_filters(): # try new default and old default freqs = fftfreq(a.shape[-1], 1.0 / sfreq) A = np.abs(fft(a)) - kwargs = dict(fir_design="firwin") + kw = dict(fir_design="firwin") for fl in ["auto", "10s", "5000ms", 1024, 1023]: - bp = filter_data(a, sfreq, 4, 8, None, fl, 1.0, 1.0, **kwargs) - bs = filter_data(a, sfreq, 8 + 1.0, 4 - 1.0, None, fl, 1.0, 1.0, **kwargs) - lp = filter_data(a, sfreq, None, 8, None, fl, 10, 1.0, n_jobs=2, **kwargs) - hp = filter_data(lp, sfreq, 4, None, None, fl, 1.0, 10, **kwargs) + bp = filter_data(a, sfreq, 4, 8, None, fl, 1.0, 1.0, **kw) + bs = filter_data(a, sfreq, 8 + 1.0, 4 - 1.0, None, fl, 1.0, 1.0, **kw) + lp = filter_data(a, sfreq, None, 8, None, fl, 10, 1.0, n_jobs=2, **kw) + hp = filter_data(lp, sfreq, 4, None, None, fl, 1.0, 10, **kw) assert_allclose(hp, bp, rtol=1e-3, atol=2e-3) assert_allclose(bp + bs, a, rtol=1e-3, atol=1e-3) # Sanity check ttenuation @@ -619,12 +619,18 @@ def test_filters(): assert_allclose(np.mean(np.abs(fft(bp)[:, mask]) / A[:, mask]), 1.0, atol=0.02) assert_allclose(np.mean(np.abs(fft(bs)[:, mask]) / A[:, mask]), 0.0, atol=0.2) # now the minimum-phase versions - bp = filter_data(a, sfreq, 4, 8, None, fl, 1.0, 1.0, phase="minimum", **kwargs) + bp = filter_data(a, sfreq, 4, 8, None, fl, 1.0, 1.0, phase="minimum-half", **kw) bs = filter_data( - a, sfreq, 8 + 1.0, 4 - 1.0, None, fl, 1.0, 1.0, phase="minimum", **kwargs + a, sfreq, 8 + 1.0, 4 - 1.0, None, fl, 1.0, 1.0, phase="minimum-half", **kw ) assert_allclose(np.mean(np.abs(fft(bp)[:, mask]) / A[:, mask]), 1.0, atol=0.11) assert_allclose(np.mean(np.abs(fft(bs)[:, mask]) / A[:, mask]), 0.0, atol=0.3) + bp = filter_data(a, sfreq, 4, 8, None, fl, 1.0, 1.0, phase="minimum", **kw) + bs = filter_data( + a, sfreq, 8 + 1.0, 4 - 1.0, None, fl, 1.0, 1.0, phase="minimum", **kw + ) + assert_allclose(np.mean(np.abs(fft(bp)[:, mask]) / A[:, mask]), 1.0, atol=0.12) + assert_allclose(np.mean(np.abs(fft(bs)[:, mask]) / A[:, mask]), 0.0, atol=0.27) # and since these are low-passed, downsampling/upsampling should be close n_resamp_ignore = 10 @@ -1050,3 +1056,45 @@ def test_filter_picks(): raw.filter(picks=picks, **kwargs) want = want[1:] assert_allclose(raw.get_data(), want) + + +def test_filter_minimum_phase_bug(): + """Test gh-12267 is fixed.""" + sfreq = 1000.0 + n_taps = 1001 + l_freq = 10.0 # Hz + kwargs = dict( + data=None, + sfreq=sfreq, + l_freq=l_freq, + h_freq=None, + filter_length=n_taps, + l_trans_bandwidth=l_freq / 2.0, + ) + h = create_filter(phase="zero", **kwargs) + h_min = create_filter(phase="minimum", **kwargs) + h_min_half = create_filter(phase="minimum-half", **kwargs) + assert h_min.size == h.size + kwargs = dict(worN=10000, fs=sfreq) + w, H = freqz(h, **kwargs) + assert w[0] == 0 + dc_dB = 20 * np.log10(np.abs(H[0])) + assert dc_dB < -100 + # good + w_min, H_min = freqz(h_min, **kwargs) + assert_allclose(w, w_min) + dc_dB_min = 20 * np.log10(np.abs(H_min[0])) + assert dc_dB_min < -100 + mask = w < 5 + assert 10 < mask.sum() < 101 + assert_allclose(np.abs(H[mask]), np.abs(H_min[mask]), atol=1e-3, rtol=1e-3) + assert_array_less(20 * np.log10(np.abs(H[mask])), -40) + assert_array_less(20 * np.log10(np.abs(H_min[mask])), -40) + # bad + w_min_half, H_min_half = freqz(h_min_half, **kwargs) + assert_allclose(w, w_min_half) + dc_dB_min_half = 20 * np.log10(np.abs(H_min_half[0])) + assert -80 < dc_dB_min_half < 40 + dB_min_half = 20 * np.log10(np.abs(H_min_half[mask])) + assert_array_less(dB_min_half, -20) + assert not (dB_min_half < -30).all() diff --git a/mne/utils/docs.py b/mne/utils/docs.py index e8c30716d48..f5d7c4f4669 100644 --- a/mne/utils/docs.py +++ b/mne/utils/docs.py @@ -2809,21 +2809,36 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): docdict["phase"] = """ phase : str Phase of the filter. - When ``method='fir'``, symmetric linear-phase FIR filters are constructed, - and if ``phase='zero'`` (default), the delay of this filter is compensated - for, making it non-causal. If ``phase='zero-double'``, - then this filter is applied twice, once forward, and once backward - (also making it non-causal). If ``'minimum'``, then a minimum-phase filter - will be constructed and applied, which is causal but has weaker stop-band - suppression. - When ``method='iir'``, ``phase='zero'`` (default) or - ``phase='zero-double'`` constructs and applies IIR filter twice, once - forward, and once backward (making it non-causal) using - :func:`~scipy.signal.filtfilt`. - If ``phase='forward'``, it constructs and applies forward IIR filter using + When ``method='fir'``, symmetric linear-phase FIR filters are constructed + with the following behaviors when ``method="fir"``: + + ``"zero"`` (default) + The delay of this filter is compensated for, making it non-causal. + ``"minimum"`` + A minimum-phase filter will be constructed by decomposing the zero-phase filter + into a minimum-phase and all-pass systems, and then retaining only the + minimum-phase system (of the same length as the original zero-phase filter) + via :func:`scipy.signal.minimum_phase`. + ``"zero-double"`` + *This is a legacy option for compatibility with MNE <= 0.13.* + The filter is applied twice, once forward, and once backward + (also making it non-causal). + ``"minimum-half"`` + *This is a legacy option for compatibility with MNE <= 1.6.* + A minimum-phase filter will be reconstructed from the zero-phase filter with + half the length of the original filter. + + When ``method='iir'``, ``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 + .. versionchanged:: 1.7 + + The behavior for ``phase="minimum"`` was fixed to use a filter of the requested + length and improved suppression. """ docdict["physical_range_export_params"] = """ diff --git a/tutorials/preprocessing/25_background_filtering.py b/tutorials/preprocessing/25_background_filtering.py index cbd10ab213b..c0f56098bad 100644 --- a/tutorials/preprocessing/25_background_filtering.py +++ b/tutorials/preprocessing/25_background_filtering.py @@ -148,6 +148,7 @@ from scipy import signal import mne +from mne.fixes import minimum_phase from mne.time_frequency.tfr import morlet from mne.viz import plot_filter, plot_ideal_filter @@ -168,7 +169,7 @@ gain = [1, 1, 0, 0] third_height = np.array(plt.rcParams["figure.figsize"]) * [1, 1.0 / 3.0] -ax = plt.subplots(1, figsize=third_height)[1] +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) # %% @@ -249,7 +250,7 @@ freq = [0.0, f_p, f_s, nyq] gain = [1.0, 1.0, 0.0, 0.0] -ax = plt.subplots(1, figsize=third_height)[1] +ax = plt.subplots(1, figsize=third_height, layout="constrained")[1] title = f"{f_p} Hz lowpass with a {trans_bandwidth} Hz transition" plot_ideal_filter(freq, gain, ax, title=title, flim=flim) @@ -316,15 +317,15 @@ # is constant) but small in the pass-band. Unlike zero-phase filters, which # require time-shifting backward the output of a linear-phase filtering stage # (and thus becoming non-causal), minimum-phase filters do not require any -# compensation to achieve small delays in the pass-band. Note that as an -# artifact of the minimum phase filter construction step, the filter does -# not end up being as steep as the linear/zero-phase version. +# compensation to achieve small delays in the pass-band. # # We can construct a minimum-phase filter from our existing linear-phase -# filter with the :func:`scipy.signal.minimum_phase` function, and note -# that the falloff is not as steep: +# filter, and note that the falloff is not as steep. Here we do this with function +# ``mne.fixes.minimum_phase()`` to avoid a SciPy bug; once SciPy 1.14.0 is released you +# could directly use +# :func:`scipy.signal.minimum_phase(..., half=False) `. -h_min = signal.minimum_phase(h) +h_min = minimum_phase(h, half=False) plot_filter(h_min, sfreq, freq, gain, "Minimum-phase", **kwargs) # %% @@ -683,7 +684,6 @@ def plot_signal(x, offset): for text in axes[0].get_yticklabels(): text.set(rotation=45, size=8) axes[1].set(xlim=flim, ylim=(-60, 10), xlabel="Frequency (Hz)", ylabel="Magnitude (dB)") -mne.viz.adjust_axes(axes) plt.show() # %% @@ -779,7 +779,7 @@ def plot_signal(x, offset): xlabel = "Time (s)" ylabel = r"Amplitude ($\mu$V)" tticks = [0, 0.5, 1.3, t[-1]] -axes = plt.subplots(2, 2)[1].ravel() +axes = plt.subplots(2, 2, layout="constrained")[1].ravel() for ax, x_f, title in zip( axes, [x_lp_2, x_lp_30, x_hp_2, x_hp_p1], @@ -791,7 +791,6 @@ def plot_signal(x, offset): ylim=ylim, xlim=xlim, xticks=tticks, title=title, xlabel=xlabel, ylabel=ylabel ) -mne.viz.adjust_axes(axes) plt.show() # %% @@ -830,7 +829,7 @@ def plot_signal(x, offset): def baseline_plot(x): - all_axes = plt.subplots(3, 2, layout="constrained")[1] + fig, all_axes = plt.subplots(3, 2, layout="constrained") for ri, (axes, freq) in enumerate(zip(all_axes, [0.1, 0.3, 0.5])): for ci, ax in enumerate(axes): if ci == 0: @@ -846,8 +845,7 @@ def baseline_plot(x): 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") - mne.viz.adjust_axes(axes) - plt.suptitle(title) + fig.suptitle(title) plt.show() From cf6e13c1e7a0cf0fabbcb33865610007bd891ef3 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Wed, 20 Mar 2024 11:00:08 -0400 Subject: [PATCH 236/405] MAINT: Reenable pre scipy and numpy (#12506) --- azure-pipelines.yml | 4 +++- tools/azure_dependencies.sh | 2 +- tools/github_actions_dependencies.sh | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 43cdb1db960..b6f2fd679a1 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -238,7 +238,7 @@ stages: variables: MNE_LOGGING_LEVEL: 'warning' MNE_FORCE_SERIAL: 'true' - OPENBLAS_NUM_THREADS: 2 + OPENBLAS_NUM_THREADS: '1' # deal with OpenBLAS conflicts safely on Windows OMP_DYNAMIC: 'false' PYTHONUNBUFFERED: 1 PYTHONIOENCODING: 'utf-8' @@ -274,6 +274,8 @@ stages: displayName: 'Print config' - script: python -c "import numpy; numpy.show_config()" displayName: Print NumPy config + - script: python -c "import numpy; import scipy.linalg; import sklearn.neighbors; from threadpoolctl import threadpool_info; from pprint import pprint; pprint(threadpool_info())" + displayName: Print threadpoolctl info - bash: source tools/get_testing_version.sh displayName: 'Get testing version' - task: Cache@2 diff --git a/tools/azure_dependencies.sh b/tools/azure_dependencies.sh index 7a691d25c29..47eae988efb 100755 --- a/tools/azure_dependencies.sh +++ b/tools/azure_dependencies.sh @@ -9,7 +9,7 @@ elif [ "${TEST_MODE}" == "pip-pre" ]; then # python -m pip install $STD_ARGS --only-binary ":all:" --extra-index-url "https://www.riverbankcomputing.com/pypi/simple" "PyQt6!=6.6.1,!=6.6.2" PyQt6-sip PyQt6-Qt6 "PyQt6-Qt6!=6.6.1,!=6.6.2" python -m pip install $STD_ARGS --only-binary ":all:" "PyQt6!=6.6.1,!=6.6.2" PyQt6-sip PyQt6-Qt6 "PyQt6-Qt6!=6.6.1,!=6.6.2" echo "Numpy etc." - python -m pip install $STD_ARGS --only-binary ":all:" --default-timeout=60 --extra-index-url "https://pypi.anaconda.org/scientific-python-nightly-wheels/simple" "numpy<2.0.0.dev0" "scipy==1.12.0" "scikit-learn>=1.5.dev0" matplotlib pillow statsmodels pyarrow h5py + python -m pip install $STD_ARGS --only-binary ":all:" --default-timeout=60 --extra-index-url "https://pypi.anaconda.org/scientific-python-nightly-wheels/simple" "numpy<2.0.0.dev0" "scipy>=1.14.0.dev0" "scikit-learn>=1.5.dev0" matplotlib pillow statsmodels pyarrow h5py # echo "dipy" # python -m pip install $STD_ARGS --only-binary ":all:" --extra-index-url "https://pypi.anaconda.org/scipy-wheels-nightly/simple" dipy echo "OpenMEEG" diff --git a/tools/github_actions_dependencies.sh b/tools/github_actions_dependencies.sh index d6996472acc..0902b3f0afa 100755 --- a/tools/github_actions_dependencies.sh +++ b/tools/github_actions_dependencies.sh @@ -30,7 +30,7 @@ else # pip install $STD_ARGS --only-binary ":all:" --default-timeout=60 --extra-index-url https://www.riverbankcomputing.com/pypi/simple "PyQt6!=6.6.1,!=6.6.2" "PyQt6-Qt6!=6.6.1,!=6.6.2" pip install $STD_ARGS --only-binary ":all:" --default-timeout=60 "PyQt6!=6.6.1,!=6.6.2" "PyQt6-Qt6!=6.6.1,!=6.6.2" echo "NumPy/SciPy/pandas etc." - pip install $STD_ARGS --only-binary ":all:" --default-timeout=60 --extra-index-url "https://pypi.anaconda.org/scientific-python-nightly-wheels/simple" "numpy<2.0.0.dev0" "scipy==1.12.0" "scikit-learn>=1.5.dev0" matplotlib pillow statsmodels pyarrow pandas h5py + pip install $STD_ARGS --only-binary ":all:" --default-timeout=60 --extra-index-url "https://pypi.anaconda.org/scientific-python-nightly-wheels/simple" "numpy<2.0.0.dev0" "scipy>=1.14.0.dev0" "scikit-learn>=1.5.dev0" matplotlib pillow statsmodels pyarrow pandas h5py # No dipy, python-picard (needs numexpr) until they update to NumPy 2.0 compat INSTALL_KIND="test_extra" echo "OpenMEEG" From 925f522829f74ded48935f39b51ef270adc70923 Mon Sep 17 00:00:00 2001 From: Daniel McCloy Date: Wed, 20 Mar 2024 12:58:14 -0500 Subject: [PATCH 237/405] updating TFR classes (#11282) Co-authored-by: Eric Larson Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- doc/api/time_frequency.rst | 5 + doc/changes/devel/11282.apichange.rst | 1 + doc/changes/devel/11282.bugfix.rst | 1 + doc/changes/devel/11282.newfeature.rst | 1 + doc/conf.py | 4 + examples/decoding/decoding_csp_timefreq.py | 18 +- examples/inverse/dics_epochs.py | 6 +- .../time_frequency/time_frequency_erds.py | 5 +- .../time_frequency_simulated.py | 80 +- mne/beamformer/tests/test_dics.py | 4 +- mne/channels/channels.py | 10 +- mne/conftest.py | 55 +- mne/decoding/time_frequency.py | 22 +- mne/epochs.py | 149 +- mne/evoked.py | 61 + mne/html_templates/repr/tfr.html.jinja | 60 + mne/io/base.py | 64 + mne/minimum_norm/tests/test_inverse.py | 6 +- mne/time_frequency/__init__.pyi | 12 +- mne/time_frequency/_stockwell.py | 79 +- mne/time_frequency/multitaper.py | 3 +- mne/time_frequency/spectrum.py | 87 +- mne/time_frequency/tests/test_spectrum.py | 12 +- mne/time_frequency/tests/test_tfr.py | 1194 ++--- mne/time_frequency/tfr.py | 4062 +++++++++++------ mne/utils/__init__.pyi | 2 + mne/utils/_testing.py | 10 + mne/utils/check.py | 22 +- mne/utils/docs.py | 726 ++- mne/utils/mixin.py | 19 +- mne/utils/numerics.py | 17 +- mne/utils/spectrum.py | 22 + mne/viz/tests/test_topo.py | 35 +- mne/viz/tests/test_topomap.py | 14 +- mne/viz/topo.py | 3 +- mne/viz/topomap.py | 6 +- mne/viz/utils.py | 23 +- pyproject.toml | 2 +- tutorials/intro/10_overview.py | 4 +- .../40_cluster_1samp_time_freq.py | 5 +- .../50_cluster_between_time_freq.py | 18 +- .../70_cluster_rmANOVA_time_freq.py | 5 +- .../75_cluster_ftest_spatiotemporal.py | 7 +- .../time-freq/20_sensors_time_frequency.py | 12 +- 44 files changed, 4565 insertions(+), 2388 deletions(-) create mode 100644 doc/changes/devel/11282.apichange.rst create mode 100644 doc/changes/devel/11282.bugfix.rst create mode 100644 doc/changes/devel/11282.newfeature.rst create mode 100644 mne/html_templates/repr/tfr.html.jinja diff --git a/doc/api/time_frequency.rst b/doc/api/time_frequency.rst index f8948909491..8923920bdba 100644 --- a/doc/api/time_frequency.rst +++ b/doc/api/time_frequency.rst @@ -14,7 +14,12 @@ Time-Frequency :toctree: ../generated/ AverageTFR + AverageTFRArray + BaseTFR EpochsTFR + EpochsTFRArray + RawTFR + RawTFRArray CrossSpectralDensity Spectrum SpectrumArray diff --git a/doc/changes/devel/11282.apichange.rst b/doc/changes/devel/11282.apichange.rst new file mode 100644 index 00000000000..9112db897cf --- /dev/null +++ b/doc/changes/devel/11282.apichange.rst @@ -0,0 +1 @@ +The default value of the ``zero_mean`` parameter of :func:`mne.time_frequency.tfr_array_morlet` will change from ``False`` to ``True`` in version 1.8, for consistency with related functions. By `Daniel McCloy`_. diff --git a/doc/changes/devel/11282.bugfix.rst b/doc/changes/devel/11282.bugfix.rst new file mode 100644 index 00000000000..72e6e73a42a --- /dev/null +++ b/doc/changes/devel/11282.bugfix.rst @@ -0,0 +1 @@ +Fixes to interactivity in time-frequency objects: the rectangle selector now works on TFR image plots of gradiometer data; and in ``TFR.plot_joint()`` plots, the colormap limits of interactively-generated topomaps match the colormap limits of the main plot. By `Daniel McCloy`_. \ No newline at end of file diff --git a/doc/changes/devel/11282.newfeature.rst b/doc/changes/devel/11282.newfeature.rst new file mode 100644 index 00000000000..5c19d68f351 --- /dev/null +++ b/doc/changes/devel/11282.newfeature.rst @@ -0,0 +1 @@ +New class :class:`mne.time_frequency.RawTFR` and new methods :meth:`mne.io.Raw.compute_tfr`, :meth:`mne.Epochs.compute_tfr`, and :meth:`mne.Evoked.compute_tfr`. These new methods supersede functions :func:`mne.time_frequency.tfr_morlet`, and :func:`mne.time_frequency.tfr_multitaper`, and :func:`mne.time_frequency.tfr_stockwell`, which are now considered "legacy" functions. By `Daniel McCloy`_. \ No newline at end of file diff --git a/doc/conf.py b/doc/conf.py index b2dbe387f27..cc2a25d7089 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -231,7 +231,11 @@ "EvokedArray": "mne.EvokedArray", "BiHemiLabel": "mne.BiHemiLabel", "AverageTFR": "mne.time_frequency.AverageTFR", + "AverageTFRArray": "mne.time_frequency.AverageTFRArray", "EpochsTFR": "mne.time_frequency.EpochsTFR", + "EpochsTFRArray": "mne.time_frequency.EpochsTFRArray", + "RawTFR": "mne.time_frequency.RawTFR", + "RawTFRArray": "mne.time_frequency.RawTFRArray", "Raw": "mne.io.Raw", "ICA": "mne.preprocessing.ICA", "Covariance": "mne.Covariance", diff --git a/examples/decoding/decoding_csp_timefreq.py b/examples/decoding/decoding_csp_timefreq.py index 6f13175846e..c389645d668 100644 --- a/examples/decoding/decoding_csp_timefreq.py +++ b/examples/decoding/decoding_csp_timefreq.py @@ -32,7 +32,7 @@ from mne.datasets import eegbci from mne.decoding import CSP from mne.io import concatenate_raws, read_raw_edf -from mne.time_frequency import AverageTFR +from mne.time_frequency import AverageTFRArray # %% # Set parameters and read data @@ -173,13 +173,15 @@ # Plot time-frequency results # Set up time frequency object -av_tfr = AverageTFR( - create_info(["freq"], sfreq), - tf_scores[np.newaxis, :], - centered_w_times, - freqs[1:], - 1, +av_tfr = AverageTFRArray( + info=create_info(["freq"], sfreq), + data=tf_scores[np.newaxis, :], + times=centered_w_times, + freqs=freqs[1:], + nave=1, ) chance = np.mean(y) # set chance level to white in the plot -av_tfr.plot([0], vmin=chance, title="Time-Frequency Decoding Scores", cmap=plt.cm.Reds) +av_tfr.plot( + [0], vlim=(chance, None), title="Time-Frequency Decoding Scores", cmap=plt.cm.Reds +) diff --git a/examples/inverse/dics_epochs.py b/examples/inverse/dics_epochs.py index d480b13f8a4..c359c30c0fb 100644 --- a/examples/inverse/dics_epochs.py +++ b/examples/inverse/dics_epochs.py @@ -22,7 +22,7 @@ import mne from mne.beamformer import apply_dics_tfr_epochs, make_dics from mne.datasets import somato -from mne.time_frequency import csd_tfr, tfr_morlet +from mne.time_frequency import csd_tfr print(__doc__) @@ -67,8 +67,8 @@ # decomposition for each epoch. We must pass ``output='complex'`` if we wish to # use this TFR later with a DICS beamformer. We also pass ``average=False`` to # compute the TFR for each individual epoch. -epochs_tfr = tfr_morlet( - epochs, freqs, n_cycles=5, return_itc=False, output="complex", average=False +epochs_tfr = epochs.compute_tfr( + "morlet", freqs, n_cycles=5, return_itc=False, output="complex", average=False ) # crop either side to use a buffer to remove edge artifact diff --git a/examples/time_frequency/time_frequency_erds.py b/examples/time_frequency/time_frequency_erds.py index 556730b6cab..1d805121739 100644 --- a/examples/time_frequency/time_frequency_erds.py +++ b/examples/time_frequency/time_frequency_erds.py @@ -45,7 +45,6 @@ from mne.datasets import eegbci from mne.io import concatenate_raws, read_raw_edf from mne.stats import permutation_cluster_1samp_test as pcluster_test -from mne.time_frequency import tfr_multitaper # %% # First, we load and preprocess the data. We use runs 6, 10, and 14 from @@ -96,8 +95,8 @@ # %% # Finally, we perform time/frequency decomposition over all epochs. -tfr = tfr_multitaper( - epochs, +tfr = epochs.compute_tfr( + method="multitaper", freqs=freqs, n_cycles=freqs, use_fft=True, diff --git a/examples/time_frequency/time_frequency_simulated.py b/examples/time_frequency/time_frequency_simulated.py index 85cc9a1f436..dc42f16da3a 100644 --- a/examples/time_frequency/time_frequency_simulated.py +++ b/examples/time_frequency/time_frequency_simulated.py @@ -25,16 +25,8 @@ from matplotlib import pyplot as plt from mne import Epochs, create_info -from mne.baseline import rescale from mne.io import RawArray -from mne.time_frequency import ( - AverageTFR, - tfr_array_morlet, - tfr_morlet, - tfr_multitaper, - tfr_stockwell, -) -from mne.viz import centers_to_edges +from mne.time_frequency import AverageTFRArray, EpochsTFRArray, tfr_array_morlet print(__doc__) @@ -112,12 +104,13 @@ "Sim: Less time smoothing,\nmore frequency smoothing", ], ): - power = tfr_multitaper( - epochs, + power = epochs.compute_tfr( + method="multitaper", freqs=freqs, n_cycles=n_cycles, time_bandwidth=time_bandwidth, return_itc=False, + average=True, ) ax.set_title(title) # Plot results. Baseline correct based on first 100 ms. @@ -125,8 +118,7 @@ [0], baseline=(0.0, 0.1), mode="mean", - vmin=vmin, - vmax=vmax, + vlim=(vmin, vmax), axes=ax, show=False, colorbar=False, @@ -146,7 +138,7 @@ fig, axs = plt.subplots(1, 3, figsize=(15, 5), sharey=True, layout="constrained") fmin, fmax = freqs[[0, -1]] for width, ax in zip((0.2, 0.7, 3.0), axs): - power = tfr_stockwell(epochs, fmin=fmin, fmax=fmax, width=width) + power = epochs.compute_tfr(method="stockwell", freqs=(fmin, fmax), width=width) power.plot( [0], baseline=(0.0, 0.1), mode="mean", axes=ax, show=False, colorbar=False ) @@ -164,13 +156,14 @@ fig, axs = plt.subplots(1, 3, figsize=(15, 5), sharey=True, layout="constrained") all_n_cycles = [1, 3, freqs / 2.0] for n_cycles, ax in zip(all_n_cycles, axs): - power = tfr_morlet(epochs, freqs=freqs, n_cycles=n_cycles, return_itc=False) + power = epochs.compute_tfr( + method="morlet", freqs=freqs, n_cycles=n_cycles, return_itc=False, average=True + ) power.plot( [0], baseline=(0.0, 0.1), mode="mean", - vmin=vmin, - vmax=vmax, + vlim=(vmin, vmax), axes=ax, show=False, colorbar=False, @@ -190,7 +183,9 @@ fig, axs = plt.subplots(1, 3, figsize=(15, 5), sharey=True, layout="constrained") bandwidths = [1.0, 2.0, 4.0] for bandwidth, ax in zip(bandwidths, axs): - data = np.zeros((len(ch_names), freqs.size, epochs.times.size), dtype=complex) + data = np.zeros( + (len(epochs), len(ch_names), freqs.size, epochs.times.size), dtype=complex + ) for idx, freq in enumerate(freqs): # Filter raw data and re-epoch to avoid the filter being longer than # the epoch data for low frequencies and short epochs, such as here. @@ -210,17 +205,13 @@ epochs_hilb = Epochs( raw_filter, events, tmin=0, tmax=n_times / sfreq, baseline=(0, 0.1) ) - tfr_data = epochs_hilb.get_data() - tfr_data = tfr_data * tfr_data.conj() # compute power - tfr_data = np.mean(tfr_data, axis=0) # average over epochs - data[:, idx] = tfr_data - power = AverageTFR(info, data, epochs.times, freqs, nave=n_epochs) - power.plot( + data[:, :, idx] = epochs_hilb.get_data() + power = EpochsTFRArray(epochs.info, data, epochs.times, freqs, method="hilbert") + power.average().plot( [0], baseline=(0.0, 0.1), mode="mean", - vmin=-0.1, - vmax=0.1, + vlim=(0, 0.1), axes=ax, show=False, colorbar=False, @@ -241,8 +232,8 @@ # :class:`mne.time_frequency.EpochsTFR` is returned. n_cycles = freqs / 2.0 -power = tfr_morlet( - epochs, freqs=freqs, n_cycles=n_cycles, return_itc=False, average=False +power = epochs.compute_tfr( + method="morlet", freqs=freqs, n_cycles=n_cycles, return_itc=False, average=False ) print(type(power)) avgpower = power.average() @@ -250,8 +241,7 @@ [0], baseline=(0.0, 0.1), mode="mean", - vmin=vmin, - vmax=vmax, + vlim=(vmin, vmax), title="Using Morlet wavelets and EpochsTFR", show=False, ) @@ -260,10 +250,12 @@ # Operating on arrays # ------------------- # -# MNE also has versions of the functions above which operate on numpy arrays -# instead of MNE objects. They expect inputs of the shape -# ``(n_epochs, n_channels, n_times)``. They will also return a numpy array -# of shape ``(n_epochs, n_channels, n_freqs, n_times)``. +# MNE-Python also has functions that operate on :class:`NumPy arrays ` +# instead of MNE-Python objects. These are :func:`~mne.time_frequency.tfr_array_morlet` +# and :func:`~mne.time_frequency.tfr_array_multitaper`. They expect inputs of the shape +# ``(n_epochs, n_channels, n_times)`` and return an array of shape +# ``(n_epochs, n_channels, n_freqs, n_times)`` (or optionally, can collapse the epochs +# dimension if you want average power or inter-trial coherence; see ``output`` param). power = tfr_array_morlet( epochs.get_data(), @@ -271,12 +263,16 @@ freqs=freqs, n_cycles=n_cycles, output="avg_power", + zero_mean=False, +) +# Put it into a TFR container for easy plotting +tfr = AverageTFRArray( + info=epochs.info, data=power, times=epochs.times, freqs=freqs, nave=len(epochs) +) +tfr.plot( + baseline=(0.0, 0.1), + picks=[0], + mode="mean", + vlim=(vmin, vmax), + title="TFR calculated on a NumPy array", ) -# Baseline the output -rescale(power, epochs.times, (0.0, 0.1), mode="mean", copy=False) -fig, ax = plt.subplots(layout="constrained") -x, y = centers_to_edges(epochs.times * 1000, freqs) -mesh = ax.pcolormesh(x, y, power[0], cmap="RdBu_r", vmin=vmin, vmax=vmax) -ax.set_title("TFR calculated on a numpy array") -ax.set(ylim=freqs[[0, -1]], xlabel="Time (ms)") -fig.colorbar(mesh) diff --git a/mne/beamformer/tests/test_dics.py b/mne/beamformer/tests/test_dics.py index 1daaaf17eb0..bcde4503307 100644 --- a/mne/beamformer/tests/test_dics.py +++ b/mne/beamformer/tests/test_dics.py @@ -30,7 +30,7 @@ from mne.io import read_info from mne.proj import compute_proj_evoked, make_projector from mne.surface import _compute_nearest -from mne.time_frequency import CrossSpectralDensity, EpochsTFR, csd_morlet, csd_tfr +from mne.time_frequency import CrossSpectralDensity, EpochsTFRArray, csd_morlet, csd_tfr from mne.time_frequency.csd import _sym_mat_to_vector from mne.transforms import apply_trans, invert_transform from mne.utils import catch_logging, object_diff @@ -727,7 +727,7 @@ def test_apply_dics_tfr(return_generator): data = rng.random((n_epochs, n_chans, len(freqs), n_times)) data *= 1e-6 data = data + data * 1j # add imag. component to simulate phase - epochs_tfr = EpochsTFR(info, data, times=times, freqs=freqs) + epochs_tfr = EpochsTFRArray(info=info, data=data, times=times, freqs=freqs) # Create a DICS beamformer and convert the EpochsTFR to source space. csd = csd_tfr(epochs_tfr) diff --git a/mne/channels/channels.py b/mne/channels/channels.py index 0d0af8279cb..341e355f363 100644 --- a/mne/channels/channels.py +++ b/mne/channels/channels.py @@ -153,7 +153,7 @@ def equalize_channels(instances, copy=True, verbose=None): from ..evoked import Evoked from ..forward import Forward from ..io import BaseRaw - from ..time_frequency import CrossSpectralDensity, _BaseTFR + from ..time_frequency import BaseTFR, CrossSpectralDensity # Instances need to have a `ch_names` attribute and a `pick_channels` # method that supports `ordered=True`. @@ -161,7 +161,7 @@ def equalize_channels(instances, copy=True, verbose=None): BaseRaw, BaseEpochs, Evoked, - _BaseTFR, + BaseTFR, Forward, Covariance, CrossSpectralDensity, @@ -607,8 +607,6 @@ def drop_channels(self, ch_names, on_missing="raise"): def _pick_drop_channels(self, idx, *, verbose=None): # avoid circular imports from ..io import BaseRaw - from ..time_frequency import AverageTFR, EpochsTFR - from ..time_frequency.spectrum import BaseSpectrum msg = "adding, dropping, or reordering channels" if isinstance(self, BaseRaw): @@ -633,10 +631,8 @@ def _pick_drop_channels(self, idx, *, verbose=None): if mat is not None: setattr(self, key, mat[idx][:, idx]) - if isinstance(self, BaseSpectrum): + if hasattr(self, "_dims"): # Spectrum and "new-style" TFRs axis = self._dims.index("channel") - elif isinstance(self, (AverageTFR, EpochsTFR)): - axis = -3 else: # All others (Evoked, Epochs, Raw) have chs axis=-2 axis = -2 if hasattr(self, "_data"): # skip non-preloaded Raw diff --git a/mne/conftest.py b/mne/conftest.py index 2d153f92f40..7dd02366ace 100644 --- a/mne/conftest.py +++ b/mne/conftest.py @@ -397,6 +397,34 @@ def epochs_spectrum(): return _get_epochs().load_data().compute_psd() +@pytest.fixture() +def epochs_tfr(): + """Get an EpochsTFR computed from mne.io.tests.data.""" + epochs = _get_epochs().load_data() + return epochs.compute_tfr(method="morlet", freqs=np.linspace(20, 40, num=5)) + + +@pytest.fixture() +def average_tfr(epochs_tfr): + """Get an AverageTFR computed by averaging an EpochsTFR (this is small & fast).""" + return epochs_tfr.average() + + +@pytest.fixture() +def full_average_tfr(full_evoked): + """Get an AverageTFR computed from Evoked. + + This is slower than the `average_tfr` fixture, but a few TFR.plot_* tests need it. + """ + return full_evoked.compute_tfr(method="morlet", freqs=np.linspace(20, 40, num=5)) + + +@pytest.fixture() +def raw_tfr(raw): + """Get a RawTFR computed from mne.io.tests.data.""" + return raw.compute_tfr(method="morlet", freqs=np.linspace(20, 40, num=5)) + + @pytest.fixture() def epochs_empty(): """Get empty epochs from mne.io.tests.data.""" @@ -408,22 +436,31 @@ def epochs_empty(): @pytest.fixture(scope="session", params=[testing._pytest_param()]) -def _evoked(): - # This one is session scoped, so be sure not to modify it (use evoked - # instead) - evoked = mne.read_evokeds( - fname_evoked, condition="Left Auditory", baseline=(None, 0) - ) - evoked.crop(0, 0.2) - return evoked +def _full_evoked(): + # This is session scoped, so be sure not to modify its return value (use + # `full_evoked` fixture instead) + return mne.read_evokeds(fname_evoked, condition="Left Auditory", baseline=(None, 0)) + + +@pytest.fixture(scope="session", params=[testing._pytest_param()]) +def _evoked(_full_evoked): + # This is session scoped, so be sure not to modify its return value (use `evoked` + # fixture instead) + return _full_evoked.copy().crop(0, 0.2) @pytest.fixture() def evoked(_evoked): - """Get evoked data.""" + """Get truncated evoked data.""" return _evoked.copy() +@pytest.fixture() +def full_evoked(_full_evoked): + """Get full-duration evoked data (needed for, e.g., testing TFR).""" + return _full_evoked.copy() + + @pytest.fixture(scope="function", params=[testing._pytest_param()]) def noise_cov(): """Get a noise cov from the testing dataset.""" diff --git a/mne/decoding/time_frequency.py b/mne/decoding/time_frequency.py index bd0076d0355..0555d190ddd 100644 --- a/mne/decoding/time_frequency.py +++ b/mne/decoding/time_frequency.py @@ -150,17 +150,17 @@ def transform(self, X): # Compute time-frequency Xt = _compute_tfr( X, - self.freqs, - self.sfreq, - self.method, - self.n_cycles, - True, - self.time_bandwidth, - self.use_fft, - self.decim, - self.output, - self.n_jobs, - self.verbose, + freqs=self.freqs, + sfreq=self.sfreq, + method=self.method, + n_cycles=self.n_cycles, + zero_mean=True, + time_bandwidth=self.time_bandwidth, + use_fft=self.use_fft, + decim=self.decim, + output=self.output, + n_jobs=self.n_jobs, + verbose=self.verbose, ) # Back to original shape diff --git a/mne/epochs.py b/mne/epochs.py index 14a0092c07a..9e48936f8bf 100644 --- a/mne/epochs.py +++ b/mne/epochs.py @@ -75,7 +75,7 @@ from .html_templates import _get_html_template from .parallel import parallel_func from .time_frequency.spectrum import EpochsSpectrum, SpectrumMixin, _validate_method -from .time_frequency.tfr import EpochsTFR +from .time_frequency.tfr import AverageTFR, EpochsTFR from .utils import ( ExtendedTimeMixin, GetEpochsMixin, @@ -419,6 +419,8 @@ class BaseEpochs( filename : str | None The filename (if the epochs are read from disk). %(metadata_epochs)s + + .. versionadded:: 0.16 %(event_repeated_epochs)s %(raw_sfreq)s annotations : instance of mne.Annotations | None @@ -2560,6 +2562,139 @@ def compute_psd( **method_kw, ) + @verbose + def compute_tfr( + self, + method, + freqs, + *, + tmin=None, + tmax=None, + picks=None, + proj=False, + output="power", + average=False, + return_itc=False, + decim=1, + n_jobs=None, + verbose=None, + **method_kw, + ): + """Compute a time-frequency representation of epoched data. + + Parameters + ---------- + %(method_tfr_epochs)s + %(freqs_tfr_epochs)s + %(tmin_tmax_psd)s + %(picks_good_data_noref)s + %(proj_psd)s + %(output_compute_tfr)s + average : bool + Whether to return average power across epochs (instead of single-trial + power). ``average=True`` is not compatible with ``output="complex"`` or + ``output="phase"``. Ignored if ``method="stockwell"`` (Stockwell method + *requires* averaging). Default is ``False``. + return_itc : bool + Whether to return inter-trial coherence (ITC) as well as power estimates. + If ``True`` then must specify ``average=True`` (or ``method="stockwell", + average="auto"``). Default is ``False``. + %(decim_tfr)s + %(n_jobs)s + %(verbose)s + %(method_kw_epochs_tfr)s + + Returns + ------- + tfr : instance of EpochsTFR or AverageTFR + The time-frequency-resolved power estimates. + itc : instance of AverageTFR + The inter-trial coherence (ITC). Only returned if ``return_itc=True``. + + Notes + ----- + If ``average=True`` (or ``method="stockwell", average="auto"``) the result will + be an :class:`~mne.time_frequency.AverageTFR` instead of an + :class:`~mne.time_frequency.EpochsTFR`. + + .. versionadded:: 1.7 + + References + ---------- + .. footbibliography:: + """ + if method == "stockwell" and not average: # stockwell method *must* average + logger.info( + 'Requested `method="stockwell"` so ignoring parameter `average=False`.' + ) + average = True + if average: + # augment `output` value for use by tfr_array_* functions + _check_option("output", output, ("power",), extra=" when average=True") + method_kw["output"] = "avg_power_itc" if return_itc else "avg_power" + else: + msg = ( + "compute_tfr() got incompatible parameters `average=False` and `{}` " + "({} requires averaging over epochs)." + ) + if return_itc: + raise ValueError(msg.format("return_itc=True", "computing ITC")) + if method == "stockwell": + raise ValueError(msg.format('method="stockwell"', "Stockwell method")) + # `average` and `return_itc` both False, so "phase" and "complex" are OK + _check_option("output", output, ("power", "phase", "complex")) + method_kw["output"] = output + + if method == "stockwell": + method_kw["return_itc"] = return_itc + method_kw.pop("output") + if isinstance(freqs, str): + _check_option("freqs", freqs, "auto") + else: + _validate_type(freqs, "array-like") + _check_option( + "freqs", np.array(freqs).shape, ((2,),), extra=" (wrong shape)." + ) + if average: + out = AverageTFR( + inst=self, + method=method, + freqs=freqs, + tmin=tmin, + tmax=tmax, + picks=picks, + proj=proj, + decim=decim, + n_jobs=n_jobs, + verbose=verbose, + **method_kw, + ) + # tfr_array_stockwell always returns ITC (but sometimes it's None) + if hasattr(out, "_itc"): + if out._itc is not None: + state = out.__getstate__() + state["data"] = out._itc + state["data_type"] = "Inter-trial coherence" + itc = AverageTFR(inst=state) + del out._itc + return out, itc + del out._itc + return out + # now handle average=False + return EpochsTFR( + inst=self, + method=method, + freqs=freqs, + tmin=tmin, + tmax=tmax, + picks=picks, + proj=proj, + decim=decim, + n_jobs=n_jobs, + verbose=verbose, + **method_kw, + ) + @verbose def plot_psd( self, @@ -3303,20 +3438,18 @@ class Epochs(BaseEpochs): %(on_missing_epochs)s %(reject_by_annotation_epochs)s %(metadata_epochs)s + + .. versionadded:: 0.16 %(event_repeated_epochs)s %(verbose)s Attributes ---------- %(info_not_none)s - event_id : dict - Names of conditions corresponding to event_ids. + %(event_id_attr)s ch_names : list of string List of channel names. - selection : array - List of indices of selected events (not dropped or ignored etc.). For - example, if the original event array had 4 events and the second event - has been dropped, this attribute would be np.array([0, 2, 3]). + %(selection_attr)s preload : bool Indicates whether epochs are in memory. drop_log : tuple of tuple @@ -3535,6 +3668,8 @@ class EpochsArray(BaseEpochs): %(proj_epochs)s %(on_missing_epochs)s %(metadata_epochs)s + + .. versionadded:: 0.16 %(selection)s %(drop_log)s diff --git a/mne/evoked.py b/mne/evoked.py index f6f752cadbf..2e36f47f81b 100644 --- a/mne/evoked.py +++ b/mne/evoked.py @@ -48,6 +48,7 @@ from .html_templates import _get_html_template from .parallel import parallel_func from .time_frequency.spectrum import Spectrum, SpectrumMixin, _validate_method +from .time_frequency.tfr import AverageTFR from .utils import ( ExtendedTimeMixin, SizeMixin, @@ -1168,6 +1169,66 @@ def compute_psd( **method_kw, ) + @verbose + def compute_tfr( + self, + method, + freqs, + *, + tmin=None, + tmax=None, + picks=None, + proj=False, + output="power", + decim=1, + n_jobs=None, + verbose=None, + **method_kw, + ): + """Compute a time-frequency representation of evoked data. + + Parameters + ---------- + %(method_tfr)s + %(freqs_tfr)s + %(tmin_tmax_psd)s + %(picks_good_data_noref)s + %(proj_psd)s + %(output_compute_tfr)s + %(decim_tfr)s + %(n_jobs)s + %(verbose)s + %(method_kw_tfr)s + + Returns + ------- + tfr : instance of AverageTFR + The time-frequency-resolved power estimates of the data. + + Notes + ----- + .. versionadded:: 1.7 + + References + ---------- + .. footbibliography:: + """ + _check_option("output", output, ("power", "phase", "complex")) + method_kw["output"] = output + return AverageTFR( + inst=self, + method=method, + freqs=freqs, + tmin=tmin, + tmax=tmax, + picks=picks, + proj=proj, + decim=decim, + n_jobs=n_jobs, + verbose=verbose, + **method_kw, + ) + @verbose def plot_psd( self, diff --git a/mne/html_templates/repr/tfr.html.jinja b/mne/html_templates/repr/tfr.html.jinja new file mode 100644 index 00000000000..f6ab107ab0b --- /dev/null +++ b/mne/html_templates/repr/tfr.html.jinja @@ -0,0 +1,60 @@ + + + + + + {%- for unit in units %} + + {%- if loop.index == 1 %} + + {%- endif %} + + + {%- endfor %} + + + + + {%- if inst_type == "Epochs" %} + + + + + {% endif -%} + {%- if inst_type == "Evoked" %} + + + + + {% endif -%} + + + + + + + + + {% if "taper" in tfr._dims %} + + + + + {% endif %} + + + + + + + + + + + + + + + + +
Data type{{ tfr._data_type }}
Units{{ unit }}
Data source{{ inst_type }}
Number of epochs{{ tfr.shape[0] }}
Number of averaged trials{{ nave }}
Dims{{ tfr._dims | join(", ") }}
Estimation method{{ tfr.method }}
Number of tapers{{ tfr._mt_weights.size }}
Number of channels{{ tfr.ch_names|length }}
Number of timepoints{{ tfr.times|length }}
Number of frequency bins{{ tfr.freqs|length }}
Frequency range{{ '%.2f'|format(tfr.freqs[0]) }} – {{ '%.2f'|format(tfr.freqs[-1]) }} Hz
diff --git a/mne/io/base.py b/mne/io/base.py index ed909e5658f..c7fb5e4ddd0 100644 --- a/mne/io/base.py +++ b/mne/io/base.py @@ -82,6 +82,7 @@ from ..html_templates import _get_html_template from ..parallel import parallel_func from ..time_frequency.spectrum import Spectrum, SpectrumMixin, _validate_method +from ..time_frequency.tfr import RawTFR from ..utils import ( SizeMixin, TimeMixin, @@ -2241,6 +2242,69 @@ def compute_psd( **method_kw, ) + @verbose + def compute_tfr( + self, + method, + freqs, + *, + tmin=None, + tmax=None, + picks=None, + proj=False, + output="power", + reject_by_annotation=True, + decim=1, + n_jobs=None, + verbose=None, + **method_kw, + ): + """Compute a time-frequency representation of sensor data. + + Parameters + ---------- + %(method_tfr)s + %(freqs_tfr)s + %(tmin_tmax_psd)s + %(picks_good_data_noref)s + %(proj_psd)s + %(output_compute_tfr)s + %(reject_by_annotation_tfr)s + %(decim_tfr)s + %(n_jobs)s + %(verbose)s + %(method_kw_tfr)s + + Returns + ------- + tfr : instance of RawTFR + The time-frequency-resolved power estimates of the data. + + Notes + ----- + .. versionadded:: 1.7 + + References + ---------- + .. footbibliography:: + """ + _check_option("output", output, ("power", "phase", "complex")) + method_kw["output"] = output + return RawTFR( + self, + method=method, + freqs=freqs, + tmin=tmin, + tmax=tmax, + picks=picks, + proj=proj, + reject_by_annotation=reject_by_annotation, + decim=decim, + n_jobs=n_jobs, + verbose=verbose, + **method_kw, + ) + @verbose def to_data_frame( self, diff --git a/mne/minimum_norm/tests/test_inverse.py b/mne/minimum_norm/tests/test_inverse.py index a620fdbbf29..e3be18a3fc9 100644 --- a/mne/minimum_norm/tests/test_inverse.py +++ b/mne/minimum_norm/tests/test_inverse.py @@ -55,7 +55,7 @@ from mne.source_estimate import VolSourceEstimate, read_source_estimate from mne.source_space._source_space import _get_src_nn from mne.surface import _normal_orth -from mne.time_frequency import EpochsTFR +from mne.time_frequency import EpochsTFRArray from mne.utils import _record_warnings, catch_logging test_path = testing.data_path(download=False) @@ -1375,11 +1375,11 @@ def test_apply_inverse_tfr(return_generator): times = np.arange(sfreq) / sfreq # make epochs 1s long data = rng.random((n_epochs, len(info.ch_names), freqs.size, times.size)) data = data + 1j * data # make complex to simulate amplitude + phase - epochs_tfr = EpochsTFR(info, data, times=times, freqs=freqs) + epochs_tfr = EpochsTFRArray(info=info, data=data, times=times, freqs=freqs) epochs_tfr.apply_baseline((0, 0.5)) pick_ori = "vector" - with pytest.raises(ValueError, match="Expected 2 inverse operators, " "got 3"): + with pytest.raises(ValueError, match="Expected 2 inverse operators, got 3"): apply_inverse_tfr_epochs(epochs_tfr, [inverse_operator] * 3, lambda2) # test epochs diff --git a/mne/time_frequency/__init__.pyi b/mne/time_frequency/__init__.pyi index 9fc0c271cc4..0faeb7263d8 100644 --- a/mne/time_frequency/__init__.pyi +++ b/mne/time_frequency/__init__.pyi @@ -1,12 +1,16 @@ __all__ = [ "AverageTFR", + "AverageTFRArray", + "BaseTFR", "CrossSpectralDensity", "EpochsSpectrum", "EpochsSpectrumArray", "EpochsTFR", + "EpochsTFRArray", + "RawTFR", + "RawTFRArray", "Spectrum", "SpectrumArray", - "_BaseTFR", "csd_array_fourier", "csd_array_morlet", "csd_array_multitaper", @@ -61,8 +65,12 @@ from .spectrum import ( ) from .tfr import ( AverageTFR, + AverageTFRArray, + BaseTFR, EpochsTFR, - _BaseTFR, + EpochsTFRArray, + RawTFR, + RawTFRArray, fwhm, morlet, read_tfrs, diff --git a/mne/time_frequency/_stockwell.py b/mne/time_frequency/_stockwell.py index d1108f8057b..08acf28b357 100644 --- a/mne/time_frequency/_stockwell.py +++ b/mne/time_frequency/_stockwell.py @@ -12,8 +12,8 @@ from .._fiff.pick import _pick_data_channels, pick_info from ..parallel import parallel_func -from ..utils import _validate_type, fill_doc, logger, verbose -from .tfr import AverageTFR, _get_data +from ..utils import _validate_type, legacy, logger, verbose +from .tfr import AverageTFRArray, _ensure_slice, _get_data def _check_input_st(x_in, n_fft): @@ -81,9 +81,10 @@ def _st(x, start_f, windows): def _st_power_itc(x, start_f, compute_itc, zero_pad, decim, W): """Aux function.""" + decim = _ensure_slice(decim) n_samp = x.shape[-1] - n_out = n_samp - zero_pad - n_out = n_out // decim + bool(n_out % decim) + decim_indices = decim.indices(n_samp - zero_pad) + n_out = len(range(*decim_indices)) psd = np.empty((len(W), n_out)) itc = np.empty_like(psd) if compute_itc else None X = fft(x) @@ -91,10 +92,7 @@ def _st_power_itc(x, start_f, compute_itc, zero_pad, decim, W): for i_f, window in enumerate(W): f = start_f + i_f ST = ifft(XX[:, f : f + n_samp] * window) - if zero_pad > 0: - TFR = ST[:, :-zero_pad:decim] - else: - TFR = ST[:, ::decim] + TFR = ST[:, slice(*decim_indices)] TFR_abs = np.abs(TFR) TFR_abs[TFR_abs == 0] = 1.0 if compute_itc: @@ -105,7 +103,22 @@ def _st_power_itc(x, start_f, compute_itc, zero_pad, decim, W): return psd, itc -@fill_doc +def _compute_freqs_st(fmin, fmax, n_fft, sfreq): + from scipy.fft import fftfreq + + freqs = fftfreq(n_fft, 1.0 / sfreq) + if fmin is None: + fmin = freqs[freqs > 0][0] + if fmax is None: + fmax = freqs.max() + + start_f = np.abs(freqs - fmin).argmin() + stop_f = np.abs(freqs - fmax).argmin() + freqs = freqs[start_f:stop_f] + return start_f, stop_f, freqs + + +@verbose def tfr_array_stockwell( data, sfreq, @@ -116,6 +129,8 @@ def tfr_array_stockwell( decim=1, return_itc=False, n_jobs=None, + *, + verbose=None, ): """Compute power and intertrial coherence using Stockwell (S) transform. @@ -143,11 +158,11 @@ def tfr_array_stockwell( The width of the Gaussian window. If < 1, increased temporal resolution, if > 1, increased frequency resolution. Defaults to 1. (classical S-Transform). - decim : int - The decimation factor on the time axis. To reduce memory usage. + %(decim_tfr)s return_itc : bool Return intertrial coherence (ITC) as well as averaged power. %(n_jobs)s + %(verbose)s Returns ------- @@ -177,26 +192,17 @@ def tfr_array_stockwell( "data must be 3D with shape (n_epochs, n_channels, n_times), " f"got {data.shape}" ) - n_epochs, n_channels = data.shape[:2] - n_out = data.shape[2] // decim + bool(data.shape[-1] % decim) + decim = _ensure_slice(decim) + _, n_channels, n_out = data[..., decim].shape data, n_fft_, zero_pad = _check_input_st(data, n_fft) - - freqs = fftfreq(n_fft_, 1.0 / sfreq) - if fmin is None: - fmin = freqs[freqs > 0][0] - if fmax is None: - fmax = freqs.max() - - start_f = np.abs(freqs - fmin).argmin() - stop_f = np.abs(freqs - fmax).argmin() - freqs = freqs[start_f:stop_f] + start_f, stop_f, freqs = _compute_freqs_st(fmin, fmax, n_fft_, sfreq) W = _precompute_st_windows(data.shape[-1], start_f, stop_f, sfreq, width) n_freq = stop_f - start_f psd = np.empty((n_channels, n_freq, n_out)) itc = np.empty((n_channels, n_freq, n_out)) if return_itc else None - parallel, my_st, n_jobs = parallel_func(_st_power_itc, n_jobs) + parallel, my_st, n_jobs = parallel_func(_st_power_itc, n_jobs, verbose=verbose) tfrs = parallel( my_st(data[:, c, :], start_f, return_itc, zero_pad, decim, W) for c in range(n_channels) @@ -209,6 +215,7 @@ def tfr_array_stockwell( return psd, itc, freqs +@legacy(alt='.compute_tfr(method="stockwell", freqs="auto")') @verbose def tfr_stockwell( inst, @@ -281,6 +288,7 @@ def tfr_stockwell( picks = _pick_data_channels(inst.info) info = pick_info(inst.info, picks) data = data[:, picks, :] + decim = _ensure_slice(decim) power, itc, freqs = tfr_array_stockwell( data, sfreq=info["sfreq"], @@ -292,18 +300,25 @@ def tfr_stockwell( return_itc=return_itc, n_jobs=n_jobs, ) - times = inst.times[::decim].copy() + times = inst.times[decim].copy() nave = len(data) - out = AverageTFR(info, power, times, freqs, nave, method="stockwell-power") + out = AverageTFRArray( + info=info, + data=power, + times=times, + freqs=freqs, + nave=nave, + method="stockwell-power", + ) if return_itc: out = ( out, - AverageTFR( - deepcopy(info), - itc, - times.copy(), - freqs.copy(), - nave, + AverageTFRArray( + info=deepcopy(info), + data=itc, + times=times.copy(), + freqs=freqs.copy(), + nave=nave, method="stockwell-itc", ), ) diff --git a/mne/time_frequency/multitaper.py b/mne/time_frequency/multitaper.py index 00e3c1c1e17..4a9e66c4673 100644 --- a/mne/time_frequency/multitaper.py +++ b/mne/time_frequency/multitaper.py @@ -488,7 +488,7 @@ def tfr_array_multitaper( The epochs. sfreq : float Sampling frequency of the data in Hz. - %(freqs_tfr)s + %(freqs_tfr_array)s %(n_cycles_tfr)s zero_mean : bool If True, make sure the wavelets have a mean of zero. Defaults to True. @@ -506,6 +506,7 @@ def tfr_array_multitaper( * ``'avg_power_itc'`` : average of single trial power and inter-trial coherence across trials. %(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 diff --git a/mne/time_frequency/spectrum.py b/mne/time_frequency/spectrum.py index e46be389695..7300753c584 100644 --- a/mne/time_frequency/spectrum.py +++ b/mne/time_frequency/spectrum.py @@ -1,7 +1,4 @@ """Container classes for spectral data.""" - -# Authors: Dan McCloy -# # License: BSD-3-Clause # Copyright the MNE-Python contributors. @@ -25,6 +22,7 @@ from ..utils import ( GetEpochsMixin, _build_data_frame, + _check_method_kwargs, _check_pandas_index_arguments, _check_pandas_installed, _check_sphere, @@ -46,12 +44,13 @@ check_fname, ) from ..utils.misc import _identity_function, _pl -from ..utils.spectrum import _split_psd_kwargs +from ..utils.spectrum import _get_instance_type_string, _split_psd_kwargs from ..viz.topo import _plot_timeseries, _plot_timeseries_unified, _plot_topo from ..viz.topomap import _make_head_outlines, _prepare_topomap_plot, plot_psds_topomap from ..viz.utils import ( _format_units_psd, _get_plot_ch_type, + _make_combine_callable, _plot_psd, _prepare_sensor_names, plt_show, @@ -314,7 +313,7 @@ def __init__( ) # method self._inst_type = type(inst) - method = _validate_method(method, self._get_instance_type_string()) + method = _validate_method(method, _get_instance_type_string(self)) # don't allow complex output psd_funcs = dict(welch=psd_array_welch, multitaper=psd_array_multitaper) if method_kw.get("output", "") == "complex": @@ -324,16 +323,8 @@ def __init__( ) # triage method and kwargs. partial() doesn't check validity of kwargs, # so we do it manually to save compute time if any are invalid. - invalid_ix = np.isin( - list(method_kw), list(signature(psd_funcs[method]).parameters), invert=True - ) - if invalid_ix.any(): - invalid_kw = np.array(list(method_kw))[invalid_ix].tolist() - s = _pl(invalid_kw) - raise TypeError( - f'Got unexpected keyword argument{s} {", ".join(invalid_kw)} ' - f'for PSD method "{method}".' - ) + psd_funcs = dict(welch=psd_array_welch, multitaper=psd_array_multitaper) + _check_method_kwargs(psd_funcs[method], method_kw, msg=f'PSD method "{method}"') self._psd_func = partial(psd_funcs[method], remove_dc=remove_dc, **method_kw) # apply proj if desired @@ -352,7 +343,7 @@ def __init__( self.info = pick_info(inst.info, sel=self._picks, copy=True) # assign some attributes - self.preload = True # needed for __getitem__, doesn't mean anything + self.preload = True # needed for __getitem__, never False self._method = method # self._dims may also get updated by child classes self._dims = ( @@ -365,6 +356,8 @@ def __init__( self._data_type = ( "Fourier Coefficients" if "taper" in self._dims else "Power Spectrum" ) + # set nave (child constructor overrides this for Evoked input) + self._nave = None def __eq__(self, other): """Test equivalence of two Spectrum instances.""" @@ -372,7 +365,7 @@ def __eq__(self, other): def __getstate__(self): """Prepare object for serialization.""" - inst_type_str = self._get_instance_type_string() + inst_type_str = _get_instance_type_string(self) out = dict( method=self.method, data=self._data, @@ -382,6 +375,7 @@ def __getstate__(self): inst_type_str=inst_type_str, data_type=self._data_type, info=self.info, + nave=self.nave, ) return out @@ -398,6 +392,7 @@ def __setstate__(self, state): self._sfreq = state["sfreq"] self.info = Info(**state["info"]) self._data_type = state["data_type"] + self._nave = state.get("nave") # objs saved before #11282 won't have `nave` self.preload = True # instance type inst_types = dict(Raw=Raw, Epochs=Epochs, Evoked=Evoked, Array=np.ndarray) @@ -405,7 +400,7 @@ def __setstate__(self, state): def __repr__(self): """Build string representation of the Spectrum object.""" - inst_type_str = self._get_instance_type_string() + inst_type_str = _get_instance_type_string(self) # shape & dimension names dims = " × ".join( [f"{dim[0]} {dim[1]}s" for dim in zip(self.shape, self._dims)] @@ -419,7 +414,7 @@ def __repr__(self): @repr_html def _repr_html_(self, caption=None): """Build HTML representation of the Spectrum object.""" - inst_type_str = self._get_instance_type_string() + inst_type_str = _get_instance_type_string(self) units = [f"{ch_type}: {unit}" for ch_type, unit in self.units().items()] t = _get_html_template("repr", "spectrum.html.jinja") t = t.render(spectrum=self, inst_type=inst_type_str, units=units) @@ -466,25 +461,6 @@ def _compute_spectra(self, data, fmin, fmax, n_jobs, method_kw, verbose): del self._psd_func del self._time_mask - def _get_instance_type_string(self): - """Get string representation of the originating instance type.""" - from ..epochs import BaseEpochs - from ..evoked import Evoked, EvokedArray - from ..io import BaseRaw - - parent_classes = self._inst_type.__bases__ - if BaseRaw in parent_classes: - inst_type_str = "Raw" - elif BaseEpochs in parent_classes: - inst_type_str = "Epochs" - elif self._inst_type in (Evoked, EvokedArray): - inst_type_str = "Evoked" - elif self._inst_type is np.ndarray: - inst_type_str = "Array" - else: - raise RuntimeError(f"Unknown instance type {self._inst_type} in Spectrum") - return inst_type_str - @property def _detrend_picks(self): """Provide compatibility with __iter__.""" @@ -494,6 +470,10 @@ def _detrend_picks(self): def ch_names(self): return self.info["ch_names"] + @property + def data(self): + return self._data + @property def freqs(self): return self._freqs @@ -502,6 +482,10 @@ def freqs(self): def method(self): return self._method + @property + def nave(self): + return self._nave + @property def sfreq(self): return self._sfreq @@ -977,7 +961,7 @@ def to_data_frame( # check pandas once here, instead of in each private utils function pd = _check_pandas_installed() # noqa # triage for Epoch-derived or unaggregated spectra - from_epo = self._dims[0] == "epoch" + from_epo = _get_instance_type_string(self) == "Epochs" unagg_welch = "segment" in self._dims unagg_mt = "taper" in self._dims # arg checking @@ -1083,8 +1067,10 @@ class Spectrum(BaseSpectrum): have been computed. %(info_not_none)s method : str - The method used to compute the spectrum (``'welch'`` or - ``'multitaper'``). + The method used to compute the spectrum (``'welch'`` or ``'multitaper'``). + nave : int | None + The number of trials averaged together when generating the spectrum. ``None`` + indicates no averaging is known to have occurred. See Also -------- @@ -1148,11 +1134,13 @@ def __init__( ) else: # Evoked data = self.inst.data[self._picks][:, self._time_mask] + # set nave + self._nave = getattr(inst, "nave", None) # compute the spectra self._compute_spectra(data, fmin, fmax, n_jobs, method_kw, verbose) # check for correct shape and bad values self._check_values() - del self._shape + del self._shape # calculated from self._data henceforth # save memory del self.inst @@ -1185,7 +1173,8 @@ def __getitem__(self, item): requested data values and the corresponding times), accessing :class:`~mne.time_frequency.Spectrum` values via subscript does **not** return the corresponding frequency bin values. If you need - them, use ``spectrum.freqs[freq_indices]``. + them, use ``spectrum.freqs[freq_indices]`` or + ``spectrum.get_data(..., return_freqs=True)``. """ from ..io import BaseRaw @@ -1220,7 +1209,7 @@ class SpectrumArray(Spectrum): data : array, shape (n_channels, n_freqs) The power spectral density for each channel. %(info_not_none)s - %(freqs_tfr)s + %(freqs_tfr_array)s %(verbose)s See Also @@ -1418,9 +1407,10 @@ def average(self, method="mean"): spectrum : instance of Spectrum The aggregated spectrum object. """ - if isinstance(method, str): - method = getattr(np, method) # mean, median, std, etc - method = partial(method, axis=0) + _validate_type(method, ("str", "callable")) + method = _make_combine_callable( + method, axis=0, valid=("mean", "median"), keepdims=False + ) if not callable(method): raise ValueError( '"method" must be a valid string or callable, ' @@ -1435,6 +1425,7 @@ def average(self, method="mean"): ) # serialize the object and update data, dims, and data type state = super().__getstate__() + state["nave"] = state["data"].shape[0] state["data"] = method(state["data"]) state["dims"] = state["dims"][1:] state["data_type"] = f'Averaged {state["data_type"]}' @@ -1464,7 +1455,7 @@ class EpochsSpectrumArray(EpochsSpectrum): data : array, shape (n_epochs, n_channels, n_freqs) The power spectral density for each channel in each epoch. %(info_not_none)s - %(freqs_tfr)s + %(freqs_tfr_array)s %(events_epochs)s %(event_id)s %(verbose)s diff --git a/mne/time_frequency/tests/test_spectrum.py b/mne/time_frequency/tests/test_spectrum.py index 18fbf4da483..752e1d000a1 100644 --- a/mne/time_frequency/tests/test_spectrum.py +++ b/mne/time_frequency/tests/test_spectrum.py @@ -125,11 +125,15 @@ def test_n_welch_windows(raw): ) -def _get_inst(inst, request, evoked): +def _get_inst(inst, request, *, evoked=None, average_tfr=None): # ↓ XXX workaround: # ↓ parametrized fixtures are not accessible via request.getfixturevalue # ↓ https://github.com/pytest-dev/pytest/issues/4666#issuecomment-456593913 - return evoked if inst == "evoked" else request.getfixturevalue(inst) + if inst == "evoked": + return evoked + elif inst == "average_tfr": + return average_tfr + return request.getfixturevalue(inst) @pytest.mark.parametrize("inst", ("raw", "epochs", "evoked")) @@ -137,7 +141,7 @@ def test_spectrum_io(inst, tmp_path, request, evoked): """Test save/load of spectrum objects.""" pytest.importorskip("h5io") fname = tmp_path / f"{inst}-spectrum.h5" - inst = _get_inst(inst, request, evoked) + inst = _get_inst(inst, request, evoked=evoked) orig = inst.compute_psd() orig.save(fname) loaded = read_spectrum(fname) @@ -214,7 +218,7 @@ def test_spectrum_to_data_frame(inst, request, evoked): # setup is_already_psd = inst in ("raw_spectrum", "epochs_spectrum") is_epochs = inst == "epochs_spectrum" - inst = _get_inst(inst, request, evoked) + inst = _get_inst(inst, request, evoked=evoked) extra_dim = () if is_epochs else (1,) extra_cols = ["freq", "condition", "epoch"] if is_epochs else ["freq"] # compute PSD diff --git a/mne/time_frequency/tests/test_tfr.py b/mne/time_frequency/tests/test_tfr.py index 4fc6f147377..5087a8c46a9 100644 --- a/mne/time_frequency/tests/test_tfr.py +++ b/mne/time_frequency/tests/test_tfr.py @@ -8,26 +8,37 @@ import matplotlib.pyplot as plt import numpy as np import pytest -from numpy.testing import assert_allclose, assert_array_equal, assert_equal +from matplotlib.collections import PathCollection +from numpy.testing import ( + assert_allclose, + assert_array_almost_equal, + assert_array_equal, + assert_equal, +) from scipy.signal import morlet2 import mne from mne import ( Epochs, EpochsArray, - Info, - Transform, create_info, pick_types, read_events, ) from mne.epochs import equalize_epoch_counts from mne.io import read_raw_fif -from mne.tests.test_epochs import assert_metadata_equal -from mne.time_frequency import tfr_array_morlet, tfr_array_multitaper -from mne.time_frequency.tfr import ( +from mne.time_frequency import ( AverageTFR, + AverageTFRArray, + EpochsSpectrum, EpochsTFR, + EpochsTFRArray, + RawTFR, + RawTFRArray, + tfr_array_morlet, + tfr_array_multitaper, +) +from mne.time_frequency.tfr import ( _compute_tfr, _make_dpss, combine_tfr, @@ -40,34 +51,41 @@ write_tfrs, ) from mne.utils import catch_logging, grand_average -from mne.viz.utils import _fake_click, _fake_keypress, _fake_scroll +from mne.utils._testing import _get_suptitle +from mne.viz.utils import ( + _channel_type_prettyprint, + _fake_click, + _fake_keypress, + _fake_scroll, +) + +from .test_spectrum import _get_inst data_path = Path(__file__).parents[2] / "io" / "tests" / "data" raw_fname = data_path / "test_raw.fif" event_fname = data_path / "test-eve.fif" raw_ctf_fname = data_path / "test_ctf_raw.fif" +freqs_linspace = np.linspace(20, 40, num=5) +freqs_unsorted_list = [26, 33, 41, 20] +mag_names = [f"MEG 01{n}1" for n in (1, 2, 3)] -def _create_test_epochstfr(): - n_epos = 3 - ch_names = ["EEG 001", "EEG 002", "EEG 003", "EEG 004"] - n_picks = len(ch_names) - ch_types = ["eeg"] * n_picks - n_freqs = 5 - n_times = 6 - data = np.random.rand(n_epos, n_picks, n_freqs, n_times) - times = np.arange(6) - srate = 1000.0 - freqs = np.arange(5) - events = np.zeros((n_epos, 3), dtype=int) - events[:, 0] = np.arange(n_epos) - events[:, 2] = np.arange(5, 5 + n_epos) - event_id = {k: v for v, k in zip(events[:, 2], ["ha", "he", "hu"])} - info = mne.create_info(ch_names, srate, ch_types) - tfr = mne.time_frequency.EpochsTFR( - info, data, times, freqs, events=events, event_id=event_id - ) - return tfr +parametrize_morlet_multitaper = pytest.mark.parametrize( + "method", ("morlet", "multitaper") +) +parametrize_power_phase_complex = pytest.mark.parametrize( + "output", ("power", "phase", "complex") +) +parametrize_inst_and_ch_type = pytest.mark.parametrize( + "inst,ch_type", + ( + pytest.param("raw_tfr", "mag"), + pytest.param("raw_tfr", "grad"), + pytest.param("epochs_tfr", "mag"), # no grad pairs in epochs fixture + pytest.param("average_tfr", "mag"), + pytest.param("average_tfr", "grad"), + ), +) def test_tfr_ctf(): @@ -111,7 +129,7 @@ def test_morlet(sfreq, freq, n_cycles): assert_allclose(fwhm_formula, fwhm_empirical, atol=3 / sfreq) -def test_time_frequency(): +def test_tfr_morlet(): """Test time-frequency transform (PSD and ITC).""" # Set parameters event_id = 1 @@ -148,7 +166,8 @@ def test_time_frequency(): # Now compute evoked evoked = epochs.average() - pytest.raises(ValueError, tfr_morlet, evoked, freqs, 1.0, return_itc=True) + with pytest.raises(ValueError, match="Inter-trial coherence is not supported with"): + tfr_morlet(evoked, freqs, n_cycles=1.0, return_itc=True) power, itc = tfr_morlet( epochs, freqs=freqs, n_cycles=n_cycles, use_fft=True, return_itc=True ) @@ -542,216 +561,193 @@ def test_tfr_multitaper(): tfr_multitaper(epochs, freqs=np.arange(-4, -1), n_cycles=7) -def test_crop(): - """Test TFR cropping.""" - data = np.zeros((3, 4, 5)) - times = np.array([0.1, 0.2, 0.3, 0.4, 0.5]) - freqs = np.array([0.10, 0.20, 0.30, 0.40]) - info = mne.create_info( - ["MEG 001", "MEG 002", "MEG 003"], 1000.0, ["mag", "mag", "mag"] - ) - tfr = AverageTFR( - info, - data=data, - times=times, - freqs=freqs, - nave=20, - comment="test", - method="crazy-tfr", - ) - - tfr.crop(tmin=0.2) - assert_array_equal(tfr.times, [0.2, 0.3, 0.4, 0.5]) - assert tfr.data.ndim == 3 - assert tfr.data.shape[-1] == 4 - - tfr.crop(fmax=0.3) - assert_array_equal(tfr.freqs, [0.1, 0.2, 0.3]) - assert tfr.data.ndim == 3 - assert tfr.data.shape[-2] == 3 - - tfr.crop(tmin=0.3, tmax=0.4, fmin=0.1, fmax=0.2) - assert_array_equal(tfr.times, [0.3, 0.4]) - assert tfr.data.ndim == 3 - assert tfr.data.shape[-1] == 2 - assert_array_equal(tfr.freqs, [0.1, 0.2]) - assert tfr.data.shape[-2] == 2 - - -def test_decim_shift_time(): - """Test TFR decimation and shift_time.""" - data = np.zeros((3, 3, 3, 1000)) - times = np.linspace(0, 1, 1000) - freqs = np.array([0.10, 0.20, 0.30]) - info = mne.create_info( - ["MEG 001", "MEG 002", "MEG 003"], 1000.0, ["mag", "mag", "mag"] - ) - with info._unlock(): - info["lowpass"] = 100 - tfr = EpochsTFR(info, data=data, times=times, freqs=freqs) - tfr_ave = tfr.average() - assert_allclose(tfr.times, tfr_ave.times) - assert not hasattr(tfr_ave, "first") - tfr_ave.decimate(3) - assert not hasattr(tfr_ave, "first") - tfr.decimate(3) - assert tfr.times.size == 1000 // 3 + 1 - assert tfr.data.shape == ((3, 3, 3, 1000 // 3 + 1)) - tfr_ave_2 = tfr.average() - assert not hasattr(tfr_ave_2, "first") - assert_allclose(tfr.times, tfr_ave.times) - assert_allclose(tfr.times, tfr_ave_2.times) - assert_allclose(tfr_ave_2.data, tfr_ave.data) - tfr.shift_time(-0.1, relative=True) - tfr_ave.shift_time(-0.1, relative=True) - tfr_ave_3 = tfr.average() - assert_allclose(tfr_ave_3.times, tfr_ave.times) - assert_allclose(tfr_ave_3.data, tfr_ave.data) - assert_allclose(tfr_ave_2.data, tfr_ave_3.data) # data unchanged - - -def test_io(tmp_path): - """Test TFR IO capacities.""" - pd = pytest.importorskip("pandas") +@pytest.mark.parametrize( + "method,freqs", + ( + pytest.param("morlet", freqs_linspace, id="morlet"), + pytest.param("multitaper", freqs_linspace, id="multitaper"), + pytest.param("stockwell", freqs_linspace[[0, -1]], id="stockwell"), + ), +) +@pytest.mark.parametrize("decim", (4, slice(0, 200), slice(1, 200, 3))) +def test_tfr_decim_and_shift_time(epochs, method, freqs, decim): + """Test TFR decimation; slices must be long-ish to be longer than the wavelets.""" + tfr = epochs.compute_tfr(method, freqs=freqs, decim=decim) + if not isinstance(decim, slice): + decim = slice(None, None, decim) + # check n_times + want = len(range(*decim.indices(len(epochs.times)))) + assert tfr.shape[-1] == want + # Check that decim changes sfreq + assert tfr.sfreq == epochs.info["sfreq"] / (decim.step or 1) + # check after-the-fact decimation. The mixin .decimate method doesn't allow slices + if isinstance(decim, int): + tfr2 = epochs.compute_tfr(method, freqs=freqs, decim=1) + tfr2.decimate(decim) + assert tfr == tfr2 + # test .shift_time() too + shift = -0.137 + data, times, freqs = tfr.get_data(return_times=True, return_freqs=True) + tfr.shift_time(shift, relative=True) + assert_allclose(times + shift, tfr.times, rtol=0, atol=0.5 / tfr.sfreq) + # shift time should only affect times: + assert_array_equal(data, tfr.get_data()) + assert_array_equal(freqs, tfr.freqs) + + +@pytest.mark.parametrize("inst", ("raw_tfr", "epochs_tfr", "average_tfr")) +def test_tfr_io(inst, average_tfr, request, tmp_path): + """Test TFR I/O.""" pytest.importorskip("h5io") + pd = pytest.importorskip("pandas") - fname = tmp_path / "test-tfr.h5" - data = np.zeros((3, 2, 3)) - times = np.array([0.1, 0.2, 0.3]) - freqs = np.array([0.10, 0.20]) - - info = mne.create_info( - ["MEG 001", "MEG 002", "MEG 003"], 1000.0, ["mag", "mag", "mag"] - ) - with info._unlock(check_after=True): - info["meas_date"] = datetime.datetime( - year=2020, month=2, day=5, tzinfo=datetime.timezone.utc - ) - tfr = AverageTFR( - info, - data=data, - times=times, - freqs=freqs, - nave=20, - comment="test", - method="crazy-tfr", - ) - tfr.save(fname) - tfr2 = read_tfrs(fname, condition="test") - assert isinstance(tfr2.info, Info) - assert isinstance(tfr2.info["dev_head_t"], Transform) - - assert_array_equal(tfr.data, tfr2.data) - assert_array_equal(tfr.times, tfr2.times) - assert_array_equal(tfr.freqs, tfr2.freqs) - assert_equal(tfr.comment, tfr2.comment) - assert_equal(tfr.nave, tfr2.nave) - - pytest.raises(OSError, tfr.save, fname) - - tfr.comment = None - # test old meas_date - with info._unlock(): - info["meas_date"] = (1, 2) + tfr = _get_inst(inst, request, average_tfr=average_tfr) + fname = tmp_path / "temp_tfr.hdf5" + # test .save() method + tfr.save(fname, overwrite=True) + assert read_tfrs(fname) == tfr + # test save single TFR with write_tfrs() + write_tfrs(fname, tfr, overwrite=True) + assert read_tfrs(fname) == tfr + # test save multiple TFRs with write_tfrs() + tfr2 = tfr.copy() + tfr2._data = np.zeros_like(tfr._data) + write_tfrs(fname, [tfr, tfr2], overwrite=True) + tfr_list = read_tfrs(fname) + assert tfr_list[0] == tfr + assert tfr_list[1] == tfr2 + # test condition-related errors + if isinstance(tfr, AverageTFR): + # auto-generated keys: first TFR has comment, so `0` not assigned + tfr2.comment = None + write_tfrs(fname, [tfr, tfr2], overwrite=True) + with pytest.raises(ValueError, match='Cannot find condition "0" in this'): + read_tfrs(fname, condition=0) + # second TFR had no comment, so should get auto-comment `1` assigned + read_tfrs(fname, condition=1) + return + else: + with pytest.raises(NotImplementedError, match="condition is only supported"): + read_tfrs(fname, condition="foo") + # the rest we only do for EpochsTFR (no need to parametrize) + if isinstance(tfr, RawTFR): + return + # make sure everything still works if there's metadata + tfr.metadata = pd.DataFrame(dict(foo=range(tfr.shape[0])), index=tfr.selection) + # test old-style meas date + sec_microsec_tuple = (1, 2) + with tfr.info._unlock(): + tfr.info["meas_date"] = sec_microsec_tuple tfr.save(fname, overwrite=True) - assert_equal(read_tfrs(fname, condition=0).comment, tfr.comment) - tfr.comment = "test-A" - tfr2.comment = "test-B" - - fname = tmp_path / "test2-tfr.h5" - write_tfrs(fname, [tfr, tfr2]) - tfr3 = read_tfrs(fname, condition="test-A") - assert_equal(tfr.comment, tfr3.comment) - - assert isinstance(tfr.info, mne.Info) - - tfrs = read_tfrs(fname, condition=None) - assert_equal(len(tfrs), 2) - tfr4 = tfrs[1] - assert_equal(tfr2.comment, tfr4.comment) - - pytest.raises(ValueError, read_tfrs, fname, condition="nonono") - # Test save of EpochsTFR. - n_events = 5 - data = np.zeros((n_events, 3, 2, 3)) - - # create fake metadata - rng = np.random.RandomState(42) - rt = np.round(rng.uniform(size=(n_events,)), 3) - trialtypes = np.array(["face", "place"]) - trial = trialtypes[(rng.uniform(size=(n_events,)) > 0.5).astype(int)] - meta = pd.DataFrame(dict(RT=rt, Trial=trial)) - # fake events and event_id - events = np.zeros([n_events, 3]) - events[:, 0] = np.arange(n_events) - events[:, 2] = np.ones(n_events) - event_id = {"a/b": 1} - # fake selection - n_dropped_epochs = 3 - selection = np.arange(n_events + n_dropped_epochs)[n_dropped_epochs:] - drop_log = tuple( - [("IGNORED",) for i in range(n_dropped_epochs)] + [() for i in range(n_events)] + tfr_loaded = read_tfrs(fname) + want = datetime.datetime( + year=1970, + month=1, + day=1, + hour=0, + minute=0, + second=sec_microsec_tuple[0], + microsecond=sec_microsec_tuple[1], + tzinfo=datetime.timezone.utc, ) - - tfr = EpochsTFR( - info, - data=data, - times=times, - freqs=freqs, - comment="test", - method="crazy-tfr", - events=events, - event_id=event_id, - selection=selection, - drop_log=drop_log, - metadata=meta, + assert tfr_loaded.info["meas_date"] == want + with tfr.info._unlock(): + tfr.info["meas_date"] = want + assert tfr_loaded == tfr + # test overwrite + with pytest.raises(OSError, match="Destination file exists."): + tfr.save(fname, overwrite=False) + + +def test_raw_tfr_init(raw): + """Test the RawTFR and RawTFRArray constructors.""" + one = RawTFR(inst=raw, method="morlet", freqs=freqs_linspace) + two = RawTFRArray(one.info, one.data, one.times, one.freqs, method="morlet") + # some attributes we know won't match: + for attr in ("_data_type", "_inst_type"): + assert getattr(one, attr) != getattr(two, attr) + delattr(one, attr) + delattr(two, attr) + assert one == two + # test RawTFR.__getitem__ + data = one[:5] + assert data.shape == (5,) + one.shape[1:] + # test missing method/freqs + with pytest.raises(ValueError, match="RawTFR got unsupported parameter value"): + RawTFR(inst=raw) + + +def test_average_tfr_init(full_evoked): + """Test the AverageTFR and AverageTFRArray constructors.""" + one = AverageTFR(inst=full_evoked, method="morlet", freqs=freqs_linspace) + two = AverageTFRArray( + one.info, + one.data, + one.times, + one.freqs, + method="morlet", + comment=one.comment, + nave=one.nave, ) - fname_save = fname - tfr.save(fname_save, True) - fname_write = tmp_path / "test3-tfr.h5" - write_tfrs(fname_write, tfr, overwrite=True) - for fname in [fname_save, fname_write]: - read_tfr = read_tfrs(fname)[0] - assert_array_equal(tfr.data, read_tfr.data) - assert_metadata_equal(tfr.metadata, read_tfr.metadata) - assert_array_equal(tfr.events, read_tfr.events) - assert tfr.event_id == read_tfr.event_id - assert_array_equal(tfr.selection, read_tfr.selection) - assert tfr.drop_log == read_tfr.drop_log - with pytest.raises(NotImplementedError, match="condition not supported"): - tfr = read_tfrs(fname, condition="a") - - -def test_init_EpochsTFR(): + # some attributes we know won't match, otherwise should be identical + assert one._data_type != two._data_type + one._data_type = two._data_type + assert one == two + # test missing method, bad freqs + with pytest.raises(ValueError, match="AverageTFR got unsupported parameter value"): + AverageTFR(inst=full_evoked) + with pytest.raises(ValueError, match='must be a length-2 iterable or "auto"'): + AverageTFR(inst=full_evoked, method="stockwell", freqs=freqs_linspace) + + +def test_epochstfr_init_errors(epochs_tfr): """Test __init__ for EpochsTFR.""" - # Create fake data: - data = np.zeros((3, 3, 3, 3)) - times = np.array([0.1, 0.2, 0.3]) - freqs = np.array([0.10, 0.20, 0.30]) - info = mne.create_info( - ["MEG 001", "MEG 002", "MEG 003"], 1000.0, ["mag", "mag", "mag"] - ) - data_x = data[:, :, :, 0] - with pytest.raises(ValueError, match="data should be 4d. Got 3"): - tfr = EpochsTFR(info, data=data_x, times=times, freqs=freqs) - data_x = data[:, :-1, :, :] - with pytest.raises(ValueError, match="channels and data size don't"): - tfr = EpochsTFR(info, data=data_x, times=times, freqs=freqs) - times_x = times[:-1] - with pytest.raises(ValueError, match="times and data size don't match"): - tfr = EpochsTFR(info, data=data, times=times_x, freqs=freqs) - freqs_x = freqs[:-1] - with pytest.raises(ValueError, match="frequencies and data size don't"): - tfr = EpochsTFR(info, data=data, times=times_x, freqs=freqs_x) - del tfr - - -def test_equalize_epochs_tfr_counts(): + state = epochs_tfr.__getstate__() + with pytest.raises(ValueError, match="EpochsTFR data should be 4D, got 3"): + EpochsTFR(inst=state | dict(data=epochs_tfr.data[..., 0])) + with pytest.raises(ValueError, match="Channel axis of data .* doesn't match info"): + EpochsTFR(inst=state | dict(data=epochs_tfr.data[:, :-1])) + with pytest.raises(ValueError, match="Time axis of data.*doesn't match times attr"): + EpochsTFR(inst=state | dict(times=epochs_tfr.times[:-1])) + with pytest.raises(ValueError, match="Frequency axis of.*doesn't match freqs attr"): + 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", + ( + ("morlet", None, "EpochsTFR got unsupported parameter value freqs=None."), + (None, freqs_linspace, "got unsupported parameter value method=None."), + (None, None, "got unsupported parameter values method=None and freqs=None."), + ), +) +def test_compute_tfr_init_errors(epochs, method, freqs, match): + """Test that method and freqs are always passed (if not using __setstate__).""" + with pytest.raises(ValueError, match=match): + epochs.compute_tfr(method=method, freqs=freqs) + + +def test_equalize_epochs_tfr_counts(epochs_tfr): """Test equalize_epoch_counts for EpochsTFR.""" - tfr = _create_test_epochstfr() - tfr2 = tfr.copy() + # make the fixture have 3 epochs instead of 1 + epochs_tfr._data = np.vstack((epochs_tfr._data, epochs_tfr._data, epochs_tfr._data)) + tfr2 = epochs_tfr.copy() tfr2 = tfr2[:-1] - equalize_epoch_counts([tfr, tfr2]) + equalize_epoch_counts([epochs_tfr, tfr2]) + assert epochs_tfr.shape == tfr2.shape def test_dB_computation(): @@ -765,9 +761,9 @@ def test_dB_computation(): ["MEG 001", "MEG 002", "MEG 003"], 1000.0, ["mag", "mag", "mag"] ) kwargs = dict(times=times, freqs=freqs, nave=20, comment="test", method="crazy-tfr") - tfr = AverageTFR(info, data=data, **kwargs) - complex_tfr = AverageTFR(info, data=complex_data, **kwargs) - plot_kwargs = dict(dB=True, combine="mean", vmin=0, vmax=7) + tfr = AverageTFRArray(info=info, data=data, **kwargs) + complex_tfr = AverageTFRArray(info=info, data=complex_data, **kwargs) + plot_kwargs = dict(dB=True, combine="mean", vlim=(0, 7)) fig1 = tfr.plot(**plot_kwargs)[0] fig2 = complex_tfr.plot(**plot_kwargs)[0] # since we're fixing vmin/vmax, equal colors should mean ~equal input data @@ -785,8 +781,8 @@ def test_plot(): info = mne.create_info( ["MEG 001", "MEG 002", "MEG 003"], 1000.0, ["mag", "mag", "mag"] ) - tfr = AverageTFR( - info, + tfr = AverageTFRArray( + info=info, data=data, times=times, freqs=freqs, @@ -795,88 +791,6 @@ def test_plot(): method="crazy-tfr", ) - # test title=auto, combine=None, and correct length of figure list - picks = [1, 2] - figs = tfr.plot( - picks, title="auto", colorbar=False, mask=np.ones(tfr.data.shape[1:], bool) - ) - assert len(figs) == len(picks) - assert "MEG" in figs[0].texts[0].get_text() - plt.close("all") - - # test combine and title keyword - figs = tfr.plot( - picks, - title="title", - colorbar=False, - combine="rms", - mask=np.ones(tfr.data.shape[1:], bool), - ) - assert len(figs) == 1 - assert figs[0].texts[0].get_text() == "title" - figs = tfr.plot( - picks, - title="auto", - colorbar=False, - combine="mean", - mask=np.ones(tfr.data.shape[1:], bool), - ) - assert len(figs) == 1 - assert figs[0].texts[0].get_text() == "Mean of 2 sensors" - figs = tfr.plot( - picks, - title="auto", - colorbar=False, - combine=lambda x: x.mean(axis=0), - mask=np.ones(tfr.data.shape[1:], bool), - ) - assert len(figs) == 1 - - with pytest.raises(ValueError, match="Invalid value for the 'combine'"): - tfr.plot( - picks, - colorbar=False, - combine="something", - mask=np.ones(tfr.data.shape[1:], bool), - ) - with pytest.raises(RuntimeError, match="must operate on a single"): - tfr.plot(picks, combine=lambda x, y: x.mean(axis=0)) - with pytest.raises(RuntimeError, match=re.escape("of shape (n_freqs, n_times).")): - tfr.plot(picks, combine=lambda x: x.mean(axis=0, keepdims=True)) - with pytest.raises( - RuntimeError, - match=re.escape("return a numpy array of shape (n_freqs, n_times)."), - ): - tfr.plot(picks, combine=lambda x: 101) - - plt.close("all") - - # test axes argument - first with list of axes - ax = plt.subplot2grid((2, 2), (0, 0)) - ax2 = plt.subplot2grid((2, 2), (0, 1)) - ax3 = plt.subplot2grid((2, 2), (1, 0)) - figs = tfr.plot(picks=[0, 1, 2], axes=[ax, ax2, ax3]) - assert len(figs) == len([ax, ax2, ax3]) - # and as a single axes - figs = tfr.plot(picks=[0], axes=ax) - assert len(figs) == 1 - plt.close("all") - # and invalid inputs - with pytest.raises(ValueError, match="axes must be None"): - tfr.plot(picks, colorbar=False, axes={}, mask=np.ones(tfr.data.shape[1:], bool)) - - # different number of axes and picks should throw a RuntimeError - with pytest.raises(RuntimeError, match="There must be an axes"): - tfr.plot( - picks=[0], - colorbar=False, - axes=[ax, ax2], - mask=np.ones(tfr.data.shape[1:], bool), - ) - - tfr.plot_topo(picks=[1, 2]) - plt.close("all") - # interactive mode on by default fig = tfr.plot(picks=[1], cmap="RdBu_r")[0] _fake_keypress(fig, "up") @@ -907,65 +821,76 @@ def test_plot(): plt.close("all") -def test_plot_joint(): - """Test TFR joint plotting.""" - raw = read_raw_fif(raw_fname) - times = np.linspace(-0.1, 0.1, 200) - n_freqs = 3 - nave = 1 - rng = np.random.RandomState(42) - data = rng.randn(len(raw.ch_names), n_freqs, len(times)) - tfr = AverageTFR(raw.info, data, times, np.arange(n_freqs), nave) - - topomap_args = {"res": 8, "contours": 0, "sensors": False} - - for combine in ("mean", "rms", lambda x: x.mean(axis=0)): - with catch_logging() as log: - tfr.plot_joint( - title="auto", - colorbar=True, - combine=combine, - topomap_args=topomap_args, - verbose="debug", - ) - plt.close("all") - log = log.getvalue() - assert "Plotting topomap for grad data" in log - - # check various timefreqs - for timefreqs in ( - { - (tfr.times[0], tfr.freqs[1]): (0.1, 0.5), - (tfr.times[-1], tfr.freqs[-1]): (0.2, 0.6), - }, - [(tfr.times[1], tfr.freqs[1])], - ): - tfr.plot_joint(timefreqs=timefreqs, topomap_args=topomap_args) - plt.close("all") - - # test bad timefreqs - timefreqs = ( - [(-100, 1)], - tfr.times[1], - [1], - [(tfr.times[1], tfr.freqs[1], tfr.freqs[1])], +@pytest.mark.parametrize( + "timefreqs,title,combine", + ( + pytest.param( + {(0.33, 23): (0, 0), (0.25, 30): (0.1, 2)}, + "0.25 ± 0.05 s,\n30.0 ± 1.0 Hz", + "mean", + id="dict,mean", + ), + pytest.param([(0.25, 30)], "0.25 s,\n30.0 Hz", "rms", id="list,rms"), + pytest.param(None, None, lambda x: x.mean(axis=0), id="none,lambda"), + ), +) +@parametrize_inst_and_ch_type +def test_tfr_plot_joint( + inst, ch_type, combine, timefreqs, title, full_average_tfr, request +): + """Test {Raw,Epochs,Average}TFR.plot_joint().""" + tfr = _get_inst(inst, request, average_tfr=full_average_tfr) + with catch_logging() as log: + fig = tfr.plot_joint( + picks=ch_type, + timefreqs=timefreqs, + combine=combine, + topomap_args=dict(res=8, contours=0, sensors=False), # for speed + verbose="debug", + ) + assert f"Plotting topomap for {ch_type} data" in log.getvalue() + # check for correct number of axes + n_topomaps = 1 if timefreqs is None else len(timefreqs) + assert len(fig.axes) == n_topomaps + 2 # n_topomaps + 1 image + 1 colorbar + # title varies by `ch_type` when `timefreqs=None`, so we don't test that here + if title is not None: + assert fig.axes[0].get_title() == title + # test interactivity + ax = [ax for ax in fig.axes if ax.get_xlabel() == "Time (s)"][0] + kw = dict(fig=fig, ax=ax, xform="ax") + _fake_click(**kw, kind="press", point=(0.4, 0.4)) + _fake_click(**kw, kind="motion", point=(0.5, 0.5)) + _fake_click(**kw, kind="release", point=(0.6, 0.6)) + # make sure we actually got a pop-up figure, and it has a plausible title + fignums = plt.get_fignums() + assert len(fignums) == 2 + popup_fig = plt.figure(fignums[-1]) + assert re.match( + r"-?\d{1,2}\.\d{3} - -?\d{1,2}\.\d{3} s,\n\d{1,2}\.\d{2} - \d{1,2}\.\d{2} Hz", + _get_suptitle(popup_fig), ) - for these_timefreqs in timefreqs: - pytest.raises(ValueError, tfr.plot_joint, these_timefreqs) - # test that the object is not internally modified - tfr_orig = tfr.copy() - tfr.plot_joint( - baseline=(0, None), exclude=[tfr.ch_names[0]], topomap_args=topomap_args - ) - plt.close("all") - assert_array_equal(tfr.data, tfr_orig.data) - assert set(tfr.ch_names) == set(tfr_orig.ch_names) - assert set(tfr.times) == set(tfr_orig.times) - # test tfr with picked channels - tfr.pick(tfr.ch_names[:-1]) - tfr.plot_joint(title="auto", colorbar=True, topomap_args=topomap_args) +@pytest.mark.parametrize( + "match,timefreqs,topomap_args", + ( + (r"Requested time point \(-88.000 s\) exceeds the range of", [(-88, 1)], None), + (r"Requested frequency \(99.0 Hz\) exceeds the range of", [(0.0, 99)], None), + ("list of tuple pairs, or a dict of such tuple pairs, not 0", [0.0], None), + ("does not match the channel type present in", None, dict(ch_type="eeg")), + ), +) +def test_tfr_plot_joint_errors(full_average_tfr, match, timefreqs, topomap_args): + """Test AverageTFR.plot_joint() error messages.""" + with pytest.raises(ValueError, match=match): + full_average_tfr.plot_joint(timefreqs=timefreqs, topomap_args=topomap_args) + + +def test_tfr_plot_joint_doesnt_modify(full_average_tfr): + """Test that the object is unchanged after plot_joint().""" + tfr = full_average_tfr.copy() + full_average_tfr.plot_joint() + assert tfr == full_average_tfr def test_add_channels(): @@ -978,8 +903,8 @@ def test_add_channels(): 1000.0, ["mag", "mag", "mag", "eeg", "eeg", "stim"], ) - tfr = AverageTFR( - info, + tfr = AverageTFRArray( + info=info, data=data, times=times, freqs=freqs, @@ -1199,13 +1124,12 @@ def test_averaging_epochsTFR(): avgpower = power.average(method=method) assert_array_equal(func(power.data, axis=0), avgpower.data) with pytest.raises( - RuntimeError, match="You passed a function that " "resulted in data" + RuntimeError, match=r"EpochsTFR.average\(\) got .* shape \(\), but it should be" ): power.average(method=np.mean) -@pytest.mark.parametrize("copy", [True, False]) -def test_averaging_freqsandtimes_epochsTFR(copy): +def test_averaging_freqsandtimes_epochsTFR(): """Test that EpochsTFR averaging freqs methods work.""" # Setup for reading the raw data event_id = 1 @@ -1240,138 +1164,60 @@ def test_averaging_freqsandtimes_epochsTFR(copy): return_itc=False, ) - # Test average methods for freqs and times - for idx, (func, method) in enumerate( - zip( - [np.mean, np.median, np.mean, np.mean], - [ - "mean", - "median", - lambda x: np.mean(x, axis=2), - lambda x: np.mean(x, axis=3), - ], - ) + # Test averaging over freqs + kwargs = dict(dim="freqs", copy=True) + for method, func in zip( + ("mean", "median", lambda x: np.mean(x, axis=2)), (np.mean, np.median, np.mean) ): - if idx == 3: - with pytest.raises(RuntimeError, match="You passed a function"): - avgpower = power.copy().average(method=method, dim="freqs", copy=copy) - continue - avgpower = power.copy().average(method=method, dim="freqs", copy=copy) - assert_array_equal(func(power.data, axis=2, keepdims=True), avgpower.data) - assert avgpower.freqs == np.mean(power.freqs) + avgpower = power.average(method=method, **kwargs) + assert_array_equal(avgpower.data, func(power.data, axis=2, keepdims=True)) + assert_array_equal(avgpower.freqs, func(power.freqs, keepdims=True)) assert isinstance(avgpower, EpochsTFR) - - # average over epochs - avgpower = avgpower.average() + avgpower = avgpower.average() # average over epochs assert isinstance(avgpower, AverageTFR) - - # Test average methods for freqs and times - for idx, (func, method) in enumerate( - zip( - [np.mean, np.median, np.mean, np.mean], - [ - "mean", - "median", - lambda x: np.mean(x, axis=3), - lambda x: np.mean(x, axis=2), - ], - ) + with pytest.raises(RuntimeError, match=r"shape \(1, 2, 3\), but it should"): + # collapsing wrong axis (time instead of freq) + avgpower = power.average(method=lambda x: np.mean(x, axis=3), **kwargs) + + # Test averaging over times + kwargs = dict(dim="times", copy=False) + for method, func in zip( + ("mean", "median", lambda x: np.mean(x, axis=3)), (np.mean, np.median, np.mean) ): - if idx == 3: - with pytest.raises(RuntimeError, match="You passed a function"): - avgpower = power.copy().average(method=method, dim="times", copy=copy) - continue - avgpower = power.copy().average(method=method, dim="times", copy=copy) - assert_array_equal(func(power.data, axis=-1, keepdims=True), avgpower.data) - assert avgpower.times == np.mean(power.times) - assert isinstance(avgpower, EpochsTFR) + avgpower = power.average(method=method, **kwargs) + assert_array_equal(avgpower.data, func(power.data, axis=-1, keepdims=False)) + assert isinstance(avgpower, EpochsSpectrum) + with pytest.raises(RuntimeError, match=r"shape \(1, 2, 420\), but it should"): + # collapsing wrong axis (freq instead of time) + avgpower = power.average(method=lambda x: np.mean(x, axis=2), **kwargs) - # average over epochs - avgpower = avgpower.average() - assert isinstance(avgpower, AverageTFR) - -def test_getitem_epochsTFR(): - """Test GetEpochsMixin in the context of EpochsTFR.""" +@pytest.mark.parametrize("n_drop", (0, 2)) +def test_epochstfr_getitem(epochs_full, n_drop): + """Test EpochsTFR.__getitem__().""" pd = pytest.importorskip("pandas") - - # Setup for reading the raw data and select a few trials - raw = read_raw_fif(raw_fname) - events = read_events(event_fname) - # create fake data, test with and without dropping epochs - for n_drop_epochs in [0, 2]: - n_events = 12 - # create fake metadata - rng = np.random.RandomState(42) - rt = rng.uniform(size=(n_events,)) - trialtypes = np.array(["face", "place"]) - trial = trialtypes[(rng.uniform(size=(n_events,)) > 0.5).astype(int)] - meta = pd.DataFrame(dict(RT=rt, Trial=trial)) - event_id = dict(a=1, b=2, c=3, d=4) - epochs = Epochs( - raw, events[:n_events], event_id=event_id, metadata=meta, decim=1 - ) - epochs.drop(np.arange(n_drop_epochs)) - n_events -= n_drop_epochs - - freqs = np.arange(12.0, 17.0, 2.0) # define frequencies of interest - n_cycles = freqs / 2.0 # 0.5 second time windows for all frequencies - - # Choose time x (full) bandwidth product - time_bandwidth = 4.0 - # With 0.5 s time windows, this gives 8 Hz smoothing - kwargs = dict( - freqs=freqs, - n_cycles=n_cycles, - use_fft=True, - time_bandwidth=time_bandwidth, - return_itc=False, - average=False, - n_jobs=None, - ) - power = tfr_multitaper(epochs, **kwargs) - - # Check that power and epochs metadata is the same - assert_metadata_equal(epochs.metadata, power.metadata) - assert_metadata_equal(epochs[::2].metadata, power[::2].metadata) - assert_metadata_equal(epochs["RT < .5"].metadata, power["RT < .5"].metadata) - assert_array_equal(epochs.selection, power.selection) - assert epochs.drop_log == power.drop_log - - # Check that get power is functioning - assert_array_equal(power[3:6].data, power.data[3:6]) - assert_array_equal(power[3:6].events, power.events[3:6]) - assert_array_equal(epochs.selection[3:6], power.selection[3:6]) - - indx_check = power.metadata["Trial"] == "face" - try: - indx_check = indx_check.to_numpy() - except Exception: - pass # older Pandas - indx_check = indx_check.nonzero() - assert_array_equal(power['Trial == "face"'].events, power.events[indx_check]) - assert_array_equal(power['Trial == "face"'].data, power.data[indx_check]) - - # Check that the wrong Key generates a Key Error for Metadata search - with pytest.raises(KeyError): - power['Trialz == "place"'] - - # Test length function - assert len(power) == n_events - assert len(power[3:6]) == 3 - - # Test iteration function - for ind, power_ep in enumerate(power): - assert_array_equal(power_ep, power.data[ind]) - if ind == 5: - break - - # Test that current state is maintained - assert_array_equal(power.next(), power.data[ind + 1]) - - # Check decim affects sfreq - power_decim = tfr_multitaper(epochs, decim=2, **kwargs) - assert power.info["sfreq"] / 2.0 == power_decim.info["sfreq"] + from pandas.testing import assert_frame_equal + + epochs_full.metadata = pd.DataFrame(dict(foo=list("aaaabbb"), bar=np.arange(7))) + epochs_full.drop(np.arange(n_drop)) + tfr = epochs_full.compute_tfr(method="morlet", freqs=freqs_linspace) + # check that various attributes are preserved + assert_frame_equal(tfr.metadata, epochs_full.metadata) + assert epochs_full.drop_log == tfr.drop_log + for attr in ("events", "selection", "times"): + assert_array_equal(getattr(epochs_full, attr), getattr(tfr, attr)) + # test pandas query + foo_a = tfr["foo == 'a'"] + bar_3 = tfr["bar <= 3"] + assert foo_a == bar_3 + assert foo_a.shape[0] == 4 - n_drop + # test integer and slice + subset_ints = tfr[[0, 1, 2]] + subset_slice = tfr[:3] + assert subset_ints == subset_slice + # test iteration + for ix, epo in enumerate(tfr): + assert_array_equal(tfr[ix].data, epo.data.obj[np.newaxis]) def test_to_data_frame(): @@ -1393,8 +1239,13 @@ def test_to_data_frame(): events[:, 2] = np.arange(5, 5 + n_epos) event_id = {k: v for v, k in zip(events[:, 2], ["ha", "he", "hu"])} info = mne.create_info(ch_names, srate, ch_types) - tfr = mne.time_frequency.EpochsTFR( - info, data, times, freqs, events=events, event_id=event_id + tfr = EpochsTFRArray( + info=info, + data=data, + times=times, + freqs=freqs, + events=events, + event_id=event_id, ) # test index checking with pytest.raises(ValueError, match="options. Valid index options are"): @@ -1477,8 +1328,13 @@ def test_to_data_frame_index(index): events[:, 2] = np.arange(5, 8) event_id = {k: v for v, k in zip(events[:, 2], ["ha", "he", "hu"])} info = mne.create_info(ch_names, 1000.0, ch_types) - tfr = mne.time_frequency.EpochsTFR( - info, data, times, freqs, events=events, event_id=event_id + tfr = EpochsTFRArray( + info=info, + data=data, + times=times, + freqs=freqs, + events=events, + event_id=event_id, ) df = tfr.to_data_frame(picks=[0, 2, 3], index=index) # test index order/hierarchy preservation @@ -1502,17 +1358,333 @@ def test_to_data_frame_time_format(time_format): n_freqs = 5 n_times = 6 data = np.random.rand(n_epos, n_picks, n_freqs, n_times) - times = np.arange(6) + times = np.arange(6, dtype=float) freqs = np.arange(5) events = np.zeros((n_epos, 3), dtype=int) events[:, 0] = np.arange(n_epos) events[:, 2] = np.arange(5, 8) event_id = {k: v for v, k in zip(events[:, 2], ["ha", "he", "hu"])} info = mne.create_info(ch_names, 1000.0, ch_types) - tfr = mne.time_frequency.EpochsTFR( - info, data, times, freqs, events=events, event_id=event_id + tfr = EpochsTFRArray( + info=info, + data=data, + times=times, + freqs=freqs, + events=events, + event_id=event_id, ) # test time_format df = tfr.to_data_frame(time_format=time_format) dtypes = {None: np.float64, "ms": np.int64, "timedelta": pd.Timedelta} assert isinstance(df["time"].iloc[0], dtypes[time_format]) + + +@parametrize_morlet_multitaper +@parametrize_power_phase_complex +@pytest.mark.parametrize("picks", ("mag", mag_names, [2, 5, 8])) # all 3 equivalent +def test_raw_compute_tfr(raw, method, output, picks): + """Test Raw.compute_tfr() and picks handling.""" + full_tfr = raw.compute_tfr(method, output=output, freqs=freqs_linspace) + pick_tfr = raw.compute_tfr(method, output=output, freqs=freqs_linspace, picks=picks) + assert isinstance(pick_tfr, RawTFR), type(pick_tfr) + # ↓↓↓ can't use [2,5,8] because ch0 is IAS, so indices change between raw and TFR + want = full_tfr.get_data(picks=mag_names) + got = pick_tfr.get_data() + assert_array_equal(want, got) + + +@parametrize_morlet_multitaper +@parametrize_power_phase_complex +@pytest.mark.parametrize("freqs", (freqs_linspace, freqs_unsorted_list)) +def test_evoked_compute_tfr(full_evoked, method, output, freqs): + """Test Evoked.compute_tfr(), with a few different ways of specifying freqs.""" + tfr = full_evoked.compute_tfr(method, freqs, output=output) + assert isinstance(tfr, AverageTFR), type(tfr) + assert tfr.nave == full_evoked.nave + assert tfr.comment == full_evoked.comment + + +@parametrize_morlet_multitaper +@pytest.mark.parametrize( + "average,return_itc,dim,want_class", + ( + pytest.param(True, False, None, None, id="average,no_itc"), + pytest.param(True, True, None, None, id="average,itc"), + pytest.param(False, False, "freqs", EpochsTFR, id="no_average,agg_freqs"), + pytest.param(False, False, "epochs", AverageTFR, id="no_average,agg_epochs"), + pytest.param(False, False, "times", EpochsSpectrum, id="no_average,agg_times"), + ), +) +def test_epochs_compute_tfr_average_itc( + epochs, method, average, return_itc, dim, want_class +): + """Test Epochs.compute_tfr(), averaging (at call time and afterward), and ITC.""" + tfr = epochs.compute_tfr( + method, freqs=freqs_linspace, average=average, return_itc=return_itc + ) + if return_itc: + tfr, itc = tfr + assert isinstance(itc, AverageTFR), type(itc) + # for single-epoch input, ITC should be (nearly) unity + assert_array_almost_equal(itc.get_data(), 1.0, decimal=15) + # if not averaging initially, make sure the post-facto .average() works too + if average: + assert isinstance(tfr, AverageTFR), type(tfr) + assert tfr.nave == 1 + assert tfr.comment == "1" + else: + assert isinstance(tfr, EpochsTFR), type(tfr) + avg = tfr.average(dim=dim) + assert isinstance(avg, want_class), type(avg) + if dim == "epochs": + assert avg.nave == len(epochs) + assert avg.comment.startswith(f"mean of {len(epochs)} EpochsTFR") + + +def test_epochs_vs_evoked_compute_tfr(epochs): + """Compare result of averaging before or after the TFR computation. + + This is mostly a test of object structure / attribute preservation. In normal cases, + the data should not match: + - epochs.compute_tfr().average() is average of squared magnitudes + - epochs.average().compute_tfr() is squared magnitude of average + But the `epochs` fixture has only one epoch, so here data should be identical too. + + The three things that will always end up different are `._comment`, `._inst_type`, + and `._data_type`, so we ignore those here. + """ + avg_first = epochs.average().compute_tfr(method="morlet", freqs=freqs_linspace) + avg_second = epochs.compute_tfr(method="morlet", freqs=freqs_linspace).average() + for attr in ("_comment", "_inst_type", "_data_type"): + assert getattr(avg_first, attr) != getattr(avg_second, attr) + delattr(avg_first, attr) + delattr(avg_second, attr) + assert avg_first == avg_second + + +morlet_kw = dict(n_cycles=freqs_linspace / 4, use_fft=False, zero_mean=True) +mt_kw = morlet_kw | dict(zero_mean=False, time_bandwidth=6) +stockwell_kw = dict(n_fft=1024, width=2) + + +@pytest.mark.parametrize( + "method,freqs,method_kw", + ( + pytest.param("morlet", freqs_linspace, morlet_kw, id="morlet-nondefaults"), + pytest.param("multitaper", freqs_linspace, mt_kw, id="multitaper-nondefaults"), + pytest.param("stockwell", "auto", stockwell_kw, id="stockwell-nondefaults"), + ), +) +def test_epochs_compute_tfr_method_kw(epochs, method, freqs, method_kw): + """Test Epochs.compute_tfr(**method_kw).""" + tfr = epochs.compute_tfr(method, freqs=freqs, average=True, **method_kw) + assert isinstance(tfr, AverageTFR), type(tfr) + + +@pytest.mark.parametrize( + "freqs", + (pytest.param("auto", id="freqauto"), pytest.param([20, 41], id="fminfmax")), +) +@pytest.mark.parametrize("return_itc", (False, True)) +def test_epochs_compute_tfr_stockwell(epochs, freqs, return_itc): + """Test Epochs.compute_tfr(method="stockwell").""" + tfr = epochs.compute_tfr("stockwell", freqs, return_itc=return_itc) + if return_itc: + tfr, itc = tfr + assert isinstance(itc, AverageTFR) + # for single-epoch input, ITC should be (nearly) unity + assert_array_almost_equal(itc.get_data(), 1.0, decimal=15) + assert isinstance(tfr, AverageTFR) + assert tfr.comment == "1" + + +@pytest.mark.parametrize("copy", (False, True)) +def test_epochstfr_iter_evoked(epochs_tfr, copy): + """Test EpochsTFR.iter_evoked().""" + avgs = list(epochs_tfr.iter_evoked(copy=copy)) + assert len(avgs) == len(epochs_tfr) + assert all(avg.nave == 1 for avg in avgs) + assert avgs[0].comment == str(epochs_tfr.events[0, -1]) + + +def test_tfr_proj(epochs): + """Test `compute_tfr(proj=True)`.""" + epochs.compute_tfr(method="morlet", freqs=freqs_linspace, proj=True) + + +def test_tfr_copy(average_tfr): + """Test BaseTFR.copy() method.""" + tfr_copy = average_tfr.copy() + # check that info is independent + tfr_copy.info["bads"] = tfr_copy.ch_names + assert average_tfr.info["bads"] == [] + # check that data is independent + tfr_copy.data = np.inf + assert np.isfinite(average_tfr.get_data()).all() + + +@pytest.mark.parametrize( + "mode", ("mean", "ratio", "logratio", "percent", "zscore", "zlogratio") +) +def test_tfr_apply_baseline(average_tfr, mode): + """Test TFR baselining.""" + average_tfr.apply_baseline((-0.1, -0.05), mode=mode) + + +def test_tfr_arithmetic(epochs): + """Test TFR arithmetic operations.""" + tfr, itc = epochs.compute_tfr( + "morlet", freqs=freqs_linspace, average=True, return_itc=True + ) + itc_copy = itc.copy() + # addition / subtraction of objects + double = tfr + tfr + double -= tfr + assert tfr == double + itc_copy += tfr + assert itc == itc_copy - tfr + # multiplication / division with scalars + bigger_itc = itc * 23 + assert_array_almost_equal(itc.data, (bigger_itc / 23).data, decimal=15) + # multiplication / division with arrays + arr = np.full_like(itc.data, 23) + assert_array_equal(bigger_itc.data, (itc * arr).data) + # in-place multiplication/division + bigger_itc *= 2 + bigger_itc /= 46 + assert_array_almost_equal(itc.data, bigger_itc.data, decimal=15) + # check errors + with pytest.raises(RuntimeError, match="types do not match"): + tfr + epochs + with pytest.raises(RuntimeError, match="times do not match"): + tfr + tfr.copy().crop(tmax=0.2) + with pytest.raises(RuntimeError, match="freqs do not match"): + tfr + tfr.copy().crop(fmax=33) + + +def test_tfr_repr_html(epochs_tfr): + """Test TFR._repr_html_().""" + result = epochs_tfr._repr_html_(caption="Foo") + for heading in ("Data type", "Data source", "Estimation method"): + assert f"{heading}" in result + for data in ("Power Estimates", "Epochs", "morlet"): + assert f"{data}" in result + + +@pytest.mark.parametrize( + "picks,combine", + ( + pytest.param("mag", "mean", id="mean_of_mags"), + pytest.param("grad", "rms", id="rms_of_grads"), + pytest.param([1], "mean", id="single_channel"), + pytest.param([1, 2], None, id="two_separate_channels"), + ), +) +def test_tfr_plot_combine(epochs_tfr, picks, combine): + """Test TFR.plot() picks, combine, and title="auto". + + No need to parametrize over {Raw,Epochs,Evoked}TFR, the code path is shared. + """ + fig = epochs_tfr.plot(picks=picks, combine=combine, title="auto") + assert len(fig) == 1 if isinstance(picks, str) else len(picks) + # test `title="auto"` + for ix, _fig in enumerate(fig): + if isinstance(picks, str): + ch_type = _channel_type_prettyprint[picks] + want = rf"{'RMS' if combine == 'rms' else 'Mean'} of \d{{1,3}} {ch_type}s" + else: + want = epochs_tfr.ch_names[picks[ix]] + assert re.search(want, _get_suptitle(_fig)) + + +def test_tfr_plot_extras(epochs_tfr): + """Test other options of TFR.plot().""" + # test mask and custom title + picks = [1] + mask = np.ones(epochs_tfr.data.shape[2:], bool) + fig = epochs_tfr.plot(picks=picks, mask=mask, title="Foo") + assert _get_suptitle(fig[0]) == "Foo" + mask = np.ones(epochs_tfr.data.shape[1:], bool) + with pytest.raises(ValueError, match="mask must have the same shape as the data"): + epochs_tfr.plot(picks=picks, mask=mask) + # test combine-related errors + with pytest.raises(ValueError, match='"combine" must be None, a callable, or one'): + epochs_tfr.plot(picks=picks, combine="foo") + with pytest.raises(RuntimeError, match="Wrong type yielded by callable"): + epochs_tfr.plot(picks=picks, combine=lambda x: 777) + with pytest.raises(RuntimeError, match="Wrong shape yielded by callable"): + epochs_tfr.plot(picks=picks, combine=lambda x: np.array([777])) + with pytest.raises(ValueError, match="wrong with the callable passed to 'combine'"): + epochs_tfr.plot(picks=picks, combine=lambda x, y: x.mean(axis=0)) + # test custom Axes + fig, axs = plt.subplots(1, 5) + fig2 = epochs_tfr.plot(picks=[1, 2], combine=lambda x: x.mean(axis=0), axes=axs[0]) + fig3 = epochs_tfr.plot(picks=[1, 2, 3], axes=axs[1:-1]) + fig4 = epochs_tfr.plot(picks=[1], axes=axs[-1:].tolist()) + for _fig in fig2 + fig3 + fig4: + assert fig == _fig + with pytest.raises(ValueError, match="axes must be None"): + epochs_tfr.plot(picks=picks, axes={}) + with pytest.raises(RuntimeError, match="must be one axes for each picked channel"): + epochs_tfr.plot(picks=[1, 2], axes=axs[-1:]) + # test singleton check by faking having 2 epochs + epochs_tfr._data = np.vstack((epochs_tfr._data, epochs_tfr._data)) + with pytest.raises(NotImplementedError, match=r"Cannot call plot\(\) from"): + epochs_tfr.plot() + + +def test_tfr_plot_interactivity(epochs_tfr): + """Test interactivity of TFR.plot().""" + fig = epochs_tfr.plot(picks="mag", combine="mean")[0] + assert len(plt.get_fignums()) == 1 + # press and release in same spot (should do nothing) + kw = dict(fig=fig, ax=fig.axes[0], xform="ax") + _fake_click(**kw, point=(0.5, 0.5), kind="press") + _fake_click(**kw, point=(0.5, 0.5), kind="motion") + _fake_click(**kw, point=(0.5, 0.5), kind="release") + assert len(plt.get_fignums()) == 1 + # click and drag (should create popup topomap) + _fake_click(**kw, point=(0.4, 0.4), kind="press") + _fake_click(**kw, point=(0.5, 0.5), kind="motion") + _fake_click(**kw, point=(0.6, 0.6), kind="release") + assert len(plt.get_fignums()) == 2 + + +@parametrize_inst_and_ch_type +def test_tfr_plot_topo(inst, ch_type, average_tfr, request): + """Test {Raw,Epochs,Average}TFR.plot_topo().""" + tfr = _get_inst(inst, request, average_tfr=average_tfr) + fig = tfr.plot_topo(picks=ch_type) + assert fig is not None + + +@parametrize_inst_and_ch_type +def test_tfr_plot_topomap(inst, ch_type, full_average_tfr, request): + """Test {Raw,Epochs,Average}TFR.plot_topomap().""" + tfr = _get_inst(inst, request, average_tfr=full_average_tfr) + fig = tfr.plot_topomap(ch_type=ch_type) + # fake a click-drag-release to select all sensors & generate a pop-up TFR image + ax = fig.axes[0] + pts = [ + coll.get_offsets() + for coll in ax.collections + if isinstance(coll, PathCollection) + ][0] + # sometimes sensors are outside axes; make sure our click starts inside axes + lims = np.vstack((ax.get_xlim(), ax.get_ylim())) + pad = np.diff(lims, axis=1).ravel() / 100 + start = np.clip(pts.min(axis=0) - pad, *(lims.min(axis=1) + pad)) + stop = np.clip(pts.max(axis=0) + pad, *(lims.max(axis=1) - pad)) + kw = dict(fig=fig, ax=ax, xform="data") + _fake_click(**kw, kind="press", point=tuple(start)) + # ↓↓↓ possible bug? using (start+stop)/2 for the motion event causes the motion + # ↓↓↓ event (not release event) coords to propagate → fails to select sensors + _fake_click(**kw, kind="motion", point=tuple(stop)) + _fake_click(**kw, kind="release", point=tuple(stop)) + # make sure we actually got a pop-up figure, and it has a plausible title + fignums = plt.get_fignums() + assert len(fignums) == 2 + popup_fig = plt.figure(fignums[-1]) + assert re.match( + rf"Average over \d{{1,3}} {ch_type} channels\.", popup_fig.axes[0].get_title() + ) diff --git a/mne/time_frequency/tfr.py b/mne/time_frequency/tfr.py index d7df408b564..97df892ad46 100644 --- a/mne/time_frequency/tfr.py +++ b/mne/time_frequency/tfr.py @@ -11,19 +11,17 @@ # License: BSD-3-Clause # Copyright the MNE-Python contributors. +import inspect from copy import deepcopy from functools import partial +import matplotlib.pyplot as plt import numpy as np from scipy.fft import fft, ifft from scipy.signal import argrelmax from .._fiff.meas_info import ContainsMixin, Info -from .._fiff.pick import ( - _picks_to_idx, - channel_type, - pick_info, -) +from .._fiff.pick import _picks_to_idx, pick_info from ..baseline import _check_baseline, rescale from ..channels.channels import UpdateChannelsMixin from ..channels.layout import _find_topomap_coords, _merge_ch_data, _pair_grad_sensors @@ -37,27 +35,35 @@ _build_data_frame, _check_combine, _check_event_id, + _check_fname, + _check_method_kwargs, _check_option, _check_pandas_index_arguments, _check_pandas_installed, _check_time_format, _convert_times, + _ensure_events, _freq_mask, - _gen_events, _import_h5io_funcs, _is_numeric, + _pl, _prepare_read_metadata, _prepare_write_metadata, _time_mask, _validate_type, check_fname, + copy_doc, copy_function_doc_to_method_doc, fill_doc, + legacy, logger, + object_diff, + repr_html, sizeof_fmt, verbose, warn, ) +from ..utils.spectrum import _get_instance_type_string from ..viz.topo import _imshow_tfr, _imshow_tfr_unified, _plot_topo from ..viz.topomap import ( _add_colorbar, @@ -67,6 +73,7 @@ plot_topomap, ) from ..viz.utils import ( + _make_combine_callable, _prepare_joint_axes, _set_title_multiple_electrodes, _setup_cmap, @@ -75,7 +82,8 @@ figure_nobar, plt_show, ) -from .multitaper import dpss_windows +from .multitaper import dpss_windows, tfr_array_multitaper +from .spectrum import EpochsSpectrum @fill_doc @@ -239,7 +247,14 @@ def fwhm(freq, n_cycles): return n_cycles * np.sqrt(2 * np.log(2)) / (np.pi * freq) -def _make_dpss(sfreq, freqs, n_cycles=7.0, time_bandwidth=4.0, zero_mean=False): +def _make_dpss( + sfreq, + freqs, + n_cycles=7.0, + time_bandwidth=4.0, + zero_mean=False, + return_weights=False, +): """Compute DPSS tapers for the given frequency range. Parameters @@ -257,6 +272,8 @@ def _make_dpss(sfreq, freqs, n_cycles=7.0, time_bandwidth=4.0, zero_mean=False): Default is 4.0, giving 3 good tapers. zero_mean : bool | None, , default False Make sure the wavelet has a mean of zero. + return_weights : bool + Whether to return the concentration weights. Returns ------- @@ -304,7 +321,8 @@ def _make_dpss(sfreq, freqs, n_cycles=7.0, time_bandwidth=4.0, zero_mean=False): Wm.append(Wk) Ws.append(Wm) - + if return_weights: + return Ws, conc return Ws @@ -360,7 +378,7 @@ def _cwt_gen(X, Ws, *, fsize=0, mode="same", decim=1, use_fft=True): The time-frequency transform of the signals. """ _check_option("mode", mode, ["same", "valid", "full"]) - decim = _check_decim(decim) + decim = _ensure_slice(decim) X = np.asarray(X) # Precompute wavelets for given frequency range to save time @@ -426,6 +444,7 @@ def _compute_tfr( decim=1, output="complex", n_jobs=None, + *, verbose=None, ): """Compute time-frequency transforms. @@ -490,8 +509,7 @@ def _compute_tfr( ``'phase'`` results in shape of ``out`` being ``(n_epochs, n_chans, n_tapers, n_freqs, n_times)``. If output is ``'avg_power_itc'``, the real values in the ``output`` contain average power' and the imaginary - values contain the inter-trial coherence: - ``out = avg_power + i * ITC``. + values contain the ITC: ``out = avg_power + i * itc``. """ # Check data epoch_data = np.asarray(epoch_data) @@ -514,7 +532,7 @@ def _compute_tfr( output, ) - decim = _check_decim(decim) + decim = _ensure_slice(decim) if (freqs > sfreq / 2.0).any(): raise ValueError( "Cannot compute freq above Nyquist freq of the data " @@ -698,7 +716,7 @@ def _time_frequency_loop(X, Ws, output, use_fft, mode, decim, method=None): dtype = np.complex128 # Init outputs - decim = _check_decim(decim) + decim = _ensure_slice(decim) n_tapers = len(Ws) n_epochs, n_times = X[:, decim].shape n_freqs = len(Ws[0]) @@ -790,7 +808,7 @@ def cwt(X, Ws, use_fft=True, mode="same", decim=1): def _cwt_array(X, Ws, nfft, mode, decim, use_fft): - decim = _check_decim(decim) + decim = _ensure_slice(decim) coefs = _cwt_gen(X, Ws, fsize=nfft, mode=mode, decim=decim, use_fft=use_fft) n_signals, n_times = X[:, decim].shape @@ -802,85 +820,31 @@ def _cwt_array(X, Ws, nfft, mode, decim, use_fft): def _tfr_aux( - method, inst, freqs, decim, return_itc, picks, average, output=None, **tfr_params + method, inst, freqs, decim, return_itc, picks, average, output, **tfr_params ): from ..epochs import BaseEpochs - """Help reduce redundancy between tfr_morlet and tfr_multitaper.""" - decim = _check_decim(decim) - data = _get_data(inst, return_itc) - info = inst.info.copy() # make a copy as sfreq can be altered - - info, data = _prepare_picks(info, data, picks, axis=1) - del picks - - if average: - if output == "complex": - raise ValueError('output must be "power" if average=True') - if return_itc: - output = "avg_power_itc" - else: - output = "avg_power" - else: - output = "power" if output is None else output - if return_itc: - raise ValueError( - "Inter-trial coherence is not supported" " with average=False" - ) - - out = _compute_tfr( - data, - freqs, - info["sfreq"], + kwargs = dict( method=method, - output=output, + freqs=freqs, + picks=picks, decim=decim, + output=output, **tfr_params, ) - times = inst.times[decim].copy() - with info._unlock(): - info["sfreq"] /= decim.step - - if average: - if return_itc: - power, itc = out.real, out.imag - else: - power = out - nave = len(data) - out = AverageTFR(info, power, times, freqs, nave, method="%s-power" % method) - if return_itc: - out = ( - out, - AverageTFR(info, itc, times, freqs, nave, method="%s-itc" % method), - ) - else: - power = out - if isinstance(inst, BaseEpochs): - meta = deepcopy(inst._metadata) - evs = deepcopy(inst.events) - ev_id = deepcopy(inst.event_id) - selection = deepcopy(inst.selection) - drop_log = deepcopy(inst.drop_log) - else: - # if the input is of class Evoked - meta = evs = ev_id = selection = drop_log = None - - out = EpochsTFR( - info, - power, - times, - freqs, - method="%s-power" % method, - events=evs, - event_id=ev_id, - selection=selection, - drop_log=drop_log, - metadata=meta, - ) - - return out - - + if isinstance(inst, BaseEpochs): + kwargs.update(average=average, return_itc=return_itc) + elif average: + logger.info("inst is Evoked, setting `average=False`") + average = False + if average and output == "complex": + raise ValueError('output must be "power" if average=True') + if not average and return_itc: + raise ValueError("Inter-trial coherence is not supported with average=False") + return inst.compute_tfr(**kwargs) + + +@legacy(alt='.compute_tfr(method="morlet")') @verbose def tfr_morlet( inst, @@ -906,7 +870,7 @@ def tfr_morlet( ---------- inst : Epochs | Evoked The epochs or evoked object. - %(freqs_tfr)s + %(freqs_tfr_array)s %(n_cycles_tfr)s use_fft : bool, default False The fft based convolution or not. @@ -977,7 +941,7 @@ def tfr_array_morlet( sfreq, freqs, n_cycles=7.0, - zero_mean=False, + zero_mean=None, use_fft=True, decim=1, output="complex", @@ -996,10 +960,15 @@ def tfr_array_morlet( The epochs. sfreq : float | int Sampling frequency of the data. - %(freqs_tfr)s + %(freqs_tfr_array)s %(n_cycles_tfr)s - zero_mean : bool + zero_mean : bool | None If True, make sure the wavelets have a mean of zero. default False. + + .. 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. + use_fft : bool Use the FFT for convolutions or not. default True. %(decim_tfr)s @@ -1054,6 +1023,13 @@ 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 " @@ -1077,6 +1053,7 @@ def tfr_array_morlet( ) +@legacy(alt='.compute_tfr(method="multitaper")') @verbose def tfr_multitaper( inst, @@ -1094,15 +1071,15 @@ def tfr_multitaper( ): """Compute Time-Frequency Representation (TFR) using DPSS tapers. - Same computation as `~mne.time_frequency.tfr_array_multitaper`, but - operates on `~mne.Epochs` or `~mne.Evoked` objects instead of + Same computation as :func:`~mne.time_frequency.tfr_array_multitaper`, but + operates on :class:`~mne.Epochs` or :class:`~mne.Evoked` objects instead of :class:`NumPy arrays `. Parameters ---------- inst : Epochs | Evoked The epochs or evoked object. - %(freqs_tfr)s + %(freqs_tfr_array)s %(n_cycles_tfr)s %(time_bandwidth_tfr)s use_fft : bool, default True @@ -1140,6 +1117,9 @@ def tfr_multitaper( .. versionadded:: 0.9.0 """ + from ..epochs import EpochsArray + from ..evoked import Evoked + tfr_params = dict( n_cycles=n_cycles, n_jobs=n_jobs, @@ -1147,23 +1127,578 @@ def tfr_multitaper( zero_mean=True, time_bandwidth=time_bandwidth, ) + if isinstance(inst, Evoked) and not average: + # convert AverageTFR to EpochsTFR for backwards compatibility + inst = EpochsArray(inst.data[np.newaxis], inst.info, tmin=inst.tmin, proj=False) return _tfr_aux( - "multitaper", inst, freqs, decim, return_itc, picks, average, **tfr_params + method="multitaper", + inst=inst, + freqs=freqs, + decim=decim, + return_itc=return_itc, + picks=picks, + average=average, + output="power", + **tfr_params, ) # TFR(s) class -class _BaseTFR(ContainsMixin, UpdateChannelsMixin, SizeMixin, ExtendedTimeMixin): - """Base TFR class.""" +@fill_doc +class BaseTFR(ContainsMixin, UpdateChannelsMixin, SizeMixin, ExtendedTimeMixin): + """Base class for RawTFR, EpochsTFR, and AverageTFR (for type checking only). + + .. note:: + This class should not be instantiated directly; it is provided in the public API + only for type-checking purposes (e.g., ``isinstance(my_obj, BaseTFR)``). To + create TFR objects, use the ``.compute_tfr()`` methods on :class:`~mne.io.Raw`, + :class:`~mne.Epochs`, or :class:`~mne.Evoked`, or use the constructors listed + below under "See Also". + + Parameters + ---------- + inst : instance of Raw, Epochs, or Evoked + The data from which to compute the time-frequency representation. + %(method_tfr)s + %(freqs_tfr)s + %(tmin_tmax_psd)s + %(picks_good_data_noref)s + %(proj_psd)s + %(decim_tfr)s + %(n_jobs)s + %(reject_by_annotation_tfr)s + %(verbose)s + %(method_kw_tfr)s + + See Also + -------- + mne.time_frequency.RawTFR + mne.time_frequency.RawTFRArray + mne.time_frequency.EpochsTFR + mne.time_frequency.EpochsTFRArray + mne.time_frequency.AverageTFR + mne.time_frequency.AverageTFRArray + """ + + def __init__( + self, + inst, + method, + freqs, + tmin, + tmax, + picks, + proj, + *, + decim, + n_jobs, + reject_by_annotation=None, + verbose=None, + **method_kw, + ): + from ..epochs import BaseEpochs + from ._stockwell import tfr_array_stockwell - def __init__(self): - self.baseline = None + # triage reading from file + if isinstance(inst, dict): + self.__setstate__(inst) + return + if method is None or freqs is None: + problem = [ + f"{k}=None" + for k, v in dict(method=method, freqs=freqs).items() + if v is None + ] + # TODO when py3.11 is min version, replace if/elif/else block with + # classname = inspect.currentframe().f_back.f_code.co_qualname.split(".")[0] + _varnames = inspect.currentframe().f_back.f_code.co_varnames + if "BaseRaw" in _varnames: + classname = "RawTFR" + elif "Evoked" in _varnames: + classname = "AverageTFR" + else: + assert "BaseEpochs" in _varnames and "Evoked" not in _varnames + classname = "EpochsTFR" + # end TODO + raise ValueError( + f'{classname} got unsupported parameter value{_pl(problem)} ' + f'{" and ".join(problem)}.' + ) + # shim for tfr_array_morlet deprecation warning (TODO: remove after 1.7 release) + if method == "morlet": + method_kw.setdefault("zero_mean", True) + # check method + valid_methods = ["morlet", "multitaper"] + if isinstance(inst, BaseEpochs): + valid_methods.append("stockwell") + method = _check_option("method", method, valid_methods) + # for stockwell, `tmin, tmax` already added to `method_kw` by calling method, + # and `freqs` vector has been pre-computed + if method != "stockwell": + method_kw.update(freqs=freqs) + # ↓↓↓ if constructor called directly, prevents key error + method_kw.setdefault("output", "power") + self._freqs = np.asarray(freqs, dtype=np.float64) + del freqs + # check validity of kwargs manually to save compute time if any are invalid + tfr_funcs = dict( + morlet=tfr_array_morlet, + multitaper=tfr_array_multitaper, + stockwell=tfr_array_stockwell, + ) + _check_method_kwargs(tfr_funcs[method], method_kw, msg=f'TFR method "{method}"') + self._tfr_func = partial(tfr_funcs[method], **method_kw) + # apply proj if desired + if proj: + inst = inst.copy().apply_proj() + self.inst = inst + + # prep picks and add the info object. bads and non-data channels are dropped by + # _picks_to_idx() so we update the info accordingly: + self._picks = _picks_to_idx(inst.info, picks, "data", with_ref_meg=False) + self.info = pick_info(inst.info, sel=self._picks, copy=True) + # assign some attributes + self._method = method + self._inst_type = type(inst) + self._baseline = None + self.preload = True # needed for __getitem__, never False for TFRs + # self._dims may also get updated by child classes + self._dims = ["channel", "freq", "time"] + self._needs_taper_dim = method == "multitaper" and method_kw["output"] in ( + "complex", + "phase", + ) + if self._needs_taper_dim: + self._dims.insert(1, "taper") + self._dims = tuple(self._dims) + # get the instance data. + time_mask = _time_mask(inst.times, tmin, tmax, sfreq=self.sfreq) + get_instance_data_kw = dict(time_mask=time_mask) + if reject_by_annotation is not None: + get_instance_data_kw.update(reject_by_annotation=reject_by_annotation) + data = self._get_instance_data(**get_instance_data_kw) + # compute the TFR + self._decim = _ensure_slice(decim) + self._raw_times = inst.times[time_mask] + self._compute_tfr(data, n_jobs, verbose) + self._update_epoch_attributes() + # "apply" decim to the rest of the object (data is decimated in _compute_tfr) + with self.info._unlock(): + self.info["sfreq"] /= self._decim.step + _decim_times = inst.times[self._decim] + _decim_time_mask = _time_mask(_decim_times, tmin, tmax, sfreq=self.sfreq) + self._raw_times = _decim_times[_decim_time_mask].copy() + self._set_times(self._raw_times) self._decim = 1 + # record data type (for repr and html_repr). ITC handled in the calling method. + if method == "stockwell": + self._data_type = "Power Estimates" + else: + data_types = dict( + power="Power Estimates", + avg_power="Average Power Estimates", + avg_power_itc="Average Power Estimates", + phase="Phase", + complex="Complex Amplitude", + ) + self._data_type = data_types[method_kw["output"]] + # check for correct shape and bad values. `tfr_array_stockwell` doesn't take kw + # `output` so it may be missing here, so use `.get()` + negative_ok = method_kw.get("output", "") in ("complex", "phase") + # if method_kw.get("output", None) in ("phase", "complex"): + # raise RuntimeError + self._check_values(negative_ok=negative_ok) + # we don't need these anymore, and they make save/load harder + del self._picks + del self._tfr_func + del self._needs_taper_dim + del self._shape # calculated from self._data henceforth + del self.inst # save memory + + def __abs__(self): + """Return the absolute value.""" + tfr = self.copy() + tfr.data = np.abs(tfr.data) + return tfr + + @fill_doc + def __add__(self, other): + """Add two TFR instances. + + %(__add__tfr)s + """ + self._check_compatibility(other) + out = self.copy() + out.data += other.data + return out + + @fill_doc + def __iadd__(self, other): + """Add a TFR instance to another, in-place. + + %(__iadd__tfr)s + """ + self._check_compatibility(other) + self.data += other.data + return self + + @fill_doc + def __sub__(self, other): + """Subtract two TFR instances. + + %(__sub__tfr)s + """ + self._check_compatibility(other) + out = self.copy() + out.data -= other.data + return out + + @fill_doc + def __isub__(self, other): + """Subtract a TFR instance from another, in-place. + + %(__isub__tfr)s + """ + self._check_compatibility(other) + self.data -= other.data + return self + + @fill_doc + def __mul__(self, num): + """Multiply a TFR instance by a scalar. + + %(__mul__tfr)s + """ + out = self.copy() + out.data *= num + return out + + @fill_doc + def __imul__(self, num): + """Multiply a TFR instance by a scalar, in-place. + + %(__imul__tfr)s + """ + self.data *= num + return self + + @fill_doc + def __truediv__(self, num): + """Divide a TFR instance by a scalar. + + %(__truediv__tfr)s + """ + out = self.copy() + out.data /= num + return out + + @fill_doc + def __itruediv__(self, num): + """Divide a TFR instance by a scalar, in-place. + + %(__itruediv__tfr)s + """ + self.data /= num + return self + + def __eq__(self, other): + """Test equivalence of two TFR instances.""" + return object_diff(vars(self), vars(other)) == "" + + def __getstate__(self): + """Prepare object for serialization.""" + return dict( + method=self.method, + data=self._data, + sfreq=self.sfreq, + dims=self._dims, + freqs=self.freqs, + times=self.times, + inst_type_str=_get_instance_type_string(self), + data_type=self._data_type, + info=self.info, + baseline=self._baseline, + decim=self._decim, + ) + + def __setstate__(self, state): + """Unpack from serialized format.""" + from ..epochs import Epochs + from ..evoked import Evoked + from ..io import Raw + + defaults = dict( + method="unknown", + dims=("epoch", "channel", "freq", "time")[-state["data"].ndim :], + baseline=None, + decim=1, + data_type="TFR", + inst_type_str="Unknown", + ) + defaults.update(**state) + self._method = defaults["method"] + self._data = defaults["data"] + self._freqs = np.asarray(defaults["freqs"], dtype=np.float64) + self._dims = defaults["dims"] + self._raw_times = np.asarray(defaults["times"], dtype=np.float64) + self._baseline = defaults["baseline"] + self.info = Info(**defaults["info"]) + self._data_type = defaults["data_type"] + self._decim = defaults["decim"] + self.preload = True + self._set_times(self._raw_times) + # Handle instance type. Prior to gh-11282, Raw was not a possibility so if + # `inst_type_str` is missing it must be Epochs or Evoked + unknown_class = Epochs if self._data.ndim == 4 else Evoked + inst_types = dict(Raw=Raw, Epochs=Epochs, Evoked=Evoked, Unknown=unknown_class) + self._inst_type = inst_types[defaults["inst_type_str"]] + # sanity check data/freqs/times/info agreement + self._check_state() + + def __repr__(self): + """Build string representation of the TFR object.""" + inst_type_str = _get_instance_type_string(self) + nave = f" (nave={self.nave})" if hasattr(self, "nave") else "" + # shape & dimension names + dims = " × ".join( + [f"{size} {dim}s" for size, dim in zip(self.shape, self._dims)] + ) + freq_range = f"{self.freqs[0]:0.1f} - {self.freqs[-1]:0.1f} Hz" + time_range = f"{self.times[0]:0.2f} - {self.times[-1]:0.2f} s" + return ( + f"<{self._data_type} from {inst_type_str}{nave}, " + f"{self.method} method | {dims}, {freq_range}, {time_range}, " + f"{sizeof_fmt(self._size)}>" + ) + + @repr_html + def _repr_html_(self, caption=None): + """Build HTML representation of the TFR object.""" + from ..html_templates import _get_html_template + + inst_type_str = _get_instance_type_string(self) + nave = getattr(self, "nave", 0) + t = _get_html_template("repr", "tfr.html.jinja") + t = t.render(tfr=self, inst_type=inst_type_str, nave=nave, caption=caption) + return t + + def _check_compatibility(self, other): + """Check compatibility of two TFR instances, in preparation for arithmetic.""" + operation = inspect.currentframe().f_back.f_code.co_name.strip("_") + if operation.startswith("i"): + operation = operation[1:] + msg = f"Cannot {operation} the two TFR instances: {{}} do not match{{}}." + extra = "" + if not isinstance(other, type(self)): + problem = "types" + extra = f" (self is {type(self)}, other is {type(other)})" + elif not self.times.shape == other.times.shape or np.any( + self.times != other.times + ): + problem = "times" + elif not self.freqs.shape == other.freqs.shape or np.any( + self.freqs != other.freqs + ): + problem = "freqs" + else: # should be OK + return + raise RuntimeError(msg.format(problem, extra)) + + def _check_state(self): + """Check data/freqs/times/info agreement during __setstate__.""" + msg = "{} axis of data ({}) doesn't match {} attribute ({})" + n_chan_info = len(self.info["chs"]) + n_chan, n_freq, n_time = self._data.shape[self._dims.index("channel") :] + if n_chan_info != n_chan: + msg = msg.format("Channel", n_chan, "info", n_chan_info) + elif n_freq != len(self.freqs): + msg = msg.format("Frequency", n_freq, "freqs", self.freqs.size) + elif n_time != len(self.times): + msg = msg.format("Time", n_time, "times", self.times.size) + else: + return + raise ValueError(msg) + + def _check_values(self, negative_ok=False): + """Check TFR results for correct shape and bad values.""" + assert len(self._dims) == self._data.ndim + assert self._data.shape == self._shape + # Check for implausible power values: take min() across all but the channel axis + # TODO: should this be more fine-grained (report "chan X in epoch Y")? + ch_dim = self._dims.index("channel") + dims = np.arange(self._data.ndim).tolist() + dims.pop(ch_dim) + negative_values = self._data.min(axis=tuple(dims)) < 0 + if negative_values.any() and not negative_ok: + chs = np.array(self.ch_names)[negative_values].tolist() + s = _pl(negative_values.sum()) + warn( + f"Negative value in time-frequency decomposition for channel{s} " + f'{", ".join(chs)}', + UserWarning, + ) + + def _compute_tfr(self, data, n_jobs, verbose): + result = self._tfr_func( + data, + self.sfreq, + decim=self._decim, + n_jobs=n_jobs, + verbose=verbose, + ) + # assign ._data and maybe ._itc + # tfr_array_stockwell always returns ITC (sometimes it's None) + if self.method == "stockwell": + self._data, self._itc, freqs = result + assert np.array_equal(self._freqs, freqs) + elif self._tfr_func.keywords.get("output", "").endswith("_itc"): + self._data, self._itc = result.real, result.imag + else: + self._data = result + # remove fake "epoch" dimension + if self.method != "stockwell" and _get_instance_type_string(self) != "Epochs": + self._data = np.squeeze(self._data, axis=0) + + # this is *expected* shape, it gets asserted later in _check_values() + # (and then deleted afterwards) + expected_shape = [ + len(self.ch_names), + len(self.freqs), + len(self._raw_times[self._decim]), # don't use self.times, not set yet + ] + # deal with the "taper" dimension + if self._needs_taper_dim: + expected_shape.insert(1, self._data.shape[1]) + self._shape = tuple(expected_shape) + + @verbose + def _onselect( + self, + eclick, + erelease, + picks=None, + exclude="bads", + combine="mean", + baseline=None, + mode=None, + cmap=None, + source_plot_joint=False, + topomap_args=None, + verbose=None, + ): + """Respond to rectangle selector in TFR image plots with a topomap plot.""" + if abs(eclick.x - erelease.x) < 0.1 or abs(eclick.y - erelease.y) < 0.1: + return + t_range = (min(eclick.xdata, erelease.xdata), max(eclick.xdata, erelease.xdata)) + f_range = (min(eclick.ydata, erelease.ydata), max(eclick.ydata, erelease.ydata)) + # snap to nearest measurement point + t_idx = np.abs(self.times - np.atleast_2d(t_range).T).argmin(axis=1) + f_idx = np.abs(self.freqs - np.atleast_2d(f_range).T).argmin(axis=1) + tmin, tmax = self.times[t_idx] + fmin, fmax = self.freqs[f_idx] + # immutable → mutable default + if topomap_args is None: + topomap_args = dict() + topomap_args.setdefault("cmap", cmap) + topomap_args.setdefault("vlim", (None, None)) + # figure out which channel types we're dealing with + types = list() + if "eeg" in self: + types.append("eeg") + if "mag" in self: + types.append("mag") + if "grad" in self: + grad_picks = _pair_grad_sensors( + self.info, topomap_coords=False, raise_error=False + ) + if len(grad_picks) > 1: + types.append("grad") + elif len(types) == 0: + logger.info( + "Need at least 2 gradiometer pairs to plot a gradiometer topomap." + ) + return # Don't draw a figure for nothing. + + fig = figure_nobar() + t_range = f"{tmin:.3f}" if tmin == tmax else f"{tmin:.3f} - {tmax:.3f}" + f_range = f"{fmin:.2f}" if fmin == fmax else f"{fmin:.2f} - {fmax:.2f}" + fig.suptitle(f"{t_range} s,\n{f_range} Hz") + + if source_plot_joint: + ax = fig.add_subplot() + data, times, freqs = self.get_data( + picks=picks, exclude=exclude, return_times=True, return_freqs=True + ) + # merge grads before baselining (makes ERDs visible) + ch_types = np.array(self.get_channel_types(unique=True)) + ch_type = ch_types.item() # will error if there are more than one + data, pos = _merge_if_grads( + data=data, + info=self.info, + ch_type=ch_type, + sphere=topomap_args.get("sphere"), + combine=combine, + ) + # baseline and crop + data, *_ = _prep_data_for_plot( + data, + times, + freqs, + tmin=tmin, + tmax=tmax, + fmin=fmin, + fmax=fmax, + baseline=baseline, + mode=mode, + verbose=verbose, + ) + # average over times and freqs + data = data.mean((-2, -1)) + + im, _ = plot_topomap(data, pos, axes=ax, show=False, **topomap_args) + _add_colorbar(ax, im, topomap_args["cmap"], title="AU") + plt_show(fig=fig) + else: + for idx, ch_type in enumerate(types): + ax = fig.add_subplot(1, len(types), idx + 1) + plot_tfr_topomap( + self, + ch_type=ch_type, + tmin=tmin, + tmax=tmax, + fmin=fmin, + fmax=fmax, + baseline=baseline, + mode=mode, + axes=ax, + **topomap_args, + ) + ax.set_title(ch_type) + + def _update_epoch_attributes(self): + # overwritten in EpochsTFR; adds things needed for to_data_frame and __getitem__ + pass + + @property + def _detrend_picks(self): + """Provide compatibility with __iter__.""" + return list() + + @property + def baseline(self): + """Start and end of the baseline period (in seconds).""" + return self._baseline + + @property + def ch_names(self): + """The channel names.""" + return self.info["ch_names"] @property def data(self): + """The time-frequency-resolved power estimates.""" return self._data @data.setter @@ -1171,9 +1706,29 @@ def data(self, data): self._data = data @property - def ch_names(self): - """Channel names.""" - return self.info["ch_names"] + def freqs(self): + """The frequencies at which power estimates were computed.""" + return self._freqs + + @property + def method(self): + """The method used to compute the time-frequency power estimates.""" + return self._method + + @property + def sfreq(self): + """Sampling frequency of the data.""" + return self.info["sfreq"] + + @property + def shape(self): + """Data shape.""" + return self._data.shape + + @property + def times(self): + """The time points present in the data (in seconds).""" + return self._times_readonly @fill_doc def crop(self, tmin=None, tmax=None, fmin=None, fmax=None, include_tmax=True): @@ -1181,10 +1736,7 @@ def crop(self, tmin=None, tmax=None, fmin=None, fmax=None, include_tmax=True): Parameters ---------- - tmin : float | None - Start time of selection in seconds. - tmax : float | None - End time of selection in seconds. + %(tmin_tmax_psd)s fmin : float | None Lowest frequency of selection in Hz. @@ -1197,7 +1749,7 @@ def crop(self, tmin=None, tmax=None, fmin=None, fmax=None, include_tmax=True): Returns ------- - inst : instance of AverageTFR + %(inst_tfr)s The modified instance. """ super().crop(tmin=tmin, tmax=tmax, include_tmax=include_tmax) @@ -1209,7 +1761,7 @@ def crop(self, tmin=None, tmax=None, fmin=None, fmax=None, include_tmax=True): else: freq_mask = slice(None) - self.freqs = self.freqs[freq_mask] + self._freqs = self.freqs[freq_mask] # Deal with broadcasting (boolean arrays do not broadcast, but indices # do, so we need to convert freq_mask to make use of broadcasting) if isinstance(freq_mask, np.ndarray): @@ -1218,12 +1770,12 @@ def crop(self, tmin=None, tmax=None, fmin=None, fmax=None, include_tmax=True): return self def copy(self): - """Return a copy of the instance. + """Return copy of the TFR instance. Returns ------- - copy : instance of EpochsTFR | instance of AverageTFR - A copy of the instance. + %(inst_tfr)s + A copy of the object. """ return deepcopy(self) @@ -1233,14 +1785,9 @@ def apply_baseline(self, baseline, mode="mean", verbose=None): Parameters ---------- - baseline : array-like, shape (2,) - The time interval to apply rescaling / baseline correction. - If None do not apply it. If baseline is (a, b) - the interval is between "a (s)" and "b (s)". - If a is None the beginning of the data is used - and if b is None then b is set to the end of the interval. - If baseline is equal to (None, None) all the time - interval is used. + %(baseline_rescale)s + + How baseline is computed is determined by the ``mode`` parameter. mode : 'mean' | 'ratio' | 'logratio' | 'percent' | 'zscore' | 'zlogratio' Perform baseline correction by @@ -1259,524 +1806,313 @@ def apply_baseline(self, baseline, mode="mean", verbose=None): Returns ------- - inst : instance of AverageTFR + %(inst_tfr)s The modified instance. - """ # noqa: E501 - self.baseline = _check_baseline( - baseline, times=self.times, sfreq=self.info["sfreq"] - ) - rescale(self.data, self.times, self.baseline, mode, copy=False) + """ + self._baseline = _check_baseline(baseline, times=self.times, sfreq=self.sfreq) + rescale(self.data, self.times, self.baseline, mode, copy=False, verbose=verbose) return self - @verbose - def save(self, fname, overwrite=False, *, verbose=None): - """Save TFR object to hdf5 file. + @fill_doc + def get_data( + self, + picks=None, + exclude="bads", + fmin=None, + fmax=None, + tmin=None, + tmax=None, + return_times=False, + return_freqs=False, + ): + """Get time-frequency data in NumPy array format. Parameters ---------- - fname : path-like - The file name, which should end with ``-tfr.h5``. - %(overwrite)s - %(verbose)s + %(picks_good_data_noref)s + %(exclude_spectrum_get_data)s + %(fmin_fmax_tfr)s + %(tmin_tmax_psd)s + return_times : bool + Whether to return the time values for the requested time range. + Default is ``False``. + return_freqs : bool + Whether to return the frequency bin values for the requested + frequency range. Default is ``False``. - See Also - -------- - read_tfrs, write_tfrs - """ - write_tfrs(fname, self, overwrite=overwrite) + Returns + ------- + data : array + The requested data in a NumPy array. + times : array + The time values for the requested data range. Only returned if + ``return_times`` is ``True``. + freqs : array + The frequency values for the requested data range. Only returned if + ``return_freqs`` is ``True``. - @verbose - def to_data_frame( - self, - picks=None, - index=None, - long_format=False, - time_format=None, - *, - verbose=None, - ): - """Export data in tabular structure as a pandas DataFrame. - - Channels are converted to columns in the DataFrame. By default, - additional columns ``'time'``, ``'freq'``, ``'epoch'``, and - ``'condition'`` (epoch event description) are added, unless ``index`` - is not ``None`` (in which case the columns specified in ``index`` will - be used to form the DataFrame's index instead). ``'epoch'``, and - ``'condition'`` are not supported for ``AverageTFR``. - - Parameters - ---------- - %(picks_all)s - %(index_df_epo)s - Valid string values are ``'time'``, ``'freq'``, ``'epoch'``, and - ``'condition'`` for ``EpochsTFR`` and ``'time'`` and ``'freq'`` - for ``AverageTFR``. - Defaults to ``None``. - %(long_format_df_epo)s - %(time_format_df)s - - .. versionadded:: 0.23 - %(verbose)s - - Returns - ------- - %(df_return)s + Notes + ----- + Returns a copy of the underlying data (not a view). """ - # check pandas once here, instead of in each private utils function - pd = _check_pandas_installed() # noqa - # arg checking - valid_index_args = ["time", "freq"] - if isinstance(self, EpochsTFR): - valid_index_args.extend(["epoch", "condition"]) - valid_time_formats = ["ms", "timedelta"] - index = _check_pandas_index_arguments(index, valid_index_args) - time_format = _check_time_format(time_format, valid_time_formats) - # get data - times = self.times - picks = _picks_to_idx(self.info, picks, "all", exclude=()) - if isinstance(self, EpochsTFR): - data = self.data[:, picks, :, :] - else: - data = self.data[np.newaxis, picks] # add singleton "epochs" axis - n_epochs, n_picks, n_freqs, n_times = data.shape - # reshape to (epochs*freqs*times) x signals - data = np.moveaxis(data, 1, -1) - data = data.reshape(n_epochs * n_freqs * n_times, n_picks) - # prepare extra columns / multiindex - mindex = list() - times = np.tile(times, n_epochs * n_freqs) - times = _convert_times(times, time_format, self.info["meas_date"]) - mindex.append(("time", times)) - freqs = self.freqs - freqs = np.tile(np.repeat(freqs, n_times), n_epochs) - mindex.append(("freq", freqs)) - if isinstance(self, EpochsTFR): - mindex.append(("epoch", np.repeat(self.selection, n_times * n_freqs))) - rev_event_id = {v: k for k, v in self.event_id.items()} - conditions = [rev_event_id[k] for k in self.events[:, 2]] - mindex.append(("condition", np.repeat(conditions, n_times * n_freqs))) - assert all(len(mdx) == len(mindex[0]) for mdx in mindex) - # build DataFrame - if isinstance(self, EpochsTFR): - default_index = ["condition", "epoch", "freq", "time"] - else: - default_index = ["freq", "time"] - df = _build_data_frame( - self, data, picks, long_format, mindex, index, default_index=default_index + tmin = self.times[0] if tmin is None else tmin + tmax = self.times[-1] if tmax is None else tmax + fmin = 0 if fmin is None else fmin + fmax = np.inf if fmax is None else fmax + picks = _picks_to_idx( + self.info, picks, "data_or_ica", exclude=exclude, with_ref_meg=False ) - return df - - -@fill_doc -class AverageTFR(_BaseTFR): - """Container for Time-Frequency data. - - Can for example store induced power at sensor level or inter-trial - coherence. - - Parameters - ---------- - %(info_not_none)s - data : ndarray, shape (n_channels, n_freqs, n_times) - The data. - times : ndarray, shape (n_times,) - The time values in seconds. - freqs : ndarray, shape (n_freqs,) - The frequencies in Hz. - nave : int - The number of averaged TFRs. - comment : str | None, default None - Comment on the data, e.g., the experimental condition. - method : str | None, default None - Comment on the method used to compute the data, e.g., morlet wavelet. - %(verbose)s - - Attributes - ---------- - %(info_not_none)s - ch_names : list - The names of the channels. - nave : int - Number of averaged epochs. - data : ndarray, shape (n_channels, n_freqs, n_times) - The data array. - times : ndarray, shape (n_times,) - The time values in seconds. - freqs : ndarray, shape (n_freqs,) - The frequencies in Hz. - comment : str - Comment on dataset. Can be the condition. - method : str | None, default None - Comment on the method used to compute the data, e.g., morlet wavelet. - """ - - @verbose - def __init__( - self, info, data, times, freqs, nave, comment=None, method=None, verbose=None - ): - super().__init__() - self.info = info - if data.ndim != 3: - raise ValueError("data should be 3d. Got %d." % data.ndim) - n_channels, n_freqs, n_times = data.shape - if n_channels != len(info["chs"]): - raise ValueError( - "Number of channels and data size don't match" - " (%d != %d)." % (n_channels, len(info["chs"])) - ) - if n_freqs != len(freqs): - raise ValueError( - "Number of frequencies and data size don't match" - " (%d != %d)." % (n_freqs, len(freqs)) - ) - if n_times != len(times): - raise ValueError( - "Number of times and data size don't match" - " (%d != %d)." % (n_times, len(times)) - ) - self.data = data - self._set_times(np.array(times, dtype=float)) - self._raw_times = self.times.copy() - self.freqs = np.array(freqs, dtype=float) - self.nave = nave - self.comment = comment - self.method = method - self.preload = True + fmin_idx = np.searchsorted(self.freqs, fmin) + fmax_idx = np.searchsorted(self.freqs, fmax, side="right") + tmin_idx = np.searchsorted(self.times, tmin) + tmax_idx = np.searchsorted(self.times, tmax, side="right") + freq_picks = np.arange(fmin_idx, fmax_idx) + time_picks = np.arange(tmin_idx, tmax_idx) + freq_axis = self._dims.index("freq") + time_axis = self._dims.index("time") + chan_axis = self._dims.index("channel") + # normally there's a risk of np.take reducing array dimension if there + # were only one channel or frequency selected, but `_picks_to_idx` + # and np.arange both always return arrays, so we're safe; the result + # will always have the same `ndim` as it started with. + data = ( + self._data.take(picks, chan_axis) + .take(freq_picks, freq_axis) + .take(time_picks, time_axis) + ) + out = [data] + if return_times: + times = self._raw_times[tmin_idx:tmax_idx] + out.append(times) + if return_freqs: + freqs = self._freqs[fmin_idx:fmax_idx] + out.append(freqs) + if not return_times and not return_freqs: + return out[0] + return tuple(out) @verbose def plot( self, picks=None, - baseline=None, - mode="mean", + *, + exclude=(), tmin=None, tmax=None, - fmin=None, - fmax=None, + fmin=0.0, + fmax=np.inf, + baseline=None, + mode="mean", + dB=False, + combine=None, + layout=None, # TODO deprecate? not used in orig implementation either + yscale="auto", vmin=None, vmax=None, - cmap="RdBu_r", - dB=False, + vlim=(None, None), + cnorm=None, + cmap=None, colorbar=True, - show=True, - title=None, - axes=None, - layout=None, - yscale="auto", + title=None, # don't deprecate this one; has (useful) option title="auto" mask=None, mask_style=None, mask_cmap="Greys", mask_alpha=0.1, - combine=None, - exclude=(), - cnorm=None, + axes=None, + show=True, verbose=None, ): - """Plot TFRs as a two-dimensional image(s). + """Plot TFRs as two-dimensional time-frequency images. Parameters ---------- %(picks_good_data)s - baseline : None (default) or tuple, shape (2,) - The time interval to apply baseline correction. - If None do not apply it. If baseline is (a, b) - the interval is between "a (s)" and "b (s)". - If a is None the beginning of the data is used - and if b is None then b is set to the end of the interval. - If baseline is equal to (None, None) all the time - interval is used. - mode : 'mean' | 'ratio' | 'logratio' | 'percent' | 'zscore' | 'zlogratio' - Perform baseline correction by + %(exclude_spectrum_plot)s + %(tmin_tmax_psd)s + %(fmin_fmax_tfr)s + %(baseline_rescale)s - - subtracting the mean of baseline values ('mean') (default) - - dividing by the mean of baseline values ('ratio') - - dividing by the mean of baseline values and taking the log - ('logratio') - - subtracting the mean of baseline values followed by dividing by - the mean of baseline values ('percent') - - subtracting the mean of baseline values and dividing by the - standard deviation of baseline values ('zscore') - - dividing by the mean of baseline values, taking the log, and - dividing by the standard deviation of log baseline values - ('zlogratio') + How baseline is computed is determined by the ``mode`` parameter. + %(mode_tfr_plot)s + %(dB_spectrum_plot)s + %(combine_tfr_plot)s - tmin : None | float - The first time instant to display. If None the first time point - available is used. Defaults to None. - tmax : None | float - The last time instant to display. If None the last time point - available is used. Defaults to None. - fmin : None | float - The first frequency to display. If None the first frequency - available is used. Defaults to None. - fmax : None | float - The last frequency to display. If None the last frequency - available is used. Defaults to None. - vmin : float | None - The minimum value an the color scale. If vmin is None, the data - minimum value is used. Defaults to None. - vmax : float | None - The maximum value an the color scale. If vmax is None, the data - maximum value is used. Defaults to None. - cmap : matplotlib colormap | 'interactive' | (colormap, bool) - The colormap to use. If tuple, the first value indicates the - colormap to use and the second value is a boolean defining - interactivity. In interactive mode the colors are adjustable by - clicking and dragging the colorbar with left and right mouse - button. Left mouse button moves the scale up and down and right - mouse button adjusts the range. Hitting space bar resets the range. - Up and down arrows can be used to change the colormap. If - 'interactive', translates to ('RdBu_r', True). Defaults to - 'RdBu_r'. - - .. warning:: Interactive mode works smoothly only for a small - amount of images. - - dB : bool - If True, 10*log10 is applied to the data to get dB. - Defaults to False. - colorbar : bool - If true, colorbar will be added to the plot. Defaults to True. - show : bool - Call pyplot.show() at the end. Defaults to True. - title : str | 'auto' | None - String for ``title``. Defaults to None (blank/no title). If - 'auto', and ``combine`` is None, the title for each figure - will be the channel name. If 'auto' and ``combine`` is not None, - ``title`` states how many channels were combined into that figure - and the method that was used for ``combine``. If str, that String - will be the title for each figure. - axes : instance of Axes | list | None - The axes to plot to. If list, the list must be a list of Axes of - the same length as ``picks``. If instance of Axes, there must be - only one channel plotted. If ``combine`` is not None, ``axes`` - must either be an instance of Axes, or a list of length 1. - layout : Layout | None - Layout instance specifying sensor positions. Used for interactive - plotting of topographies on rectangle selection. If possible, the - correct layout is inferred from the data. - yscale : 'auto' (default) | 'linear' | 'log' - The scale of y (frequency) axis. 'linear' gives linear y axis, - 'log' leads to log-spaced y axis and 'auto' detects if frequencies - are log-spaced and only then sets the y axis to 'log'. + .. versionchanged:: 1.3 + Added support for ``callable``. + %(layout_spectrum_plot_topo)s + %(yscale_tfr_plot)s .. versionadded:: 0.14.0 - mask : ndarray | None - An array of booleans of the same shape as the data. Entries of the - data that correspond to False in the mask are plotted - transparently. Useful for, e.g., masking for statistical - significance. + %(vmin_vmax_tfr_plot)s + %(vlim_tfr_plot)s + %(cnorm)s + + .. versionadded:: 0.24 + %(cmap_topomap)s + %(colorbar)s + %(title_tfr_plot)s + %(mask_tfr_plot)s .. versionadded:: 0.16.0 - mask_style : None | 'both' | 'contour' | 'mask' - If ``mask`` is not None: if ``'contour'``, a contour line is drawn - around the masked areas (``True`` in ``mask``). If ``'mask'``, - entries not ``True`` in ``mask`` are shown transparently. If - ``'both'``, both a contour and transparency are used. - If ``None``, defaults to ``'both'`` if ``mask`` is not None, and is - ignored otherwise. + %(mask_style_tfr_plot)s .. versionadded:: 0.17 - mask_cmap : matplotlib colormap | (colormap, bool) | 'interactive' - The colormap chosen for masked parts of the image (see below), if - ``mask`` is not ``None``. If None, ``cmap`` is reused. Defaults to - ``'Greys'``. Not interactive. Otherwise, as ``cmap``. + %(mask_cmap_tfr_plot)s .. versionadded:: 0.17 - mask_alpha : float - A float between 0 and 1. If ``mask`` is not None, this sets the - alpha level (degree of transparency) for the masked-out segments. - I.e., if 0, masked-out segments are not visible at all. - Defaults to 0.1. + %(mask_alpha_tfr_plot)s .. versionadded:: 0.16.0 - combine : 'mean' | 'rms' | callable | None - Type of aggregation to perform across selected channels. If - None, plot one figure per selected channel. If a function, it must - operate on an array of shape ``(n_channels, n_freqs, n_times)`` and - return an array of shape ``(n_freqs, n_times)``. - - .. versionchanged:: 1.3 - Added support for ``callable``. - exclude : list of str | 'bads' - Channels names to exclude from being shown. If 'bads', the - bad channels are excluded. Defaults to an empty list. - %(cnorm)s - - .. versionadded:: 0.24 + %(axes_tfr_plot)s + %(show)s %(verbose)s Returns ------- figs : list of instances of matplotlib.figure.Figure A list of figures containing the time-frequency power. - """ # noqa: E501 - return self._plot( - picks=picks, - baseline=baseline, - mode=mode, + """ + # deprecations + vlim = _warn_deprecated_vmin_vmax(vlim, vmin, vmax) + # the rectangle selector plots topomaps, which needs all channels uncombined, + # so we keep a reference to that state here, and (because the topomap plotting + # function wants an AverageTFR) update it with `comment` and `nave` values in + # case we started out with a singleton EpochsTFR or RawTFR + initial_state = self.__getstate__() + initial_state.setdefault("comment", "") + initial_state.setdefault("nave", 1) + # `_picks_to_idx` also gets done inside `get_data()`` below, but we do it here + # because we need the indices later + idx_picks = _picks_to_idx( + self.info, picks, "data_or_ica", exclude=exclude, with_ref_meg=False + ) + pick_names = np.array(self.ch_names)[idx_picks].tolist() # for titles + ch_types = self.get_channel_types(idx_picks) + # get data arrays + data, times, freqs = self.get_data( + picks=idx_picks, exclude=(), return_times=True, return_freqs=True + ) + # pass tmin/tmax here ↓↓↓, not here ↑↑↑; we want to crop *after* baselining + data, times, freqs = _prep_data_for_plot( + data, + times, + freqs, tmin=tmin, tmax=tmax, fmin=fmin, fmax=fmax, - vmin=vmin, - vmax=vmax, - cmap=cmap, + baseline=baseline, + mode=mode, dB=dB, - colorbar=colorbar, - show=show, - title=title, - axes=axes, - layout=layout, - yscale=yscale, - mask=mask, - mask_style=mask_style, - mask_cmap=mask_cmap, - mask_alpha=mask_alpha, - combine=combine, - exclude=exclude, - cnorm=cnorm, verbose=verbose, ) - - @verbose - def _plot( - self, - picks=None, - baseline=None, - mode="mean", - tmin=None, - tmax=None, - fmin=None, - fmax=None, - vmin=None, - vmax=None, - cmap="RdBu_r", - dB=False, - colorbar=True, - show=True, - title=None, - axes=None, - layout=None, - yscale="auto", - mask=None, - mask_style=None, - mask_cmap="Greys", - mask_alpha=0.25, - combine=None, - exclude=None, - copy=True, - source_plot_joint=False, - topomap_args=None, - ch_type=None, - cnorm=None, - verbose=None, - ): - """Plot TFRs as a two-dimensional image(s). - - See self.plot() for parameters description. - """ - _validate_type(topomap_args, (dict, None), "topomap_args") - topomap_args = {} if topomap_args is None else topomap_args - import matplotlib.pyplot as plt - - # channel selection - # simply create a new tfr object(s) with the desired channel selection - tfr = _preproc_tfr_instance( - self, - picks, - tmin, - tmax, - fmin, - fmax, - vmin, - vmax, - dB, - mode, - baseline, - exclude, - copy, + # shape + ch_axis = self._dims.index("channel") + freq_axis = self._dims.index("freq") + time_axis = self._dims.index("time") + want_shape = list(self.shape) + want_shape[ch_axis] = len(idx_picks) if combine is None else 1 + want_shape[freq_axis] = len(freqs) # in case there was fmin/fmax cropping + want_shape[time_axis] = len(times) # in case there was tmin/tmax cropping + want_shape = tuple(want_shape) + # combine + combine_was_none = combine is None + combine = _make_combine_callable( + combine, axis=ch_axis, valid=("mean", "rms"), keepdims=True ) - del picks - - data = tfr.data - n_picks = len(tfr.ch_names) if combine is None else 1 - - # combine picks - _validate_type(combine, (None, str, "callable")) - if isinstance(combine, str): - _check_option("combine", combine, ("mean", "rms")) - if combine == "mean": - data = data.mean(axis=0, keepdims=True) - elif combine == "rms": - data = np.sqrt((data**2).mean(axis=0, keepdims=True)) - elif combine is not None: # callable - # It must operate on (n_channels, n_freqs, n_times) and return - # (n_freqs, n_times). Operates on a copy in-case 'combine' does - # some in-place operations. - try: - data = combine(data.copy()) - except TypeError: - raise RuntimeError( - "A callable 'combine' must operate on a single argument, " - "a numpy array of shape (n_channels, n_freqs, n_times)." - ) - if not isinstance(data, np.ndarray) or data.shape != tfr.data.shape[1:]: - raise RuntimeError( - "A callable 'combine' must return a numpy array of shape " - "(n_freqs, n_times)." - ) - # keep initial dimensions + try: + data = combine(data) # no need to copy; get_data() never returns a view + except Exception as e: + msg = ( + "Something went wrong with the callable passed to 'combine'; see " + "traceback." + ) + raise ValueError(msg) from e + # call succeeded, check type and shape + mismatch = False + if not isinstance(data, np.ndarray): + mismatch = "type" + extra = "" + elif data.shape not in (want_shape, want_shape[1:]): + mismatch = "shape" + extra = f" of shape {data.shape}" + if mismatch: + raise RuntimeError( + f"Wrong {mismatch} yielded by callable passed to 'combine'. Make sure " + "your function takes a single argument (an array of shape " + "(n_channels, n_freqs, n_times)) and returns an array of shape " + f"(n_freqs, n_times); yours yielded: {type(data)}{extra}." + ) + # restore singleton collapsed axis (removed by user-provided callable): + # (n_freqs, n_times) → (1, n_freqs, n_times) + if data.shape == (len(freqs), len(times)): data = data[np.newaxis] - # figure overhead - # set plot dimension - tmin, tmax = tfr.times[[0, -1]] - if vmax is None: - vmax = np.abs(data).max() - if vmin is None: - vmin = -np.abs(data).max() - - # set colorbar - cmap = _setup_cmap(cmap) - - # make sure there are as many axes as there will be channels to plot - if isinstance(axes, list) or isinstance(axes, np.ndarray): - figs_and_axes = [(ax.get_figure(), ax) for ax in axes] + assert data.shape == want_shape + # cmap handling. power may be negative depending on baseline strategy so set + # `norm` empirically — but only if user didn't set limits explicitly. + norm = False if vlim == (None, None) else data.min() >= 0.0 + vmin, vmax = _setup_vmin_vmax(data, *vlim, norm=norm) + cmap = _setup_cmap(cmap, norm=norm) + # prepare figure(s) + if axes is None: + figs = [plt.figure(layout="constrained") for _ in range(data.shape[0])] + axes = [fig.add_subplot() for fig in figs] elif isinstance(axes, plt.Axes): - figs_and_axes = [(ax.get_figure(), ax) for ax in [axes]] - elif axes is None: - figs = [plt.figure(layout="constrained") for i in range(n_picks)] - figs_and_axes = [(fig, fig.add_subplot(111)) for fig in figs] + figs = [axes.get_figure()] + axes = [axes] + elif isinstance(axes, np.ndarray): # allow plotting into a grid of axes + figs = [ax.get_figure() for ax in axes.flat] + elif hasattr(axes, "__iter__") and len(axes): + figs = [ax.get_figure() for ax in axes] else: - raise ValueError("axes must be None, plt.Axes, or list " "of plt.Axes.") - if len(figs_and_axes) != n_picks: - raise RuntimeError("There must be an axes for each picked " "channel.") - - for idx in range(n_picks): - fig = figs_and_axes[idx][0] - ax = figs_and_axes[idx][1] - onselect_callback = partial( - tfr._onselect, + raise ValueError( + f"axes must be None, Axes, or list/array of Axes, got {type(axes)}" + ) + if len(axes) != data.shape[0]: + raise RuntimeError( + f"Mismatch between picked channels ({data.shape[0]}) and axes " + f"({len(axes)}); there must be one axes for each picked channel." + ) + # check if we're being called from within plot_joint(). If so, get the + # `topomap_args` from the calling context and pass it to the onselect handler. + # (we need 2 `f_back` here because of the verbose decorator) + calling_frame = inspect.currentframe().f_back.f_back + source_plot_joint = calling_frame.f_code.co_name == "plot_joint" + topomap_args = ( + dict() + if not source_plot_joint + else calling_frame.f_locals.get("topomap_args", dict()) + ) + # plot + for ix, _fig in enumerate(figs): + # restrict the onselect instance to the channel type of the picks used in + # the image plot + uniq_types = np.unique(ch_types) + ch_type = None if len(uniq_types) > 1 else uniq_types.item() + this_tfr = AverageTFR(inst=initial_state).pick(ch_type, verbose=verbose) + _onselect_callback = partial( + this_tfr._onselect, + picks=None, # already restricted the picks in `this_tfr` + exclude=(), + baseline=baseline, + mode=mode, cmap=cmap, source_plot_joint=source_plot_joint, - topomap_args={ - k: v - for k, v in topomap_args.items() - if k not in {"vmin", "vmax", "cmap", "axes"} - }, + topomap_args=topomap_args, ) + # draw the image plot _imshow_tfr( - ax, - 0, - tmin, - tmax, - vmin, - vmax, - onselect_callback, + ax=axes[ix], + tfr=data[[ix]], + ch_idx=0, + tmin=times[0], + tmax=times[-1], + vmin=vmin, + vmax=vmax, + onselect=_onselect_callback, ylim=None, - tfr=data[idx : idx + 1], - freq=tfr.freqs, + freq=freqs, x_label="Time (s)", y_label="Frequency (Hz)", colorbar=colorbar, @@ -1788,123 +2124,83 @@ def _plot( mask_alpha=mask_alpha, cnorm=cnorm, ) - + # handle title. automatic title is: + # f"{Baselined} {power} ({ch_name})" or + # f"{Baselined} {power} ({combination} of {N} {ch_type}s)" if title == "auto": - if len(tfr.info["ch_names"]) == 1 or combine is None: - subtitle = tfr.info["ch_names"][idx] - else: - subtitle = _set_title_multiple_electrodes( - None, combine, tfr.info["ch_names"], all_=True, ch_type=ch_type + if combine_was_none: # one plot per channel + which_chs = pick_names[ix] + elif len(pick_names) == 1: # there was only one pick anyway + which_chs = pick_names[0] + else: # one plot for all chs combined + which_chs = _set_title_multiple_electrodes( + None, combine, pick_names, all_=True, ch_type=ch_type ) + _prefix = "Power" if baseline is None else "Baselined power" + _title = f"{_prefix} ({which_chs})" else: - subtitle = title - fig.suptitle(subtitle) - + _title = title + _fig.suptitle(_title) plt_show(show) - return [fig for (fig, ax) in figs_and_axes] + return figs @verbose def plot_joint( self, + *, timefreqs=None, picks=None, - baseline=None, - mode="mean", + exclude=(), + combine="mean", tmin=None, tmax=None, fmin=None, fmax=None, + baseline=None, + mode="mean", + dB=False, + yscale="auto", vmin=None, vmax=None, - cmap="RdBu_r", - dB=False, + vlim=(None, None), + cnorm=None, + cmap=None, colorbar=True, + title=None, # TODO consider deprecating this one, or adding an "auto" option show=True, - title=None, - yscale="auto", - combine="mean", - exclude=(), topomap_args=None, image_args=None, verbose=None, ): - """Plot TFRs as a two-dimensional image with topomaps. + """Plot TFRs as a two-dimensional image with topomap highlights. Parameters ---------- - timefreqs : None | list of tuple | dict of tuple - The time-frequency point(s) for which topomaps will be plotted. - See Notes. + %(timefreqs)s %(picks_good_data)s - baseline : None (default) or tuple of length 2 - The time interval to apply baseline correction. - If None do not apply it. If baseline is (a, b) - the interval is between "a (s)" and "b (s)". - If a is None, the beginning of the data is used. - If b is None, then b is set to the end of the interval. - If baseline is equal to (None, None), the entire time - interval is used. - mode : None | str - If str, must be one of 'ratio', 'zscore', 'mean', 'percent', - 'logratio' and 'zlogratio'. - Do baseline correction with ratio (power is divided by mean - power during baseline) or zscore (power is divided by standard - deviation of power during baseline after subtracting the mean, - power = [power - mean(power_baseline)] / std(power_baseline)), - mean simply subtracts the mean power, percent is the same as - applying ratio then mean, logratio is the same as mean but then - rendered in log-scale, zlogratio is the same as zscore but data - is rendered in log-scale first. - If None no baseline correction is applied. - %(tmin_tmax_psd)s - %(fmin_fmax_psd)s - vmin : float | None - The minimum value of the color scale for the image (for - topomaps, see ``topomap_args``). If vmin is None, the data - absolute minimum value is used. - vmax : float | None - The maximum value of the color scale for the image (for - topomaps, see ``topomap_args``). If vmax is None, the data - absolute maximum value is used. - cmap : matplotlib colormap - The colormap to use. - dB : bool - If True, 10*log10 is applied to the data to get dB. - colorbar : bool - If true, colorbar will be added to the plot (relating to the - topomaps). For user defined axes, the colorbar cannot be drawn. - Defaults to True. - show : bool - Call pyplot.show() at the end. - title : str | None - String for title. Defaults to None (blank/no title). - yscale : 'auto' (default) | 'linear' | 'log' - The scale of y (frequency) axis. 'linear' gives linear y axis, - 'log' leads to log-spaced y axis and 'auto' detects if frequencies - are log-spaced and only then sets the y axis to 'log'. - combine : 'mean' | 'rms' | callable - Type of aggregation to perform across selected channels. If a - function, it must operate on an array of shape - ``(n_channels, n_freqs, n_times)`` and return an array of shape - ``(n_freqs, n_times)``. + %(exclude_psd)s + Default is an empty :class:`tuple` which includes all channels. + %(combine_tfr_plot_joint)s .. versionchanged:: 1.3 - Added support for ``callable``. - exclude : list of str | 'bads' - Channels names to exclude from being shown. If 'bads', the - bad channels are excluded. Defaults to an empty list, i.e., ``[]``. - topomap_args : None | dict - A dict of ``kwargs`` that are forwarded to - :func:`mne.viz.plot_topomap` to style the topomaps. ``axes`` and - ``show`` are ignored. If ``times`` is not in this dict, automatic - peak detection is used. Beyond that, if ``None``, no customizable - arguments will be passed. - Defaults to ``None``. - image_args : None | dict - A dict of ``kwargs`` that are forwarded to :meth:`AverageTFR.plot` - to style the image. ``axes`` and ``show`` are ignored. Beyond that, - if ``None``, no customizable arguments will be passed. - Defaults to ``None``. + Added support for ``callable``. + %(tmin_tmax_psd)s + %(fmin_fmax_tfr)s + %(baseline_rescale)s + + How baseline is computed is determined by the ``mode`` parameter. + %(mode_tfr_plot)s + %(dB_tfr_plot_topo)s + %(yscale_tfr_plot)s + %(vmin_vmax_tfr_plot)s + %(vlim_tfr_plot_joint)s + %(cnorm)s + %(cmap_tfr_plot_topo)s + %(colorbar_tfr_plot_joint)s + %(title_none)s + %(show)s + %(topomap_args)s + %(image_args)s %(verbose)s Returns @@ -1914,68 +2210,37 @@ def plot_joint( Notes ----- - ``timefreqs`` has three different modes: tuples, dicts, and auto. - For (list of) tuple(s) mode, each tuple defines a pair - (time, frequency) in s and Hz on the TFR plot. For example, to - look at 10 Hz activity 1 second into the epoch and 3 Hz activity - 300 msec into the epoch, :: - - timefreqs=((1, 10), (.3, 3)) - - If provided as a dictionary, (time, frequency) tuples are keys and - (time_window, frequency_window) tuples are the values - indicating the - width of the windows (centered on the time and frequency indicated by - the key) to be averaged over. For example, :: - - timefreqs={(1, 10): (0.1, 2)} - - would translate into a window that spans 0.95 to 1.05 seconds, as - well as 9 to 11 Hz. If None, a single topomap will be plotted at the - absolute peak across the time-frequency representation. + %(notes_timefreqs_tfr_plot_joint)s .. versionadded:: 0.16.0 - """ # noqa: E501 + """ + from matplotlib import ticker from matplotlib.patches import ConnectionPatch - ##################################### - # Handle channels (picks and types) # - ##################################### - - # it would be nicer to let this happen in self._plot, - # but we need it here to do the loop over the remaining channel - # types in case a user supplies `picks` that pre-select only one - # channel type. - # Nonetheless, it should be refactored for code reuse. - copy = any(var is not None for var in (exclude, picks, baseline)) - tfr = self - if copy: - tfr = tfr.copy() - picks = "data" if picks is None else picks - tfr.pick(picks, exclude=() if exclude is None else exclude) - del picks - ch_types = tfr.info.get_channel_types(unique=True) - - # if multiple sensor types: one plot per channel type, recursive call - if len(ch_types) > 1: - logger.info( - "Multiple channel types selected, returning one " "figure per type." - ) + # deprecations + vlim = _warn_deprecated_vmin_vmax(vlim, vmin, vmax) + # handle recursion + picks = _picks_to_idx( + self.info, picks, "data_or_ica", exclude=exclude, with_ref_meg=False + ) + all_ch_types = np.array(self.get_channel_types()) + uniq_ch_types = sorted(set(all_ch_types[picks])) + if len(uniq_ch_types) > 1: + msg = "Multiple channel types selected, returning one figure per type." + logger.info(msg) figs = list() - for this_type in ch_types: # pick corresponding channel type - type_picks = [ - idx - for idx in range(tfr.info["nchan"]) - if channel_type(tfr.info, idx) == this_type - ] - tf_ = tfr.copy().pick(type_picks) - if len(tf_.info.get_channel_types(unique=True)) > 1: - raise RuntimeError( - "Possibly infinite loop due to channel selection " - "problem. This should never happen! Please check " - "your channel types." - ) + for this_type in uniq_ch_types: + this_picks = np.intersect1d( + picks, + np.nonzero(np.isin(all_ch_types, this_type))[0], + assume_unique=True, + ) + # TODO might be nice to not "copy first, then pick"; alternative might + # be to subset the data with `this_picks` and then construct the "copy" + # using __getstate__ and __setstate__ + _tfr = self.copy().pick(this_picks) figs.append( - tf_.plot_joint( + _tfr.plot_joint( timefreqs=timefreqs, picks=None, baseline=baseline, @@ -1984,8 +2249,7 @@ def plot_joint( tmax=tmax, fmin=fmin, fmax=fmax, - vmin=vmin, - vmax=vmax, + vlim=vlim, cmap=cmap, dB=dB, colorbar=colorbar, @@ -1993,205 +2257,181 @@ def plot_joint( title=title, yscale=yscale, combine=combine, - exclude=None, + exclude=(), topomap_args=topomap_args, verbose=verbose, ) ) return figs else: - ch_type = ch_types.pop() - - # Handle timefreqs - timefreqs = _get_timefreqs(tfr, timefreqs) - n_timefreqs = len(timefreqs) - - if topomap_args is None: - topomap_args = dict() - topomap_args_pass = { - k: v - for k, v in topomap_args.items() - if k not in ("axes", "show", "colorbar") - } - topomap_args_pass["outlines"] = topomap_args.get("outlines", "head") - topomap_args_pass["contours"] = topomap_args.get("contours", 6) - topomap_args_pass["ch_type"] = ch_type - - ############## - # Image plot # - ############## - - fig, tf_ax, map_ax = _prepare_joint_axes(n_timefreqs) - - cmap = _setup_cmap(cmap) - - # image plot - # we also use this to baseline and truncate (times and freqs) - # (a copy of) the instance - if image_args is None: - image_args = dict() - fig = tfr._plot( - picks=None, - baseline=baseline, - mode=mode, + ch_type = uniq_ch_types[0] + + # handle defaults + _validate_type(combine, ("str", "callable"), item_name="combine") # no `None` + image_args = dict() if image_args is None else image_args + topomap_args = dict() if topomap_args is None else topomap_args.copy() + # make sure if topomap_args["ch_type"] is set, it matches what is in `self.info` + topomap_args.setdefault("ch_type", ch_type) + if topomap_args["ch_type"] != ch_type: + raise ValueError( + f"topomap_args['ch_type'] is {topomap_args['ch_type']} which does not " + f"match the channel type present in the object ({ch_type})." + ) + # some necessary defaults + topomap_args.setdefault("outlines", "head") + topomap_args.setdefault("contours", 6) + # don't pass these: + topomap_args.pop("axes", None) + topomap_args.pop("show", None) + topomap_args.pop("colorbar", None) + + # get the time/freq limits of the image plot, to make sure requested annotation + # times/freqs are in range + _, times, freqs = self.get_data( + picks=picks, + exclude=(), tmin=tmin, tmax=tmax, fmin=fmin, fmax=fmax, - vmin=vmin, - vmax=vmax, - cmap=cmap, + return_times=True, + return_freqs=True, + ) + # validate requested annotation times and freqs + timefreqs = _get_timefreqs(self, timefreqs) + valid_timefreqs = dict() + while timefreqs: + (_time, _freq), (t_win, f_win) = timefreqs.popitem() + # convert to half-windows + t_win /= 2 + f_win /= 2 + # make sure the times / freqs are in-bounds + msg = ( + "Requested {} exceeds the range of the data ({}). Choose different " + "`timefreqs`." + ) + if (times > _time).all() or (times < _time).all(): + _var = f"time point ({_time:0.3f} s)" + _range = f"{times[0]:0.3f} - {times[-1]:0.3f} s" + raise ValueError(msg.format(_var, _range)) + elif (freqs > _freq).all() or (freqs < _freq).all(): + _var = f"frequency ({_freq:0.1f} Hz)" + _range = f"{freqs[0]:0.1f} - {freqs[-1]:0.1f} Hz" + raise ValueError(msg.format(_var, _range)) + # snap the times/freqs to the nearest point we have an estimate for, and + # store the validated points + if t_win == 0: + _time = times[np.argmin(np.abs(times - _time))] + if f_win == 0: + _freq = freqs[np.argmin(np.abs(freqs - _freq))] + valid_timefreqs[(_time, _freq)] = (t_win, f_win) + + # prep data for topomaps (unlike image plot, must include all channels of the + # current ch_type). Don't pass tmin/tmax here (crop later after baselining) + topomap_picks = _picks_to_idx(self.info, ch_type) + data, times, freqs = self.get_data( + picks=topomap_picks, exclude=(), return_times=True, return_freqs=True + ) + # merge grads before baselining (makes ERDS visible) + info = pick_info(self.info, sel=topomap_picks, copy=True) + data, pos = _merge_if_grads( + data=data, + info=info, + ch_type=ch_type, + sphere=topomap_args.get("sphere"), + combine=combine, + ) + # loop over intended topomap locations, to find one vlim that works for all. + tf_array = np.array(list(valid_timefreqs)) # each row is [time, freq] + tf_array = tf_array[tf_array[:, 0].argsort()] # sort by time + _vmin, _vmax = (np.inf, -np.inf) + topomap_arrays = list() + topomap_titles = list() + for _time, _freq in tf_array: + # reduce data to the range of interest in the TF plane (i.e., finally crop) + t_win, f_win = valid_timefreqs[(_time, _freq)] + _tmin, _tmax = np.array([-1, 1]) * t_win + _time + _fmin, _fmax = np.array([-1, 1]) * f_win + _freq + _data, *_ = _prep_data_for_plot( + data, + times, + freqs, + tmin=_tmin, + tmax=_tmax, + fmin=_fmin, + fmax=_fmax, + baseline=baseline, + mode=mode, + verbose=verbose, + ) + _data = _data.mean(axis=(-1, -2)) # avg over times and freqs + topomap_arrays.append(_data) + _vmin = min(_data.min(), _vmin) + _vmax = max(_data.max(), _vmax) + # construct topopmap subplot title + t_pm = "" if t_win == 0 else f" ± {t_win:0.2f}" + f_pm = "" if f_win == 0 else f" ± {f_win:0.1f}" + _title = f"{_time:0.2f}{t_pm} s,\n{_freq:0.1f}{f_pm} Hz" + topomap_titles.append(_title) + # handle cmap. Power may be negative depending on baseline strategy so set + # `norm` empirically. vmin/vmax will be handled separately within the `plot()` + # call for the image plot. + norm = np.min(topomap_arrays) >= 0.0 + cmap = _setup_cmap(cmap, norm=norm) + topomap_args.setdefault("cmap", cmap[0]) # prevent interactive cbar + # finalize topomap vlims and compute contour locations. + # By passing `data=None` here ↓↓↓↓ we effectively assert vmin & vmax aren't None + _vlim = _setup_vmin_vmax(data=None, vmin=_vmin, vmax=_vmax, norm=norm) + topomap_args.setdefault("vlim", _vlim) + locator, topomap_args["contours"] = _set_contour_locator( + *topomap_args["vlim"], topomap_args["contours"] + ) + # initialize figure and do the image plot. `self.plot()` needed to wait to be + # called until after `topomap_args` was fully populated --- we don't pass the + # dict through to `self.plot()` explicitly here, but we do "reach back" and get + # it if it's needed by the interactive rectangle selector. + fig, image_ax, topomap_axes = _prepare_joint_axes(len(valid_timefreqs)) + fig = self.plot( + picks=picks, + exclude=(), + tmin=tmin, + tmax=tmax, + fmin=fmin, + fmax=fmax, + baseline=baseline, + mode=mode, dB=dB, + combine=combine, + yscale=yscale, + vlim=vlim, + cnorm=cnorm, + cmap=cmap, colorbar=False, - show=False, title=title, - axes=tf_ax, - yscale=yscale, - combine=combine, - exclude=None, - copy=False, - source_plot_joint=True, - topomap_args=topomap_args_pass, - ch_type=ch_type, + # mask, mask_style, mask_cmap, mask_alpha + axes=image_ax, + show=False, + verbose=verbose, **image_args, - )[0] - - # set and check time and freq limits ... - # can only do this after the tfr plot because it may change these - # parameters - tmax, tmin = tfr.times.max(), tfr.times.min() - fmax, fmin = tfr.freqs.max(), tfr.freqs.min() - for time, freq in timefreqs.keys(): - if not (tmin <= time <= tmax): - error_value = "time point (" + str(time) + " s)" - elif not (fmin <= freq <= fmax): - error_value = "frequency (" + str(freq) + " Hz)" - else: - continue - raise ValueError( - "Requested " + error_value + " exceeds the range" - "of the data. Choose different `timefreqs`." - ) - - ############ - # Topomaps # - ############ - - titles, all_data, all_pos, vlims = [], [], [], [] - - # the structure here is a bit complicated to allow aggregating vlims - # over all topomaps. First, one loop over all timefreqs to collect - # vlims. Then, find the max vlims and in a second loop over timefreqs, - # do the actual plotting. - timefreqs_array = np.array([np.array(keys) for keys in timefreqs]) - order = timefreqs_array[:, 0].argsort() # sort by time - - for ii, (time, freq) in enumerate(timefreqs_array[order]): - avg = timefreqs[(time, freq)] - # set up symmetric windows - time_half_range, freq_half_range = avg / 2.0 - - if time_half_range == 0: - time = tfr.times[np.argmin(np.abs(tfr.times - time))] - if freq_half_range == 0: - freq = tfr.freqs[np.argmin(np.abs(tfr.freqs - freq))] - - if (time_half_range == 0) and (freq_half_range == 0): - sub_map_title = f"({time:.2f} s,\n{freq:.1f} Hz)" - else: - sub_map_title = ( - f"({time:.1f} \u00b1 {time_half_range:.1f} " - f"s,\n{freq:.1f} \u00b1 {freq_half_range:.1f} Hz)" - ) - - tmin = time - time_half_range - tmax = time + time_half_range - fmin = freq - freq_half_range - fmax = freq + freq_half_range - - data = tfr.data - - # merging grads here before rescaling makes ERDs visible - - sphere = topomap_args.get("sphere") - if ch_type == "grad": - picks = _pair_grad_sensors(tfr.info, topomap_coords=False) - pos = _find_topomap_coords(tfr.info, picks=picks[::2], sphere=sphere) - method = combine if isinstance(combine, str) else "rms" - data, _ = _merge_ch_data(data[picks], ch_type, [], method=method) - del picks, method - else: - pos, _ = _get_pos_outlines(tfr.info, None, sphere) - del sphere - - all_pos.append(pos) - - data, times, freqs, _, _ = _preproc_tfr( - data, - tfr.times, - tfr.freqs, - tmin, - tmax, - fmin, - fmax, - mode, - baseline, - vmin, - vmax, - None, - tfr.info["sfreq"], - ) - - vlims.append(np.abs(data).max()) - titles.append(sub_map_title) - all_data.append(data) - new_t = tfr.times[np.abs(tfr.times - np.median([times])).argmin()] - new_f = tfr.freqs[np.abs(tfr.freqs - np.median([freqs])).argmin()] - timefreqs_array[ii] = (new_t, new_f) - - # passing args to the topomap calls - max_lim = max(vlims) - _vlim = list(topomap_args.get("vlim", (None, None))) - # fall back on ± max_lim - for sign, index in zip((-1, 1), (0, 1)): - if _vlim[index] is None: - _vlim[index] = sign * max_lim - topomap_args_pass["vlim"] = tuple(_vlim) - locator, contours = _set_contour_locator(*_vlim, topomap_args_pass["contours"]) - topomap_args_pass["contours"] = contours - - for ax, title, data, pos in zip(map_ax, titles, all_data, all_pos): + )[0] # [0] because `.plot()` always returns a list + # now, actually plot the topomaps + for ax, title, _data in zip(topomap_axes, topomap_titles, topomap_arrays): ax.set_title(title) - plot_topomap( - data.mean(axis=(-1, -2)), - pos, - cmap=cmap[0], - axes=ax, - show=False, - **topomap_args_pass, - ) - - ############# - # Finish up # - ############# + plot_topomap(_data, pos, axes=ax, show=False, **topomap_args) + # draw colorbar if colorbar: - from matplotlib import ticker - cbar = fig.colorbar(ax.images[0]) - if locator is None: - locator = ticker.MaxNLocator(nbins=5) - cbar.locator = locator + cbar.locator = ticker.MaxNLocator(nbins=5) if locator is None else locator cbar.update_ticks() - - # draw the connection lines between time series and topoplots - for (time_, freq_), map_ax_ in zip(timefreqs_array, map_ax): + # draw the connection lines between time-frequency image and topoplots + for (time_, freq_), topo_ax in zip(tf_array, topomap_axes): con = ConnectionPatch( xyA=[time_, freq_], xyB=[0.5, 0], coordsA="data", coordsB="axes fraction", - axesA=tf_ax, - axesB=map_ax_, + axesA=image_ax, + axesB=topo_ax, color="grey", linestyle="-", linewidth=1.5, @@ -2204,108 +2444,6 @@ def plot_joint( plt_show(show) return fig - @verbose - def _onselect( - self, - eclick, - erelease, - baseline=None, - mode=None, - cmap=None, - source_plot_joint=False, - topomap_args=None, - verbose=None, - ): - """Handle rubber band selector in channel tfr.""" - if abs(eclick.x - erelease.x) < 0.1 or abs(eclick.y - erelease.y) < 0.1: - return - tmin = round(min(eclick.xdata, erelease.xdata), 5) # s - tmax = round(max(eclick.xdata, erelease.xdata), 5) - fmin = round(min(eclick.ydata, erelease.ydata), 5) # Hz - fmax = round(max(eclick.ydata, erelease.ydata), 5) - tmin = min(self.times, key=lambda x: abs(x - tmin)) # find closest - tmax = min(self.times, key=lambda x: abs(x - tmax)) - fmin = min(self.freqs, key=lambda x: abs(x - fmin)) - fmax = min(self.freqs, key=lambda x: abs(x - fmax)) - if tmin == tmax or fmin == fmax: - logger.info( - "The selected area is too small. " - "Select a larger time-frequency window." - ) - return - - types = list() - if "eeg" in self: - types.append("eeg") - if "mag" in self: - types.append("mag") - if "grad" in self: - if ( - len( - _pair_grad_sensors( - self.info, topomap_coords=False, raise_error=False - ) - ) - >= 2 - ): - types.append("grad") - elif len(types) == 0: - return # Don't draw a figure for nothing. - - fig = figure_nobar() - fig.suptitle( - f"{tmin:.2f} s - {tmax:.2f} s, {fmin:.2f} Hz - {fmax:.2f} Hz", - y=0.04, - ) - - if source_plot_joint: - ax = fig.add_subplot(111) - data = _preproc_tfr( - self.data, - self.times, - self.freqs, - tmin, - tmax, - fmin, - fmax, - None, - None, - None, - None, - None, - self.info["sfreq"], - )[0] - data = data.mean(-1).mean(-1) - vmax = np.abs(data).max() - im, _ = plot_topomap( - data, - self.info, - vlim=(-vmax, vmax), - cmap=cmap[0], - axes=ax, - show=False, - **topomap_args, - ) - _add_colorbar(ax, im, cmap, title="AU", pad=0.1) - fig.show() - else: - for idx, ch_type in enumerate(types): - ax = fig.add_subplot(1, len(types), idx + 1) - plot_tfr_topomap( - self, - ch_type=ch_type, - tmin=tmin, - tmax=tmax, - fmin=fmin, - fmax=fmax, - baseline=baseline, - mode=mode, - cmap=None, - vlim=(None, None), - axes=ax, - ) - ax.set_title(ch_type) - @verbose def plot_topo( self, @@ -2316,11 +2454,11 @@ def plot_topo( tmax=None, fmin=None, fmax=None, - vmin=None, + vmin=None, # TODO deprecate in favor of `vlim` (needs helper func refactor) vmax=None, layout=None, cmap="RdBu_r", - title=None, + title=None, # don't deprecate; topo titles aren't standard (color, size, just.) dB=False, colorbar=True, layout_scale=0.945, @@ -2332,88 +2470,38 @@ def plot_topo( yscale="auto", verbose=None, ): - """Plot TFRs in a topography with images. + """Plot a TFR image for each channel in a sensor layout arrangement. Parameters ---------- %(picks_good_data)s - baseline : None (default) or tuple of length 2 - The time interval to apply baseline correction. - If None do not apply it. If baseline is (a, b) - the interval is between "a (s)" and "b (s)". - If a is None the beginning of the data is used - and if b is None then b is set to the end of the interval. - If baseline is equal to (None, None) all the time - interval is used. - mode : 'mean' | 'ratio' | 'logratio' | 'percent' | 'zscore' | 'zlogratio' - Perform baseline correction by - - - subtracting the mean of baseline values ('mean') - - dividing by the mean of baseline values ('ratio') - - dividing by the mean of baseline values and taking the log - ('logratio') - - subtracting the mean of baseline values followed by dividing by - the mean of baseline values ('percent') - - subtracting the mean of baseline values and dividing by the - standard deviation of baseline values ('zscore') - - dividing by the mean of baseline values, taking the log, and - dividing by the standard deviation of log baseline values - ('zlogratio') + %(baseline_rescale)s - tmin : None | float - The first time instant to display. If None the first time point - available is used. - tmax : None | float - The last time instant to display. If None the last time point - available is used. - fmin : None | float - The first frequency to display. If None the first frequency - available is used. - fmax : None | float - The last frequency to display. If None the last frequency - available is used. - vmin : float | None - The minimum value of the color scale. If vmin is None, the data - minimum value is used. - vmax : float | None - The maximum value of the color scale. If vmax is None, the data - maximum value is used. - layout : Layout | None - Layout instance specifying sensor positions. If possible, the - correct layout is inferred from the data. - cmap : matplotlib colormap | str - The colormap to use. Defaults to 'RdBu_r'. - title : str - Title of the figure. - dB : bool - If True, 10*log10 is applied to the data to get dB. - colorbar : bool - If true, colorbar will be added to the plot. - layout_scale : float - Scaling factor for adjusting the relative size of the layout - on the canvas. - show : bool - Call pyplot.show() at the end. - border : str - Matplotlib borders style to be used for each sensor plot. - fig_facecolor : color - The figure face color. Defaults to black. - fig_background : None | array - A background image for the figure. This must be a valid input to - `matplotlib.pyplot.imshow`. Defaults to None. - font_color : color - The color of tick labels in the colorbar. Defaults to white. - yscale : 'auto' (default) | 'linear' | 'log' - The scale of y (frequency) axis. 'linear' gives linear y axis, - 'log' leads to log-spaced y axis and 'auto' detects if frequencies - are log-spaced and only then sets the y axis to 'log'. + How baseline is computed is determined by the ``mode`` parameter. + %(mode_tfr_plot)s + %(tmin_tmax_psd)s + %(fmin_fmax_tfr)s + %(vmin_vmax_tfr_plot_topo)s + %(layout_spectrum_plot_topo)s + %(cmap_tfr_plot_topo)s + %(title_none)s + %(dB_tfr_plot_topo)s + %(colorbar)s + %(layout_scale)s + %(show)s + %(border_topo)s + %(fig_facecolor)s + %(fig_background)s + %(font_color)s + %(yscale_tfr_plot)s %(verbose)s Returns ------- fig : matplotlib.figure.Figure The figure containing the topography. - """ # noqa: E501 + """ + # convenience vars times = self.times.copy() freqs = self.freqs data = self.data @@ -2422,6 +2510,8 @@ def plot_topo( info, data = _prepare_picks(info, data, picks, axis=0) del picks + # TODO this is the only remaining call to _preproc_tfr; should be refactored + # (to use _prep_data_for_plot?) data, times, freqs, vmin, vmax = _preproc_tfr( data, times, @@ -2548,160 +2638,1138 @@ def plot_topomap( show=show, ) - def _check_compat(self, tfr): - """Check that self and tfr have the same time-frequency ranges.""" - assert np.all(tfr.times == self.times) - assert np.all(tfr.freqs == self.freqs) - - def __add__(self, tfr): # noqa: D105 - """Add instances.""" - self._check_compat(tfr) - out = self.copy() - out.data += tfr.data - return out - - def __iadd__(self, tfr): # noqa: D105 - self._check_compat(tfr) - self.data += tfr.data - return self + @verbose + def save(self, fname, *, overwrite=False, verbose=None): + """Save time-frequency data to disk (in HDF5 format). - def __sub__(self, tfr): # noqa: D105 - """Subtract instances.""" - self._check_compat(tfr) - out = self.copy() - out.data -= tfr.data - return out + Parameters + ---------- + fname : path-like + Path of file to save to. + %(overwrite)s + %(verbose)s - def __isub__(self, tfr): # noqa: D105 - self._check_compat(tfr) - self.data -= tfr.data - return self + See Also + -------- + mne.time_frequency.read_spectrum + """ + _, write_hdf5 = _import_h5io_funcs() + check_fname(fname, "time-frequency object", (".h5", ".hdf5")) + fname = _check_fname(fname, overwrite=overwrite, verbose=verbose) + out = self.__getstate__() + if "metadata" in out: + out["metadata"] = _prepare_write_metadata(out["metadata"]) + write_hdf5(fname, out, overwrite=overwrite, title="mnepython", slash="replace") - def __truediv__(self, a): # noqa: D105 - """Divide instances.""" - out = self.copy() - out /= a - return out + @verbose + def to_data_frame( + self, + picks=None, + index=None, + long_format=False, + time_format=None, + *, + verbose=None, + ): + """Export data in tabular structure as a pandas DataFrame. - def __itruediv__(self, a): # noqa: D105 - self.data /= a - return self + Channels are converted to columns in the DataFrame. By default, + additional columns ``'time'``, ``'freq'``, ``'epoch'``, and + ``'condition'`` (epoch event description) are added, unless ``index`` + is not ``None`` (in which case the columns specified in ``index`` will + be used to form the DataFrame's index instead). ``'epoch'``, and + ``'condition'`` are not supported for ``AverageTFR``. - def __mul__(self, a): - """Multiply source instances.""" - out = self.copy() - out *= a - return out + Parameters + ---------- + %(picks_all)s + %(index_df_epo)s + Valid string values are ``'time'``, ``'freq'``, ``'epoch'``, and + ``'condition'`` for ``EpochsTFR`` and ``'time'`` and ``'freq'`` + for ``AverageTFR``. + Defaults to ``None``. + %(long_format_df_epo)s + %(time_format_df)s - def __imul__(self, a): # noqa: D105 - self.data *= a - return self + .. versionadded:: 0.23 + %(verbose)s - def __repr__(self): # noqa: D105 - s = f"time : [{self.times[0]}, {self.times[-1]}]" - s += f", freq : [{self.freqs[0]}, {self.freqs[-1]}]" - s += ", nave : %d" % self.nave - s += ", channels : %d" % self.data.shape[0] - s += f", ~{sizeof_fmt(self._size)}" - return "" % s + Returns + ------- + %(df_return)s + """ + # check pandas once here, instead of in each private utils function + pd = _check_pandas_installed() # noqa + # arg checking + valid_index_args = ["time", "freq"] + if isinstance(self, EpochsTFR): + valid_index_args.extend(["epoch", "condition"]) + valid_time_formats = ["ms", "timedelta"] + index = _check_pandas_index_arguments(index, valid_index_args) + time_format = _check_time_format(time_format, valid_time_formats) + # get data + picks = _picks_to_idx(self.info, picks, "all", exclude=()) + data, times, freqs = self.get_data(picks, return_times=True, return_freqs=True) + axis = self._dims.index("channel") + if not isinstance(self, EpochsTFR): + data = data[np.newaxis] # add singleton "epochs" axis + axis += 1 + n_epochs, n_picks, n_freqs, n_times = data.shape + # reshape to (epochs*freqs*times) x signals + data = np.moveaxis(data, axis, -1) + data = data.reshape(n_epochs * n_freqs * n_times, n_picks) + # prepare extra columns / multiindex + mindex = list() + times = _convert_times(times, time_format, self.info["meas_date"]) + times = np.tile(times, n_epochs * n_freqs) + freqs = np.tile(np.repeat(freqs, n_times), n_epochs) + mindex.append(("time", times)) + mindex.append(("freq", freqs)) + if isinstance(self, EpochsTFR): + mindex.append(("epoch", np.repeat(self.selection, n_times * n_freqs))) + rev_event_id = {v: k for k, v in self.event_id.items()} + conditions = [rev_event_id[k] for k in self.events[:, 2]] + mindex.append(("condition", np.repeat(conditions, n_times * n_freqs))) + assert all(len(mdx) == len(mindex[0]) for mdx in mindex[1:]) + # build DataFrame + if isinstance(self, EpochsTFR): + default_index = ["condition", "epoch", "freq", "time"] + else: + default_index = ["freq", "time"] + df = _build_data_frame( + self, data, picks, long_format, mindex, index, default_index=default_index + ) + return df + + +@fill_doc +class AverageTFR(BaseTFR): + """Data object for spectrotemporal representations of averaged data. + + .. warning:: The preferred means of creating AverageTFR objects is via the + instance methods :meth:`mne.Epochs.compute_tfr` and + :meth:`mne.Evoked.compute_tfr`, or via + :meth:`mne.time_frequency.EpochsTFR.average`. Direct class + instantiation is discouraged. + + Parameters + ---------- + %(info_not_none)s + + .. deprecated:: 1.7 + Pass an instance of :class:`~mne.Epochs` or :class:`~mne.Evoked` instead, or + use :class:`~mne.time_frequency.AverageTFRArray` which retains the old API. + data : ndarray, shape (n_channels, n_freqs, n_times) + The data. + + .. deprecated:: 1.7 + Pass an instance of :class:`~mne.Epochs` or :class:`~mne.Evoked` instead, or + use :class:`~mne.time_frequency.AverageTFRArray` which retains the old API. + times : ndarray, shape (n_times,) + The time values in seconds. + + .. deprecated:: 1.7 + Pass an instance of :class:`~mne.Epochs` or :class:`~mne.Evoked` instead and + (optionally) use ``tmin`` and ``tmax`` to restrict the time domain; or use + :class:`~mne.time_frequency.AverageTFRArray` which retains the old API. + freqs : ndarray, shape (n_freqs,) + The frequencies in Hz. + nave : int + The number of averaged TFRs. + + .. deprecated:: 1.7 + Pass an instance of :class:`~mne.Epochs` or :class:`~mne.Evoked` instead; + ``nave`` will be inferred automatically. Or, use + :class:`~mne.time_frequency.AverageTFRArray` which retains the old API. + inst : instance of Evoked | instance of Epochs | dict + The data from which to compute the time-frequency representation. Passing a + :class:`dict` will create the AverageTFR using the ``__setstate__`` interface + and is not recommended for typical use cases. + %(method_tfr)s + %(freqs_tfr)s + %(tmin_tmax_psd)s + %(picks_good_data_noref)s + %(proj_psd)s + %(decim_tfr)s + %(comment_averagetfr)s + %(n_jobs)s + %(verbose)s + %(method_kw_tfr)s + + Attributes + ---------- + %(baseline_tfr_attr)s + %(ch_names_tfr_attr)s + %(comment_averagetfr_attr)s + %(freqs_tfr_attr)s + %(info_not_none)s + %(method_tfr_attr)s + %(nave_tfr_attr)s + %(sfreq_tfr_attr)s + %(shape_tfr_attr)s + + See Also + -------- + RawTFR + EpochsTFR + AverageTFRArray + mne.Evoked.compute_tfr + mne.time_frequency.EpochsTFR.average + + Notes + ----- + The old API (prior to version 1.7) was:: + + AverageTFR(info, data, times, freqs, nave, comment=None, method=None) + + That API is still available via :class:`~mne.time_frequency.AverageTFRArray` for + cases where the data are precomputed or do not originate from MNE-Python objects. + The preferred new API uses instance methods:: + + evoked.compute_tfr(method, freqs, ...) + epochs.compute_tfr(method, freqs, average=True, ...) + + The new API also supports AverageTFR instantiation from a :class:`dict`, but this + is primarily for save/load and internal purposes, and wraps ``__setstate__``. + During the transition from the old to the new API, it may be expedient to use + :class:`~mne.time_frequency.AverageTFRArray` as a "quick-fix" approach to updating + scripts under active development. + + References + ---------- + .. footbibliography:: + """ + + def __init__( + self, + info=None, + data=None, + times=None, + freqs=None, + nave=None, + *, + inst=None, + method=None, + tmin=None, + tmax=None, + picks=None, + proj=False, + decim=1, + comment=None, + n_jobs=None, + verbose=None, + **method_kw, + ): + from ..epochs import BaseEpochs + 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( + inst, (BaseEpochs, Evoked, dict), "object passed to AverageTFR constructor" + ) + # stockwell API is very different from multitaper/morlet + if method == "stockwell" and not isinstance(inst, dict): + if isinstance(freqs, str) and freqs == "auto": + fmin, fmax = None, None + elif len(freqs) == 2: + fmin, fmax = freqs + else: + raise ValueError( + "for Stockwell method, freqs must be a length-2 iterable " + f'or "auto", got {freqs}.' + ) + method_kw.update(fmin=fmin, fmax=fmax) + # Compute freqs. We need a couple lines of code dupe here (also in + # BaseTFR.__init__) to get the subset of times to pass to _check_input_st() + _mask = _time_mask(inst.times, tmin, tmax, sfreq=inst.info["sfreq"]) + _times = inst.times[_mask].copy() + _, default_nfft, _ = _check_input_st(_times, None) + n_fft = method_kw.get("n_fft", default_nfft) + *_, freqs = _compute_freqs_st(fmin, fmax, n_fft, inst.info["sfreq"]) + + # use Evoked.comment or str(Epochs.event_id) as the default comment... + if comment is None: + comment = getattr(inst, "comment", ",".join(getattr(inst, "event_id", ""))) + # ...but don't overwrite if it's coming in with a comment already set + if isinstance(inst, dict): + inst.setdefault("comment", comment) + else: + self._comment = getattr(self, "_comment", comment) + super().__init__( + inst, + method, + freqs, + tmin=tmin, + tmax=tmax, + picks=picks, + proj=proj, + decim=decim, + n_jobs=n_jobs, + verbose=verbose, + **method_kw, + ) + + def __getstate__(self): + """Prepare AverageTFR object for serialization.""" + out = super().__getstate__() + out.update(nave=self.nave, comment=self.comment) + # NOTE: self._itc should never exist in the instance returned to the user; it + # is temporarily present in the output from the tfr_array_* function, and is + # split out into a separate AverageTFR object (and deleted from the object + # holding power estimates) before those objects are passed back to the user. + # The following lines are there because we make use of __getstate__ to achieve + # that splitting of objects. + if hasattr(self, "_itc"): + out.update(itc=self._itc) + return out + + def __setstate__(self, state): + """Unpack AverageTFR from serialized format.""" + super().__setstate__(state) + self._comment = state.get("comment", "") + self._nave = state.get("nave", 1) + + @property + def comment(self): + return self._comment + + @comment.setter + def comment(self, comment): + self._comment = comment + + @property + def nave(self): + return self._nave + + @nave.setter + def nave(self, nave): + self._nave = nave + + def _get_instance_data(self, time_mask): + # AverageTFRs can be constructed from Epochs data, so we triage shape here. + # Evoked data get a fake singleton "epoch" axis prepended + dim = slice(None) if _get_instance_type_string(self) == "Epochs" else np.newaxis + data = self.inst.get_data(picks=self._picks)[dim, :, time_mask] + self._nave = getattr(self.inst, "nave", data.shape[0]) + return data + + +@fill_doc +class AverageTFRArray(AverageTFR): + """Data object for *precomputed* spectrotemporal representations of averaged data. + + Parameters + ---------- + %(info_not_none)s + %(data_tfr)s + %(times)s + %(freqs_tfr_array)s + nave : int + The number of averaged TFRs. + %(comment_averagetfr_attr)s + %(method_tfr_array)s + + Attributes + ---------- + %(baseline_tfr_attr)s + %(ch_names_tfr_attr)s + %(comment_averagetfr_attr)s + %(freqs_tfr_attr)s + %(info_not_none)s + %(method_tfr_attr)s + %(nave_tfr_attr)s + %(sfreq_tfr_attr)s + %(shape_tfr_attr)s + + See Also + -------- + AverageTFR + EpochsTFRArray + mne.Epochs.compute_tfr + mne.Evoked.compute_tfr + """ + + def __init__( + self, info, data, times, freqs, *, nave=None, comment=None, method=None + ): + state = dict(info=info, data=data, times=times, freqs=freqs) + for name, optional in dict(nave=nave, comment=comment, method=method).items(): + if optional is not None: + state[name] = optional + self.__setstate__(state) + + +@fill_doc +class EpochsTFR(BaseTFR, GetEpochsMixin): + """Data object for spectrotemporal representations of epoched data. + + .. important:: + The preferred means of creating EpochsTFR objects from :class:`~mne.Epochs` + objects is via the instance method :meth:`~mne.Epochs.compute_tfr`. + To create an EpochsTFR object from pre-computed data (i.e., a NumPy array) use + :class:`~mne.time_frequency.EpochsTFRArray`. + + Parameters + ---------- + %(info_not_none)s + + .. deprecated:: 1.7 + Pass an instance of :class:`~mne.Epochs` as ``inst`` instead, or use + :class:`~mne.time_frequency.EpochsTFRArray` which retains the old API. + data : ndarray, shape (n_channels, n_freqs, n_times) + The data. + + .. deprecated:: 1.7 + Pass an instance of :class:`~mne.Epochs` as ``inst`` instead, or use + :class:`~mne.time_frequency.EpochsTFRArray` which retains the old API. + times : ndarray, shape (n_times,) + The time values in seconds. + + .. deprecated:: 1.7 + Pass an instance of :class:`~mne.Epochs` as ``inst`` instead and + (optionally) use ``tmin`` and ``tmax`` to restrict the time domain; or use + :class:`~mne.time_frequency.EpochsTFRArray` which retains the old API. + %(freqs_tfr_epochs)s + inst : instance of Epochs + The data from which to compute the time-frequency representation. + %(method_tfr_epochs)s + %(comment_tfr_attr)s + + .. deprecated:: 1.7 + Pass an instance of :class:`~mne.Epochs` as ``inst`` instead, or use + :class:`~mne.time_frequency.EpochsTFRArray` which retains the old API. + %(tmin_tmax_psd)s + %(picks_good_data_noref)s + %(proj_psd)s + %(decim_tfr)s + %(events_epochstfr)s + + .. deprecated:: 1.7 + Pass an instance of :class:`~mne.Epochs` as ``inst`` instead, or use + :class:`~mne.time_frequency.EpochsTFRArray` which retains the old API. + %(event_id_epochstfr)s + + .. deprecated:: 1.7 + Pass an instance of :class:`~mne.Epochs` as ``inst`` instead, or use + :class:`~mne.time_frequency.EpochsTFRArray` which retains the old API. + selection : array + List of indices of selected events (not dropped or ignored etc.). For + example, if the original event array had 4 events and the second event + has been dropped, this attribute would be np.array([0, 2, 3]). + + .. deprecated:: 1.7 + Pass an instance of :class:`~mne.Epochs` as ``inst`` instead, or use + :class:`~mne.time_frequency.EpochsTFRArray` which retains the old API. + drop_log : tuple of tuple + A tuple of the same length as the event array used to initialize the + ``EpochsTFR`` object. If the i-th original event is still part of the + selection, drop_log[i] will be an empty tuple; otherwise it will be + a tuple of the reasons the event is not longer in the selection, e.g.: + + - ``'IGNORED'`` + If it isn't part of the current subset defined by the user + - ``'NO_DATA'`` or ``'TOO_SHORT'`` + If epoch didn't contain enough data names of channels that + exceeded the amplitude threshold + - ``'EQUALIZED_COUNTS'`` + See :meth:`~mne.Epochs.equalize_event_counts` + - ``'USER'`` + For user-defined reasons (see :meth:`~mne.Epochs.drop`). + + .. deprecated:: 1.7 + Pass an instance of :class:`~mne.Epochs` as ``inst`` instead, or use + :class:`~mne.time_frequency.EpochsTFRArray` which retains the old API. + %(metadata_epochstfr)s + + .. deprecated:: 1.7 + Pass an instance of :class:`~mne.Epochs` as ``inst`` instead, or use + :class:`~mne.time_frequency.EpochsTFRArray` which retains the old API. + %(n_jobs)s + %(verbose)s + %(method_kw_tfr)s + + Attributes + ---------- + %(baseline_tfr_attr)s + %(ch_names_tfr_attr)s + %(comment_tfr_attr)s + %(drop_log)s + %(event_id_attr)s + %(events_attr)s + %(freqs_tfr_attr)s + %(info_not_none)s + %(metadata_attr)s + %(method_tfr_attr)s + %(selection_attr)s + %(sfreq_tfr_attr)s + %(shape_tfr_attr)s + + See Also + -------- + mne.Epochs.compute_tfr + RawTFR + AverageTFR + EpochsTFRArray + + References + ---------- + .. footbibliography:: + """ + + def __init__( + self, + info=None, + data=None, + times=None, + freqs=None, + *, + inst=None, + method=None, + comment=None, + tmin=None, + tmax=None, + picks=None, + proj=False, + decim=1, + events=None, + event_id=None, + selection=None, + drop_log=None, + metadata=None, + n_jobs=None, + verbose=None, + **method_kw, + ): + from ..epochs import BaseEpochs + + # deprecations. TODO remove after 1.7 release + depr_params = dict(info=info, data=data, times=times, comment=comment) + 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 ``EpochsTFRArray`` 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.' + ) + # sensible defaults are created in __setstate__ so only pass these through + # if they're user-specified + optional = dict( + freqs=freqs, + method=method, + events=events, + event_id=event_id, + selection=selection, + drop_log=drop_log, + metadata=metadata, + ) + optional_params = { + key: val for key, val in optional.items() if val is not None + } + inst = depr_params | optional_params + # end TODO ↑↑↑↑↑↑ + + # dict is allowed for __setstate__ compatibility + _validate_type( + inst, (BaseEpochs, dict), "object passed to EpochsTFR constructor", "Epochs" + ) + super().__init__( + inst, + method, + freqs, + tmin=tmin, + tmax=tmax, + picks=picks, + proj=proj, + decim=decim, + n_jobs=n_jobs, + verbose=verbose, + **method_kw, + ) + + @fill_doc + def __getitem__(self, item): + """Subselect epochs from an EpochsTFR. + + Parameters + ---------- + %(item)s + Access options are the same as for :class:`~mne.Epochs` objects, see the + docstring Notes section of :meth:`mne.Epochs.__getitem__` for explanation. + + Returns + ------- + %(getitem_epochstfr_return)s + """ + return super().__getitem__(item) + + def __getstate__(self): + """Prepare EpochsTFR object for serialization.""" + out = super().__getstate__() + out.update( + metadata=self._metadata, + drop_log=self.drop_log, + event_id=self.event_id, + events=self.events, + selection=self.selection, + raw_times=self._raw_times, + ) + return out + + def __setstate__(self, state): + """Unpack EpochsTFR from serialized format.""" + if state["data"].ndim != 4: + raise ValueError(f"EpochsTFR data should be 4D, got {state['data'].ndim}.") + super().__setstate__(state) + self._metadata = state.get("metadata", None) + n_epochs = self.shape[0] + n_times = self.shape[-1] + fake_samps = np.linspace( + n_times, n_times * (n_epochs + 1), n_epochs, dtype=int, endpoint=False + ) + fake_events = np.dstack( + (fake_samps, np.zeros_like(fake_samps), np.ones_like(fake_samps)) + ).squeeze(axis=0) + self.events = state.get("events", _ensure_events(fake_events)) + self.event_id = state.get("event_id", _check_event_id(None, self.events)) + self.drop_log = state.get("drop_log", tuple()) + self.selection = state.get("selection", np.arange(n_epochs)) + self._bad_dropped = True # always true, need for `equalize_event_counts()` + + def __next__(self, return_event_id=False): + """Iterate over EpochsTFR objects. + + NOTE: __iter__() and _stop_iter() are defined by the GetEpochs mixin. + + Parameters + ---------- + return_event_id : bool + If ``True``, return both the EpochsTFR data and its associated ``event_id``. + + Returns + ------- + epoch : array of shape (n_channels, n_freqs, n_times) + The single-epoch time-frequency data. + event_id : int + The integer event id associated with the epoch. Only returned if + ``return_event_id`` is ``True``. + """ + if self._current >= len(self._data): + self._stop_iter() + epoch = self._data[self._current] + event_id = self.events[self._current][-1] + self._current += 1 + if return_event_id: + return epoch, event_id + return epoch + + def _check_singleton(self): + """Check if self contains only one Epoch, and return it as an AverageTFR.""" + if self.shape[0] > 1: + calling_func = inspect.currentframe().f_back.f_code.co_name + raise NotImplementedError( + f"Cannot call {calling_func}() from EpochsTFR with multiple epochs; " + "please subselect a single epoch before plotting." + ) + return list(self.iter_evoked())[0] + + def _get_instance_data(self, time_mask): + return self.inst.get_data(picks=self._picks)[:, :, time_mask] + + def _update_epoch_attributes(self): + # adjust dims and shape + if self.method != "stockwell": # stockwell consumes epochs dimension + self._dims = ("epoch",) + self._dims + self._shape = (len(self.inst),) + self._shape + # we need these for to_data_frame() + self.event_id = self.inst.event_id.copy() + self.events = self.inst.events.copy() + self.selection = self.inst.selection.copy() + # we need these for __getitem__() + self.drop_log = deepcopy(self.inst.drop_log) + self._metadata = self.inst.metadata + # we need this for compatibility with equalize_event_counts() + self._bad_dropped = True + + def average(self, method="mean", *, dim="epochs", copy=False): + """Aggregate the EpochsTFR across epochs, frequencies, or times. + + Parameters + ---------- + method : "mean" | "median" | callable + How to aggregate the data across the given ``dim``. If callable, + must take a :class:`NumPy array` of shape + ``(n_epochs, n_channels, n_freqs, n_times)`` and return an array + with one fewer dimensions (which dimension is collapsed depends on + the value of ``dim``). Default is ``"mean"``. + dim : "epochs" | "freqs" | "times" + The dimension along which to combine the data. + copy : bool + Whether to return a copy of the modified instance, or modify in place. + Ignored when ``dim="epochs"`` or ``"times"`` because those options return + different types (:class:`~mne.time_frequency.AverageTFR` and + :class:`~mne.time_frequency.EpochsSpectrum`, respectively). + + Returns + ------- + tfr : instance of EpochsTFR | AverageTFR | EpochsSpectrum + The aggregated TFR object. + + Notes + ----- + Passing in ``np.median`` is considered unsafe for complex data; pass + the string ``"median"`` instead to compute the *marginal* median + (i.e. the median of the real and imaginary components separately). + See discussion here: + + https://github.com/scipy/scipy/pull/12676#issuecomment-783370228 + """ + _check_option("dim", dim, ("epochs", "freqs", "times")) + axis = self._dims.index(dim[:-1]) # self._dims entries aren't plural + + func = _check_combine(mode=method, axis=axis) + data = func(self.data) + + n_epochs, n_channels, n_freqs, n_times = self.data.shape + freqs, times = self.freqs, self.times + if dim == "epochs": + expected_shape = self._data.shape[1:] + elif dim == "freqs": + expected_shape = (n_epochs, n_channels, n_times) + freqs = np.mean(self.freqs, keepdims=True) + elif dim == "times": + expected_shape = (n_epochs, n_channels, n_freqs) + times = np.mean(self.times, keepdims=True) + + if data.shape != expected_shape: + raise RuntimeError( + "EpochsTFR.average() got a method that resulted in data of shape " + f"{data.shape}, but it should be {expected_shape}." + ) + # restore singleton freqs axis (not necessary for epochs/times: class changes) + if dim == "freqs": + data = np.expand_dims(data, axis=axis) + state = self.__getstate__() + state["data"] = data + state["info"] = deepcopy(self.info) + state["dims"] = (*state["dims"][:axis], *state["dims"][axis + 1 :]) + state["freqs"] = freqs + state["times"] = times + if dim == "epochs": + state["inst_type_str"] = "Evoked" + state["nave"] = n_epochs + state["comment"] = f"{method} of {n_epochs} EpochsTFR{_pl(n_epochs)}" + out = AverageTFR(inst=state) + out._data_type = "Average Power" + return out + + elif dim == "times": + return EpochsSpectrum( + state, + method=None, + fmin=None, + fmax=None, + tmin=None, + tmax=None, + picks=None, + exclude=None, + proj=None, + remove_dc=None, + n_jobs=None, + ) + # ↓↓↓ these two are for dim == "freqs" + elif copy: + return EpochsTFR(inst=state, method=None, freqs=None) + else: + self._data = np.expand_dims(data, axis=axis) + self._freqs = freqs + return self + + @verbose + def drop(self, indices, reason="USER", verbose=None): + """Drop epochs based on indices or boolean mask. + + .. note:: The indices refer to the current set of undropped epochs + rather than the complete set of dropped and undropped epochs. + They are therefore not necessarily consistent with any + external indices (e.g., behavioral logs). To drop epochs + based on external criteria, do not use the ``preload=True`` + flag when constructing an Epochs object, and call this + method before calling the :meth:`mne.Epochs.drop_bad` or + :meth:`mne.Epochs.load_data` methods. + + Parameters + ---------- + indices : array of int or bool + Set epochs to remove by specifying indices to remove or a boolean + mask to apply (where True values get removed). Events are + correspondingly modified. + reason : str + Reason for dropping the epochs ('ECG', 'timeout', 'blink' etc). + Default: 'USER'. + %(verbose)s + + Returns + ------- + epochs : instance of Epochs or EpochsTFR + The epochs with indices dropped. Operates in-place. + """ + from ..epochs import BaseEpochs + + BaseEpochs.drop(self, indices=indices, reason=reason, verbose=verbose) + + return self + + def iter_evoked(self, copy=False): + """Iterate over EpochsTFR to yield a sequence of AverageTFR objects. + + The AverageTFR objects will each contain a single epoch (i.e., no averaging is + performed). This method resets the EpochTFR instance's iteration state to the + first epoch. + + Parameters + ---------- + copy : bool + Whether to yield copies of the data and measurement info, or views/pointers. + """ + self.__iter__() + state = self.__getstate__() + state["inst_type_str"] = "Evoked" + state["dims"] = state["dims"][1:] # drop "epochs" + + while True: + try: + data, event_id = self.__next__(return_event_id=True) + except StopIteration: + break + if copy: + state["info"] = deepcopy(self.info) + state["data"] = data.copy() + else: + state["data"] = data + state["nave"] = 1 + yield AverageTFR(inst=state, method=None, freqs=None, comment=str(event_id)) + + @verbose + @copy_doc(BaseTFR.plot) + def plot( + self, + picks=None, + *, + exclude=(), + tmin=None, + tmax=None, + fmin=None, + fmax=None, + baseline=None, + mode="mean", + dB=False, + combine=None, + layout=None, # TODO deprecate; not used in orig implementation + yscale="auto", + vmin=None, + vmax=None, + vlim=(None, None), + cnorm=None, + cmap=None, + colorbar=True, + title=None, # don't deprecate this one; has (useful) option title="auto" + mask=None, + mask_style=None, + mask_cmap="Greys", + mask_alpha=0.1, + axes=None, + show=True, + verbose=None, + ): + singleton_epoch = self._check_singleton() + return singleton_epoch.plot( + picks=picks, + exclude=exclude, + tmin=tmin, + tmax=tmax, + fmin=fmin, + fmax=fmax, + baseline=baseline, + mode=mode, + dB=dB, + combine=combine, + layout=layout, + yscale=yscale, + vmin=vmin, + vmax=vmax, + vlim=vlim, + cnorm=cnorm, + cmap=cmap, + colorbar=colorbar, + title=title, + mask=mask, + mask_style=mask_style, + mask_cmap=mask_cmap, + mask_alpha=mask_alpha, + axes=axes, + show=show, + verbose=verbose, + ) + + @verbose + @copy_doc(BaseTFR.plot_topo) + def plot_topo( + self, + picks=None, + baseline=None, + mode="mean", + tmin=None, + tmax=None, + fmin=None, + fmax=None, + vmin=None, # TODO deprecate in favor of `vlim` (needs helper func refactor) + vmax=None, + layout=None, + cmap=None, + title=None, # don't deprecate; topo titles aren't standard (color, size, just.) + dB=False, + colorbar=True, + layout_scale=0.945, + show=True, + border="none", + fig_facecolor="k", + fig_background=None, + font_color="w", + yscale="auto", + verbose=None, + ): + singleton_epoch = self._check_singleton() + return singleton_epoch.plot_topo( + picks=picks, + baseline=baseline, + mode=mode, + tmin=tmin, + tmax=tmax, + fmin=fmin, + fmax=fmax, + vmin=vmin, + vmax=vmax, + layout=layout, + cmap=cmap, + title=title, + dB=dB, + colorbar=colorbar, + layout_scale=layout_scale, + show=show, + border=border, + fig_facecolor=fig_facecolor, + fig_background=fig_background, + font_color=font_color, + yscale=yscale, + verbose=verbose, + ) + + @verbose + @copy_doc(BaseTFR.plot_joint) + def plot_joint( + self, + *, + timefreqs=None, + picks=None, + exclude=(), + combine="mean", + tmin=None, + tmax=None, + fmin=None, + fmax=None, + baseline=None, + mode="mean", + dB=False, + yscale="auto", + vmin=None, + vmax=None, + vlim=(None, None), + cnorm=None, + cmap=None, + colorbar=True, + title=None, + show=True, + topomap_args=None, + image_args=None, + verbose=None, + ): + singleton_epoch = self._check_singleton() + return singleton_epoch.plot_joint( + timefreqs=timefreqs, + picks=picks, + exclude=exclude, + combine=combine, + tmin=tmin, + tmax=tmax, + fmin=fmin, + fmax=fmax, + baseline=baseline, + mode=mode, + dB=dB, + yscale=yscale, + vmin=vmin, + vmax=vmax, + vlim=vlim, + cnorm=cnorm, + cmap=cmap, + colorbar=colorbar, + title=title, + show=show, + topomap_args=topomap_args, + image_args=image_args, + verbose=verbose, + ) + + @copy_doc(BaseTFR.plot_topomap) + def plot_topomap( + self, + tmin=None, + tmax=None, + fmin=0.0, + fmax=np.inf, + *, + ch_type=None, + baseline=None, + mode="mean", + sensors=True, + show_names=False, + mask=None, + mask_params=None, + contours=6, + outlines="head", + sphere=None, + image_interp=_INTERPOLATION_DEFAULT, + extrapolate=_EXTRAPOLATE_DEFAULT, + border=_BORDER_DEFAULT, + res=64, + size=2, + cmap=None, + vlim=(None, None), + cnorm=None, + colorbar=True, + cbar_fmt="%1.1e", + units=None, + axes=None, + show=True, + ): + singleton_epoch = self._check_singleton() + return singleton_epoch.plot_topomap( + tmin=tmin, + tmax=tmax, + fmin=fmin, + fmax=fmax, + ch_type=ch_type, + baseline=baseline, + mode=mode, + sensors=sensors, + show_names=show_names, + mask=mask, + mask_params=mask_params, + contours=contours, + outlines=outlines, + sphere=sphere, + image_interp=image_interp, + extrapolate=extrapolate, + border=border, + res=res, + size=size, + cmap=cmap, + vlim=vlim, + cnorm=cnorm, + colorbar=colorbar, + cbar_fmt=cbar_fmt, + units=units, + axes=axes, + show=show, + ) @fill_doc -class EpochsTFR(_BaseTFR, GetEpochsMixin): - """Container for Time-Frequency data on epochs. - - Can for example store induced power at sensor level. +class EpochsTFRArray(EpochsTFR): + """Data object for *precomputed* spectrotemporal representations of epoched data. Parameters ---------- %(info_not_none)s - data : ndarray, shape (n_epochs, n_channels, n_freqs, n_times) - The data. - times : ndarray, shape (n_times,) - The time values in seconds. - freqs : ndarray, shape (n_freqs,) - The frequencies in Hz. - comment : str | None, default None - Comment on the data, e.g., the experimental condition. - method : str | None, default None - Comment on the method used to compute the data, e.g., morlet wavelet. - events : ndarray, shape (n_events, 3) | None - The events as stored in the Epochs class. If None (default), all event - values are set to 1 and event time-samples are set to range(n_epochs). - event_id : dict | None - Example: dict(auditory=1, visual=3). They keys can be used to access - associated events. If None, all events will be used and a dict is - created with string integer names corresponding to the event id - integers. - selection : iterable | None - Iterable of indices of selected epochs. If ``None``, will be - automatically generated, corresponding to all non-zero events. - - .. versionadded:: 0.23 - drop_log : tuple | None - Tuple of tuple of strings indicating which epochs have been marked to - be ignored. - - .. versionadded:: 0.23 - metadata : instance of pandas.DataFrame | None - A :class:`pandas.DataFrame` containing pertinent information for each - trial. See :class:`mne.Epochs` for further details. - %(verbose)s + %(data_tfr)s + %(times)s + %(freqs_tfr_array)s + %(comment_tfr_attr)s + %(method_tfr_array)s + %(events_epochstfr)s + %(event_id_epochstfr)s + %(selection)s + %(drop_log)s + %(metadata_epochstfr)s Attributes ---------- + %(baseline_tfr_attr)s + %(ch_names_tfr_attr)s + %(comment_tfr_attr)s + %(drop_log)s + %(event_id_attr)s + %(events_attr)s + %(freqs_tfr_attr)s %(info_not_none)s - ch_names : list - The names of the channels. - data : ndarray, shape (n_epochs, n_channels, n_freqs, n_times) - The data array. - times : ndarray, shape (n_times,) - The time values in seconds. - freqs : ndarray, shape (n_freqs,) - The frequencies in Hz. - comment : string - Comment on dataset. Can be the condition. - method : str | None, default None - Comment on the method used to compute the data, e.g., morlet wavelet. - events : ndarray, shape (n_events, 3) | None - Array containing sample information as event_id - event_id : dict | None - Names of conditions correspond to event_ids - selection : array - List of indices of selected events (not dropped or ignored etc.). For - example, if the original event array had 4 events and the second event - has been dropped, this attribute would be np.array([0, 2, 3]). - drop_log : tuple of tuple - A tuple of the same length as the event array used to initialize the - ``EpochsTFR`` object. If the i-th original event is still part of the - selection, drop_log[i] will be an empty tuple; otherwise it will be - a tuple of the reasons the event is not longer in the selection, e.g.: - - - ``'IGNORED'`` - If it isn't part of the current subset defined by the user - - ``'NO_DATA'`` or ``'TOO_SHORT'`` - If epoch didn't contain enough data names of channels that - exceeded the amplitude threshold - - ``'EQUALIZED_COUNTS'`` - See :meth:`~mne.Epochs.equalize_event_counts` - - ``'USER'`` - For user-defined reasons (see :meth:`~mne.Epochs.drop`). - - metadata : pandas.DataFrame, shape (n_events, n_cols) | None - DataFrame containing pertinent information for each trial + %(metadata_attr)s + %(method_tfr_attr)s + %(selection_attr)s + %(sfreq_tfr_attr)s + %(shape_tfr_attr)s - Notes - ----- - .. versionadded:: 0.13.0 + See Also + -------- + AverageTFR + mne.Epochs.compute_tfr + mne.Evoked.compute_tfr """ - @verbose def __init__( self, info, data, times, freqs, + *, comment=None, method=None, events=None, @@ -2709,206 +3777,204 @@ def __init__( selection=None, drop_log=None, metadata=None, - verbose=None, ): - super().__init__() - self.info = info - if data.ndim != 4: - raise ValueError("data should be 4d. Got %d." % data.ndim) - n_epochs, n_channels, n_freqs, n_times = data.shape - if n_channels != len(info["chs"]): - raise ValueError( - "Number of channels and data size don't match" - " (%d != %d)." % (n_channels, len(info["chs"])) - ) - if n_freqs != len(freqs): - raise ValueError( - "Number of frequencies and data size don't match" - " (%d != %d)." % (n_freqs, len(freqs)) - ) - if n_times != len(times): - raise ValueError( - "Number of times and data size don't match" - " (%d != %d)." % (n_times, len(times)) - ) - if events is None: - n_epochs = len(data) - events = _gen_events(n_epochs) - if selection is None: - n_epochs = len(data) - selection = np.arange(n_epochs) - if drop_log is None: - n_epochs_prerejection = max(len(events), max(selection) + 1) - drop_log = tuple( - () if k in selection else ("IGNORED",) - for k in range(n_epochs_prerejection) - ) - else: - drop_log = drop_log - # check consistency: - assert len(selection) == len(events) - assert len(drop_log) >= len(events) - assert len(selection) == sum(len(dl) == 0 for dl in drop_log) - event_id = _check_event_id(event_id, events) - self.data = data - self._set_times(np.array(times, dtype=float)) - self._raw_times = self.times.copy() # needed for decimate - self.freqs = np.array(freqs, dtype=float) - self.events = events - self.event_id = event_id - self.selection = selection - self.drop_log = drop_log - self.comment = comment - self.method = method - self.preload = True - self.metadata = metadata - # we need this to allow equalize_epoch_counts to work with EpochsTFRs - self._bad_dropped = True + state = dict(info=info, data=data, times=times, freqs=freqs) + optional = dict( + comment=comment, + method=method, + events=events, + event_id=event_id, + selection=selection, + drop_log=drop_log, + metadata=metadata, + ) + for name, value in optional.items(): + if value is not None: + state[name] = value + self.__setstate__(state) - @property - def _detrend_picks(self): - return list() - def __repr__(self): # noqa: D105 - s = f"time : [{self.times[0]}, {self.times[-1]}]" - s += f", freq : [{self.freqs[0]}, {self.freqs[-1]}]" - s += ", epochs : %d" % self.data.shape[0] - s += ", channels : %d" % self.data.shape[1] - s += f", ~{sizeof_fmt(self._size)}" - return "" % s +@fill_doc +class RawTFR(BaseTFR): + """Data object for spectrotemporal representations of continuous data. + + .. warning:: The preferred means of creating RawTFR objects from + :class:`~mne.io.Raw` objects is via the instance method + :meth:`~mne.io.Raw.compute_tfr`. Direct class instantiation + is not supported. - def __abs__(self): - """Take the absolute value.""" - epochs = self.copy() - epochs.data = np.abs(self.data) - return epochs + Parameters + ---------- + inst : instance of Raw + The data from which to compute the time-frequency representation. + %(method_tfr)s + %(freqs_tfr)s + %(tmin_tmax_psd)s + %(picks_good_data_noref)s + %(proj_psd)s + %(reject_by_annotation_tfr)s + %(decim_tfr)s + %(n_jobs)s + %(verbose)s + %(method_kw_tfr)s + + Attributes + ---------- + ch_names : list + The channel names. + freqs : array + Frequencies at which the amplitude, power, or fourier coefficients + have been computed. + %(info_not_none)s + method : str + The method used to compute the spectra (``'morlet'``, ``'multitaper'`` + or ``'stockwell'``). + + See Also + -------- + mne.io.Raw.compute_tfr + EpochsTFR + AverageTFR + + References + ---------- + .. footbibliography:: + """ + + def __init__( + self, + inst, + method=None, + freqs=None, + *, + tmin=None, + tmax=None, + picks=None, + proj=False, + reject_by_annotation=False, + decim=1, + n_jobs=None, + verbose=None, + **method_kw, + ): + from ..io import BaseRaw + + # dict is allowed for __setstate__ compatibility + _validate_type( + inst, (BaseRaw, dict), "object passed to RawTFR constructor", "Raw" + ) + super().__init__( + inst, + method, + freqs, + tmin=tmin, + tmax=tmax, + picks=picks, + proj=proj, + reject_by_annotation=reject_by_annotation, + decim=decim, + n_jobs=n_jobs, + verbose=verbose, + **method_kw, + ) - def average(self, method="mean", dim="epochs", copy=False): - """Average the data across epochs. + def __getitem__(self, item): + """Get RawTFR data. Parameters ---------- - method : str | callable - How to combine the data. If "mean"/"median", the mean/median - are returned. Otherwise, must be a callable which, when passed - an array of shape (n_epochs, n_channels, n_freqs, n_time) - returns an array of shape (n_channels, n_freqs, n_time). - Note that due to file type limitations, the kind for all - these will be "average". - dim : 'epochs' | 'freqs' | 'times' - The dimension along which to combine the data. - copy : bool - Whether to return a copy of the modified instance, - or modify in place. Ignored when ``dim='epochs'`` - because a new instance must be returned. + item : int | slice | array-like + Indexing is similar to a :class:`NumPy array`; see + Notes. Returns ------- - ave : instance of AverageTFR | EpochsTFR - The averaged data. + %(getitem_tfr_return)s Notes ----- - Passing in ``np.median`` is considered unsafe when there is complex - data because NumPy doesn't compute the marginal median. Numpy currently - sorts the complex values by real part and return whatever value is - computed. Use with caution. We use the marginal median in the - complex case (i.e. the median of each component separately) if - one passes in ``median``. See a discussion in scipy: + The last axis is always time, the next-to-last axis is always + frequency, and the first axis is always channel. If + ``method='multitaper'`` and ``output='complex'`` then the second axis + will be taper index. - https://github.com/scipy/scipy/pull/12676#issuecomment-783370228 - """ - _check_option("dim", dim, ("epochs", "freqs", "times")) - axis = dict(epochs=0, freqs=2, times=self.data.ndim - 1)[dim] + Integer-, list-, and slice-based indexing is possible: - # return a lambda function for computing a combination metric - # over epochs - func = _check_combine(mode=method, axis=axis) - data = func(self.data) + - ``raw_tfr[[0, 2]]`` gives the whole time-frequency plane for the + first and third channels. + - ``raw_tfr[..., :3, :]`` gives the first 3 frequency bins and all + times for all channels (and tapers, if present). + - ``raw_tfr[..., :100]`` gives the first 100 time samples in all + frequency bins for all channels (and tapers). + - ``raw_tfr[(4, 7)]`` is the same as ``raw_tfr[4, 7]``. - n_epochs, n_channels, n_freqs, n_times = self.data.shape - freqs, times = self.freqs, self.times + .. note:: - if dim == "freqs": - freqs = np.mean(self.freqs, keepdims=True) - n_freqs = 1 - elif dim == "times": - times = np.mean(self.times, keepdims=True) - n_times = 1 - if dim == "epochs": - expected_shape = self._data.shape[1:] - else: - expected_shape = (n_epochs, n_channels, n_freqs, n_times) - data = np.expand_dims(data, axis=axis) + Unlike :class:`~mne.io.Raw` objects (which returns a tuple of the + requested data values and the corresponding times), accessing + :class:`~mne.time_frequency.RawTFR` values via subscript does + **not** return the corresponding frequency bin values. If you need + them, use ``RawTFR.freqs[freq_indices]`` or + ``RawTFR.get_data(..., return_freqs=True)``. + """ + from ..io import BaseRaw - if data.shape != expected_shape: - raise RuntimeError( - f"You passed a function that resulted in data of shape " - f"{data.shape}, but it should be {expected_shape}." - ) + self._parse_get_set_params = partial(BaseRaw._parse_get_set_params, self) + return BaseRaw._getitem(self, item, return_times=False) - if dim == "epochs": - return AverageTFR( - info=self.info.copy(), - data=data, - times=times, - freqs=freqs, - nave=self.data.shape[0], - method=self.method, - comment=self.comment, - ) - elif copy: - return EpochsTFR( - info=self.info.copy(), - data=data, - times=times, - freqs=freqs, - method=self.method, - comment=self.comment, - metadata=self.metadata, - events=self.events, - event_id=self.event_id, - ) - else: - self.data = data - self._set_times(times) - self.freqs = freqs - return self + def _get_instance_data(self, time_mask, reject_by_annotation): + start, stop = np.where(time_mask)[0][[0, -1]] + rba = "NaN" if reject_by_annotation else None + data = self.inst.get_data( + self._picks, start, stop + 1, reject_by_annotation=rba + ) + # prepend a singleton "epochs" axis + return data[np.newaxis] - @verbose - def drop(self, indices, reason="USER", verbose=None): - """Drop epochs based on indices or boolean mask. - .. note:: The indices refer to the current set of undropped epochs - rather than the complete set of dropped and undropped epochs. - They are therefore not necessarily consistent with any - external indices (e.g., behavioral logs). To drop epochs - based on external criteria, do not use the ``preload=True`` - flag when constructing an Epochs object, and call this - method before calling the :meth:`mne.Epochs.drop_bad` or - :meth:`mne.Epochs.load_data` methods. +@fill_doc +class RawTFRArray(RawTFR): + """Data object for *precomputed* spectrotemporal representations of continuous data. - Parameters - ---------- - indices : array of int or bool - Set epochs to remove by specifying indices to remove or a boolean - mask to apply (where True values get removed). Events are - correspondingly modified. - reason : str - Reason for dropping the epochs ('ECG', 'timeout', 'blink' etc). - Default: 'USER'. - %(verbose)s + Parameters + ---------- + %(info_not_none)s + %(data_tfr)s + %(times)s + %(freqs_tfr_array)s + %(method_tfr_array)s - Returns - ------- - epochs : instance of Epochs or EpochsTFR - The epochs with indices dropped. Operates in-place. - """ - from ..epochs import BaseEpochs + Attributes + ---------- + %(baseline_tfr_attr)s + %(ch_names_tfr_attr)s + %(freqs_tfr_attr)s + %(info_not_none)s + %(method_tfr_attr)s + %(sfreq_tfr_attr)s + %(shape_tfr_attr)s - BaseEpochs.drop(self, indices=indices, reason=reason, verbose=verbose) + See Also + -------- + RawTFR + mne.io.Raw.compute_tfr + EpochsTFRArray + AverageTFRArray + """ - return self + def __init__( + self, + info, + data, + times, + freqs, + *, + method=None, + ): + state = dict(info=info, data=data, times=times, freqs=freqs) + if method is not None: + state["method"] = method + self.__setstate__(state) def combine_tfr(all_tfr, weights="nave"): @@ -2972,6 +4038,7 @@ def combine_tfr(all_tfr, weights="nave"): # Utils +# ↓↓↓↓↓↓↓↓↓↓↓ this is still used in _stockwell.py def _get_data(inst, return_itc): """Get data from Epochs or Evoked instance as epochs x ch x time.""" from ..epochs import BaseEpochs @@ -3065,8 +4132,7 @@ def _preproc_tfr( return data, times, freqs, vmin, vmax -# TODO: Name duplication with mne/utils/mixin.py -def _check_decim(decim): +def _ensure_slice(decim): """Aux function checking the decim parameter.""" _validate_type(decim, ("int-like", slice), "decim") if not isinstance(decim, slice): @@ -3088,10 +4154,11 @@ def write_tfrs(fname, tfr, overwrite=False, *, verbose=None): ---------- fname : path-like The file name, which should end with ``-tfr.h5``. - tfr : AverageTFR | list of AverageTFR | EpochsTFR - The TFR dataset, or list of TFR datasets, to save in one file. - Note. If .comment is not None, a name will be generated on the fly, - based on the order in which the TFR objects are passed. + tfr : RawTFR | EpochsTFR | AverageTFR | list of RawTFR | list of EpochsTFR | list of AverageTFR + The (list of) TFR object(s) to save in one file. If ``tfr.comment`` is ``None``, + a sequential numeric string name will be generated on the fly, based on the + order in which the TFR objects are passed. This can be used to selectively load + single TFR objects from the file later. %(overwrite)s %(verbose)s @@ -3102,92 +4169,112 @@ def write_tfrs(fname, tfr, overwrite=False, *, verbose=None): Notes ----- .. versionadded:: 0.9.0 - """ + """ # noqa E501 _, write_hdf5 = _import_h5io_funcs() out = [] if not isinstance(tfr, (list, tuple)): tfr = [tfr] for ii, tfr_ in enumerate(tfr): - comment = ii if tfr_.comment is None else tfr_.comment - out.append(_prepare_write_tfr(tfr_, condition=comment)) + comment = ii if getattr(tfr_, "comment", None) is None else tfr_.comment + state = tfr_.__getstate__() + if "metadata" in state: + state["metadata"] = _prepare_write_metadata(state["metadata"]) + out.append((comment, state)) write_hdf5(fname, out, overwrite=overwrite, title="mnepython", slash="replace") -def _prepare_write_tfr(tfr, condition): - """Aux function.""" - attributes = dict( - times=tfr.times, - freqs=tfr.freqs, - data=tfr.data, - info=tfr.info, - comment=tfr.comment, - method=tfr.method, - ) - if hasattr(tfr, "nave"): # if AverageTFR - attributes["nave"] = tfr.nave - elif hasattr(tfr, "events"): # if EpochsTFR - attributes["events"] = tfr.events - attributes["event_id"] = tfr.event_id - attributes["selection"] = tfr.selection - attributes["drop_log"] = tfr.drop_log - attributes["metadata"] = _prepare_write_metadata(tfr.metadata) - return condition, attributes - - @verbose def read_tfrs(fname, condition=None, *, verbose=None): - """Read TFR datasets from hdf5 file. + """Load a TFR object from disk. Parameters ---------- fname : path-like - The file name, which should end with -tfr.h5 . + Path to a TFR file in HDF5 format. condition : int or str | list of int or str | None - The condition to load. If None, all conditions will be returned. - Defaults to None. + The condition to load. If ``None``, all conditions will be returned. + Defaults to ``None``. %(verbose)s Returns ------- - tfr : AverageTFR | list of AverageTFR | EpochsTFR - Depending on ``condition`` either the TFR object or a list of multiple - TFR objects. + tfr : RawTFR | EpochsTFR | AverageTFR | list of RawTFR | list of EpochsTFR | list of AverageTFR + The loaded time-frequency object. See Also -------- + mne.time_frequency.RawTFR.save + mne.time_frequency.EpochsTFR.save + mne.time_frequency.AverageTFR.save write_tfrs Notes ----- .. versionadded:: 0.9.0 - """ - check_fname(fname, "tfr", ("-tfr.h5", "_tfr.h5")) + """ # noqa E501 read_hdf5, _ = _import_h5io_funcs() + fname = _check_fname(fname=fname, overwrite="read", must_exist=False) + valid_fnames = tuple( + f"{sep}tfr.{ext}" for sep in ("-", "_") for ext in ("h5", "hdf5") + ) + check_fname(fname, "tfr", valid_fnames) + logger.info(f"Reading {fname} ...") + hdf5_dict = read_hdf5(fname, title="mnepython", slash="replace") + # single TFR from TFR.save() + if "inst_type_str" in hdf5_dict: + inst_type_str = hdf5_dict["inst_type_str"] + Klass = dict(Epochs=EpochsTFR, Raw=RawTFR, Evoked=AverageTFR)[inst_type_str] + out = Klass(inst=hdf5_dict) + if getattr(out, "metadata", None) is not None: + out.metadata = _prepare_read_metadata(out.metadata) + return out + # maybe multiple TFRs from write_tfrs() + return _read_multiple_tfrs(hdf5_dict, condition=condition, verbose=verbose) - logger.info("Reading %s ..." % fname) - tfr_data = read_hdf5(fname, title="mnepython", slash="replace") - for k, tfr in tfr_data: + +@verbose +def _read_multiple_tfrs(tfr_data, condition=None, *, verbose=None): + """Read (possibly multiple) TFR datasets from an h5 file written by write_tfrs().""" + out = list() + keys = list() + # tfr_data is a list of (comment, tfr_dict) tuples + for key, tfr in tfr_data: + keys.append(str(key)) # auto-assigned keys are ints + is_epochs = tfr["data"].ndim == 4 + is_average = "nave" in tfr + if condition is not None: + if not is_average: + raise NotImplementedError( + "condition is only supported when reading AverageTFRs." + ) + if key != condition: + continue + tfr = dict(tfr) tfr["info"] = Info(tfr["info"]) tfr["info"]._check_consistency() if "metadata" in tfr: tfr["metadata"] = _prepare_read_metadata(tfr["metadata"]) - is_average = "nave" in tfr - if condition is not None: - if not is_average: - raise NotImplementedError( - "condition not supported when reading " "EpochsTFR." - ) - tfr_dict = dict(tfr_data) - if condition not in tfr_dict: - keys = ["%s" % k for k in tfr_dict] - raise ValueError( - 'Cannot find condition ("{}") in this file. ' - 'The file contains "{}""'.format(condition, " or ".join(keys)) + # additional keys needed for TFR __setstate__ + defaults = dict(baseline=None, data_type="Power Estimates") + if is_epochs: + Klass = EpochsTFR + defaults.update( + inst_type_str="Epochs", dims=("epoch", "channel", "freq", "time") ) - out = AverageTFR(**tfr_dict[condition]) - else: - inst = AverageTFR if is_average else EpochsTFR - out = [inst(**d) for d in list(zip(*tfr_data))[1]] + elif is_average: + Klass = AverageTFR + defaults.update(inst_type_str="Evoked", dims=("channel", "freq", "time")) + else: + Klass = RawTFR + defaults.update(inst_type_str="Raw", dims=("channel", "freq", "time")) + out.append(Klass(inst=defaults | tfr)) + if len(out) == 0: + raise ValueError( + f'Cannot find condition "{condition}" in this file. ' + f'The file contains conditions {", ".join(keys)}' + ) + if len(out) == 1: + out = out[0] return out @@ -3196,7 +4283,7 @@ def _get_timefreqs(tfr, timefreqs): # Input check timefreq_error_msg = ( "Supplied `timefreqs` are somehow malformed. Please supply None, " - "a list of tuple pairs, or a dict of such tuple pairs, not: " + "a list of tuple pairs, or a dict of such tuple pairs, not {}" ) if isinstance(timefreqs, dict): for k, v in timefreqs.items(): @@ -3205,7 +4292,7 @@ def _get_timefreqs(tfr, timefreqs): raise ValueError(timefreq_error_msg, item) elif timefreqs is not None: if not hasattr(timefreqs, "__len__"): - raise ValueError(timefreq_error_msg, timefreqs) + raise ValueError(timefreq_error_msg.format(timefreqs)) if len(timefreqs) == 2 and all(_is_numeric(v) for v in timefreqs): timefreqs = [tuple(timefreqs)] # stick a pair of numbers in a list else: @@ -3217,7 +4304,7 @@ def _get_timefreqs(tfr, timefreqs): ): pass else: - raise ValueError(timefreq_error_msg, item) + raise ValueError(timefreq_error_msg.format(item)) # If None, automatic identification of max peak else: @@ -3244,59 +4331,66 @@ def _get_timefreqs(tfr, timefreqs): return timefreqs -def _preproc_tfr_instance( - tfr, - picks, - tmin, - tmax, - fmin, - fmax, - vmin, - vmax, - dB, - mode, - baseline, - exclude, - copy=True, -): - """Baseline and truncate (times and freqs) a TFR instance.""" - tfr = tfr.copy() if copy else tfr - - exclude = None if picks is None else exclude - picks = _picks_to_idx(tfr.info, picks, exclude="bads") - pick_names = [tfr.info["ch_names"][pick] for pick in picks] - tfr.pick(pick_names) - - if exclude == "bads": - exclude = [ch for ch in tfr.info["bads"] if ch in tfr.info["ch_names"]] - if exclude is not None: - tfr.drop_channels(exclude) - - data, times, freqs, _, _ = _preproc_tfr( - tfr.data, - tfr.times, - tfr.freqs, - tmin, - tmax, - fmin, - fmax, - mode, - baseline, - vmin, - vmax, - dB, - tfr.info["sfreq"], - copy=False, - ) - - tfr._set_times(times) - tfr.freqs = freqs - tfr.data = data - - return tfr - - def _check_tfr_complex(tfr, reason="source space estimation"): """Check that time-frequency epochs or average data is complex.""" if not np.iscomplexobj(tfr.data): raise RuntimeError(f"Time-frequency data must be complex for {reason}") + + +def _merge_if_grads(data, info, ch_type, sphere, combine=None): + if ch_type == "grad": + grad_picks = _pair_grad_sensors(info, topomap_coords=False) + pos = _find_topomap_coords(info, picks=grad_picks[::2], sphere=sphere) + grad_method = combine if isinstance(combine, str) else "rms" + data, _ = _merge_ch_data(data[grad_picks], ch_type, [], method=grad_method) + else: + pos, _ = _get_pos_outlines(info, picks=ch_type, sphere=sphere) + return data, pos + + +@verbose +def _prep_data_for_plot( + data, + times, + freqs, + *, + tmin=None, + tmax=None, + fmin=None, + fmax=None, + baseline=None, + mode=None, + dB=False, + verbose=None, +): + # baseline + copy = baseline is not None + data = rescale(data, times, baseline, mode, copy=copy, verbose=verbose) + # crop times + time_mask = np.nonzero(_time_mask(times, tmin, tmax))[0] + times = times[time_mask] + # crop freqs + freq_mask = np.nonzero(_time_mask(freqs, fmin, fmax))[0] + freqs = freqs[freq_mask] + # crop data + data = data[..., freq_mask, :][..., time_mask] + # complex amplitude → real power; real-valued data is already power (or ITC) + if np.iscomplexobj(data): + data = (data * data.conj()).real + if dB: + data = 10 * np.log10(data) + return data, times, freqs + + +def _warn_deprecated_vmin_vmax(vlim, vmin, vmax): + if vmin is not None or vmax is not None: + warning = "Parameters `vmin` and `vmax` are deprecated, use `vlim` instead." + if vlim[0] is None and vlim[1] is None: + vlim = (vmin, vmax) + else: + warning += ( + " You've also provided a (non-default) value for `vlim`, " + "so `vmin` and `vmax` will be ignored." + ) + warn(warning, FutureWarning) + return vlim diff --git a/mne/utils/__init__.pyi b/mne/utils/__init__.pyi index 3e4d1292ee2..e22d8f6166c 100644 --- a/mne/utils/__init__.pyi +++ b/mne/utils/__init__.pyi @@ -41,6 +41,7 @@ __all__ = [ "_check_if_nan", "_check_info_inv", "_check_integer_or_list", + "_check_method_kwargs", "_check_on_missing", "_check_one_ch_type", "_check_option", @@ -239,6 +240,7 @@ from .check import ( _check_if_nan, _check_info_inv, _check_integer_or_list, + _check_method_kwargs, _check_on_missing, _check_one_ch_type, _check_option, diff --git a/mne/utils/_testing.py b/mne/utils/_testing.py index b2829917f59..f0e76c70e8a 100644 --- a/mne/utils/_testing.py +++ b/mne/utils/_testing.py @@ -365,3 +365,13 @@ def _click_ch_name(fig, ch_index=0, button=1): x = bbox.intervalx.mean() y = bbox.intervaly.mean() _fake_click(fig, fig.mne.ax_main, (x, y), xform="pix", button=button) + + +def _get_suptitle(fig): + """Get fig suptitle (shim for matplotlib < 3.8.0).""" + # TODO: obsolete when minimum MPL version is 3.8 + if check_version("matplotlib", "3.8"): + return fig.get_suptitle() + else: + # unreliable hack; should work in most tests as we rarely use `sup_{x,y}label` + return fig.texts[0].get_text() diff --git a/mne/utils/check.py b/mne/utils/check.py index b703317f9d0..80d87cafd2b 100644 --- a/mne/utils/check.py +++ b/mne/utils/check.py @@ -11,6 +11,7 @@ from builtins import input # noqa: UP029 from difflib import get_close_matches from importlib import import_module +from inspect import signature from pathlib import Path import numpy as np @@ -313,10 +314,10 @@ def _check_preload(inst, msg): from ..epochs import BaseEpochs from ..evoked import Evoked from ..source_estimate import _BaseSourceEstimate - from ..time_frequency import _BaseTFR + from ..time_frequency import BaseTFR from ..time_frequency.spectrum import BaseSpectrum - if isinstance(inst, (_BaseTFR, Evoked, BaseSpectrum, _BaseSourceEstimate)): + if isinstance(inst, (BaseTFR, Evoked, BaseSpectrum, _BaseSourceEstimate)): pass else: name = "epochs" if isinstance(inst, BaseEpochs) else "raw" @@ -914,6 +915,7 @@ def _check_all_same_channel_names(instances): def _check_combine(mode, valid=("mean", "median", "std"), axis=0): + # XXX TODO Possibly de-duplicate with _make_combine_callable of mne/viz/utils.py if mode == "mean": def fun(data): @@ -1244,3 +1246,19 @@ def _import_nibabel(why="use MRI files"): except ImportError as exp: raise exp.__class__(f"nibabel is required to {why}, got:\n{exp}") from None return nib + + +def _check_method_kwargs(func, kwargs, msg=None): + """Ensure **kwargs are compatible with the function they're passed to.""" + from .misc import _pl + + valid = list(signature(func).parameters) + is_invalid = np.isin(list(kwargs), valid, invert=True) + if is_invalid.any(): + invalid_kw = np.array(list(kwargs))[is_invalid].tolist() + s = _pl(invalid_kw) + if msg is None: + msg = f'function "{func}"' + raise TypeError( + f'Got unexpected keyword argument{s} {", ".join(invalid_kw)} ' f"for {msg}." + ) diff --git a/mne/utils/docs.py b/mne/utils/docs.py index f5d7c4f4669..c82f9d74344 100644 --- a/mne/utils/docs.py +++ b/mne/utils/docs.py @@ -64,6 +64,61 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): # %% # A +tfr_arithmetics_return_template = """ +Returns +------- +tfr : instance of RawTFR | instance of EpochsTFR | instance of AverageTFR + {} +""" + +tfr_add_sub_template = """ +Parameters +---------- +other : instance of RawTFR | instance of EpochsTFR | instance of AverageTFR + The TFR instance to {}. Must have the same type as ``self``, and matching + ``.times`` and ``.freqs`` attributes. + +{} +""" + +tfr_mul_truediv_template = """ +Parameters +---------- +num : int | float + The number to {} by. + +{} +""" + +tfr_arithmetics_return = tfr_arithmetics_return_template.format( + "A new TFR instance, of the same type as ``self``." +) +tfr_inplace_arithmetics_return = tfr_arithmetics_return_template.format( + "The modified TFR instance." +) + +docdict["__add__tfr"] = tfr_add_sub_template.format("add", tfr_arithmetics_return) +docdict["__iadd__tfr"] = tfr_add_sub_template.format( + "add", tfr_inplace_arithmetics_return +) +docdict["__imul__tfr"] = tfr_mul_truediv_template.format( + "multiply", tfr_inplace_arithmetics_return +) +docdict["__isub__tfr"] = tfr_add_sub_template.format( + "subtract", tfr_inplace_arithmetics_return +) +docdict["__itruediv__tfr"] = tfr_mul_truediv_template.format( + "divide", tfr_inplace_arithmetics_return +) +docdict["__mul__tfr"] = tfr_mul_truediv_template.format( + "multiply", tfr_arithmetics_return +) +docdict["__sub__tfr"] = tfr_add_sub_template.format("subtract", tfr_arithmetics_return) +docdict["__truediv__tfr"] = tfr_mul_truediv_template.format( + "divide", tfr_arithmetics_return +) + + docdict["accept"] = """ accept : bool If True (default False), accept the license terms of this dataset. @@ -303,42 +358,67 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): """ _axes_base = """\ -{} : instance of Axes | {}None - The axes to plot to. If ``None``, a new :class:`~matplotlib.figure.Figure` - will be created{}. {}Default is ``None``. -""" -_axes_num = ( - "If :class:`~matplotlib.axes.Axes` are provided (either as a " - "single instance or a :class:`list` of axes), the number of axes " - "provided must {}." -) +{param} : instance of Axes | {allowed}None + The axes to plot into. If ``None``, a new :class:`~matplotlib.figure.Figure` + will be created{created}. {list_extra}{extra}Default is ``None``. +""" _axes_list = _axes_base.format( - "{}", "list of Axes | ", " with the correct number of axes", _axes_num + param="{param}", + allowed="list of Axes | ", + created=" with the correct number of axes", + list_extra="""If :class:`~matplotlib.axes.Axes` + are provided (either as a single instance or a :class:`list` of axes), + the number of axes provided must {must}. """, + extra="{extra}", +) +_match_chtypes_present_in = "match the number of channel types present in the {}object." +docdict["ax_plot_psd"] = _axes_list.format( + param="ax", must=_match_chtypes_present_in.format(""), extra="" +) +docdict["axes_cov_plot_topomap"] = _axes_list.format( + param="axes", must="be length 1", extra="" ) -_ch_types_present = "match the number of channel types present in the {}" "object." -docdict["ax_plot_psd"] = _axes_list.format("ax", _ch_types_present.format("")) -docdict["axes_cov_plot_topomap"] = _axes_list.format("axes", "be length 1") docdict["axes_evoked_plot_topomap"] = _axes_list.format( - "axes", "match the number of ``times`` provided (unless ``times`` is ``None``)" + param="axes", + must="match the number of ``times`` provided (unless ``times`` is ``None``)", + extra="", ) docdict["axes_montage"] = """ axes : instance of Axes | instance of Axes3D | None Axes to draw the sensors to. If ``kind='3d'``, axes must be an instance - of Axes3D. If None (default), a new axes will be created.""" + of Axes3D. If None (default), a new axes will be created. +""" docdict["axes_plot_projs_topomap"] = _axes_list.format( - "axes", "match the number of projectors" + param="axes", + must="match the number of projectors", + extra="", +) +docdict["axes_plot_topomap"] = _axes_base.format( + param="axes", + allowed="", + created="", + list_extra="", + extra="", ) -docdict["axes_plot_topomap"] = _axes_base.format("axes", "", "", "") docdict["axes_spectrum_plot"] = _axes_list.format( - "axes", _ch_types_present.format(":class:`~mne.time_frequency.Spectrum`") + param="axes", + must=_match_chtypes_present_in.format(":class:`~mne.time_frequency.Spectrum` "), + extra="", ) docdict["axes_spectrum_plot_topo"] = _axes_list.format( - "axes", - "be length 1 (for efficiency, subplots for each channel are simulated " + param="axes", + must="be length 1 (for efficiency, subplots for each channel are simulated " "within a single :class:`~matplotlib.axes.Axes` object)", + extra="", ) docdict["axes_spectrum_plot_topomap"] = _axes_list.format( - "axes", "match the length of ``bands``" + param="axes", must="match the length of ``bands``", extra="" +) +docdict["axes_tfr_plot"] = _axes_list.format( + param="axes", + must="match the number of picks", + extra="""If ``combine`` is not None, + ``axes`` must either be an instance of Axes, or a list of length 1. """, ) docdict["axis_facecolor"] = """\ @@ -396,11 +476,12 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): If a tuple ``(a, b)``, the interval is between ``a`` and ``b`` (in seconds), including the endpoints. If ``a`` is ``None``, the **beginning** of the data is used; and if ``b`` - is ``None``, it is set to the **end** of the interval. + is ``None``, it is set to the **end** of the data. If ``(None, None)``, the entire time interval is used. - .. note:: The baseline ``(a, b)`` includes both endpoints, i.e. all - timepoints ``t`` such that ``a <= t <= b``. + .. note:: + The baseline ``(a, b)`` includes both endpoints, i.e. all timepoints ``t`` + such that ``a <= t <= b``. """ docdict["baseline_epochs"] = f"""{_baseline_rescale_base} @@ -448,12 +529,21 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): """ +docdict["baseline_tfr_attr"] = """ +baseline : array-like, shape (2,) + The start and end times of the baseline period, in seconds.""" + + docdict["block"] = """\ block : bool Whether to halt program execution until the figure is closed. May not work on all systems / platforms. Defaults to ``False``. """ +docdict["border_topo"] = """ +border : str + Matplotlib border style to be used for each sensor plot. +""" docdict["border_topomap"] = """ border : float | 'mean' Value to extrapolate to on the topomap borders. If ``'mean'`` (default), @@ -560,6 +650,9 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): description=['Start', 'BAD_flux', 'BAD_noise'], ch_names=[[], ['MEG0111', 'MEG2563'], ['MEG1443']]) """ +docdict["ch_names_tfr_attr"] = """ +ch_names : list + The channel names.""" docdict["ch_type_set_eeg_reference"] = """ ch_type : list of str | str @@ -652,13 +745,19 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): ``pos_lims``, as the surface plot must show the magnitude. """ -docdict["cmap"] = """ -cmap : matplotlib colormap | str | None - The :class:`~matplotlib.colors.Colormap` to use. Defaults to ``None``, which - will use the matplotlib default colormap. +_cmap_template = """ +cmap : matplotlib colormap | str{allowed} + The :class:`~matplotlib.colors.Colormap` to use. If a :class:`str`, must be a + valid Matplotlib colormap name. Default is {default}. """ - -docdict["cmap_topomap"] = """ +docdict["cmap"] = _cmap_template.format( + allowed=" | None", + default="``None``, which will use the Matplotlib default colormap", +) +docdict["cmap_tfr_plot_topo"] = _cmap_template.format( + allowed="", default='``"RdBu_r"``' +) +docdict["cmap_topomap"] = """\ cmap : matplotlib colormap | (colormap, bool) | 'interactive' | None Colormap to use. If :class:`tuple`, the first value indicates the colormap to use and the second value is a boolean defining interactivity. In @@ -707,6 +806,15 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): white. """ +docdict["colorbar"] = """\ +colorbar : bool + Whether to add a colorbar to the plot. Default is ``True``. +""" +docdict["colorbar_tfr_plot_joint"] = """ +colorbar : bool + Whether to add a colorbar to the plot (for the topomap annotations). Not compatible + with user-defined ``axes``. Default is ``True``. +""" docdict["colorbar_topomap"] = """ colorbar : bool Plot a colorbar in the rightmost column of the figure. @@ -720,27 +828,29 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): """ _combine_template = """ -combine : 'mean' | {literals} | callable | None - How to aggregate across channels. If ``None``, {none}. If a string, +combine : 'mean' | {literals} | callable{none} + How to aggregate across channels. {none_sentence}If a string, ``"mean"`` uses :func:`numpy.mean`, {other_string}. If :func:`callable`, it must operate on an :class:`array ` of shape ``({shape})`` and return an array of shape - ``({return_shape})``. {example} - {notes}Defaults to ``None``. + ``({return_shape})``. {example}{notes}Defaults to {default}. """ _example = """For example:: combine = lambda data: np.median(data, axis=1) -""" + + """ # ← the 4 trailing spaces are intentional here! _median_std_gfp = """``"median"`` computes the `marginal median `__, ``"std"`` uses :func:`numpy.std`, and ``"gfp"`` computes global field power for EEG channels and RMS amplitude for MEG channels""" +_none_default = dict(none=" | None", default="``None``") docdict["combine_plot_compare_evokeds"] = _combine_template.format( literals="'median' | 'std' | 'gfp'", - none="""channels are combined by + **_none_default, + none_sentence="""If ``None``, channels are combined by computing GFP/RMS, unless ``picks`` is a single channel (not channel type) - or ``axes="topo"``, in which cases no combining is performed""", + or ``axes="topo"``, in which cases no combining is performed. """, other_string=_median_std_gfp, shape="n_evokeds, n_channels, n_times", return_shape="n_evokeds, n_times", @@ -749,16 +859,54 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): ) docdict["combine_plot_epochs_image"] = _combine_template.format( literals="'median' | 'std' | 'gfp'", - none="""channels are combined by + **_none_default, + none_sentence="""If ``None``, channels are combined by computing GFP/RMS, unless ``group_by`` is also ``None`` and ``picks`` is a list of specific channels (not channel types), in which case no combining - is performed and each channel gets its own figure""", + is performed and each channel gets its own figure. """, other_string=_median_std_gfp, shape="n_epochs, n_channels, n_times", return_shape="n_epochs, n_times", example=_example, notes="See Notes for further details. ", ) +docdict["combine_tfr_plot"] = _combine_template.format( + literals="'rms'", + **_none_default, + none_sentence="If ``None``, plot one figure per selected channel. ", + shape="n_channels, n_freqs, n_times", + return_shape="n_freqs, n_times", + other_string='``"rms"`` computes the root-mean-square', + example="", + notes="", +) +docdict["combine_tfr_plot_joint"] = _combine_template.format( + literals="'rms'", + none="", + none_sentence="", + shape="n_channels, n_freqs, n_times", + return_shape="n_freqs, n_times", + other_string='``"rms"`` computes the root-mean-square', + example="", + notes="", + default='``"mean"``', +) + +_comment_template = """ +comment : str{or_none} + Comment on the data, e.g., the experimental condition(s){avgd}.{extra}""" +docdict["comment_averagetfr"] = _comment_template.format( + or_none=" | None", + avgd="averaged", + extra="""Default is ``None`` + which is replaced with ``inst.comment`` (for :class:`~mne.Evoked` instances) + or a comma-separated string representation of the keys in ``inst.event_id`` + (for :class:`~mne.Epochs` instances).""", +) +docdict["comment_averagetfr_attr"] = _comment_template.format( + or_none="", avgd=" averaged", extra="" +) +docdict["comment_tfr_attr"] = _comment_template.format(or_none="", avgd="", extra="") docdict["compute_proj_ecg"] = """This function will: @@ -850,11 +998,13 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): # %% # D -_dB = """\ +_dB = """ dB : bool Whether to plot on a decibel-like scale. If ``True``, plots - 10 × log₁₀(spectral power){}.{} + 10 × log₁₀({quantity}){caveat}.{extra} """ +_ignored_if_normalize = " Ignored if ``normalize=True``." +_psd = "spectral power" docdict["dB_plot_psd"] = """\ dB : bool @@ -867,10 +1017,21 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): ``dB=True`` and ``estimate='amplitude'``. """ docdict["dB_plot_topomap"] = _dB.format( - " following the application of ``agg_fun``", " Ignored if ``normalize=True``." + quantity=_psd, + caveat=" following the application of ``agg_fun``", + extra=_ignored_if_normalize, ) -docdict["dB_spectrum_plot"] = _dB.format("", "") -docdict["dB_spectrum_plot_topo"] = _dB.format("", " Ignored if ``normalize=True``.") +docdict["dB_spectrum_plot"] = _dB.format(quantity=_psd, caveat="", extra="") +docdict["dB_spectrum_plot_topo"] = _dB.format( + quantity=_psd, caveat="", extra=_ignored_if_normalize +) +docdict["dB_tfr_plot_topo"] = _dB.format(quantity="data", caveat="", extra="") + +_data_template = """ +data : ndarray, shape ({}) + The data. +""" +docdict["data_tfr"] = _data_template.format("n_channels, n_freqs, n_times") docdict["daysback_anonymize_info"] = """ daysback : int | None @@ -916,12 +1077,13 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): """ docdict["decim_tfr"] = """ -decim : int | slice, default 1 - To reduce memory usage, decimation factor after time-frequency - decomposition. +decim : int | slice + Decimation factor, applied *after* time-frequency decomposition. - - if `int`, returns ``tfr[..., ::decim]``. - - if `slice`, returns ``tfr[..., decim]``. + - if :class:`int`, returns ``tfr[..., ::decim]`` (keep only every Nth + sample along the time axis). + - if :class:`slice`, returns ``tfr[..., decim]`` (keep only the specified + slice along the time axis). .. note:: Decimation is done after convolutions and may create aliasing @@ -1002,8 +1164,7 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): docdict["drop_log"] = """ drop_log : tuple | None Tuple of tuple of strings indicating which epochs have been marked to - be ignored. -""" + be ignored.""" docdict["dtype_applyfun"] = """ dtype : numpy.dtype @@ -1151,11 +1312,21 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): and then the IDs must be the name(s) of the annotations to use. If None, all :term:`events` will be used and a dict is created with string integer names corresponding to the event id integers.""" - +_event_id_template = """ +event_id : dict{or_none} + Mapping from condition descriptions (strings) to integer event codes.{extra}""" +docdict["event_id_attr"] = _event_id_template.format(or_none="", extra="") docdict["event_id_ecg"] = """ event_id : int The index to assign to found ECG events. """ +docdict["event_id_epochstfr"] = _event_id_template.format( + or_none=" | None", + extra="""If ``None``, + all events in ``events`` will be included, and the ``event_id`` attribute + will be a :class:`dict` mapping a string version of each integer event ID + to the corresponding integer.""", +) docdict["event_repeated_epochs"] = """ event_repeated : str @@ -1167,19 +1338,28 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): .. versionadded:: 0.19 """ -docdict["events"] = """ -events : array of int, shape (n_events, 3) - The array of :term:`events`. The first column contains the event time in - samples, with :term:`first_samp` included. The third column contains the - event id.""" - -docdict["events_epochs"] = """ -events : array of int, shape (n_events, 3) - The array of :term:`events`. The first column contains the event time in - samples, with :term:`first_samp` included. The third column contains the - event id. - If some events don't match the events of interest as specified by - ``event_id``, they will be marked as ``IGNORED`` in the drop log.""" +_events_template = """ +events : ndarray of int, shape (n_events, 3){or_none} + The identity and timing of experimental events, around which the epochs were + created. See :term:`events` for more information.{extra} +""" +docdict["events"] = _events_template.format(or_none="", extra="") +docdict["events_attr"] = """ +events : ndarray of int, shape (n_events, 3) + The events array.""" +docdict["events_epochs"] = _events_template.format( + or_none="", + extra="""Events that don't match + the events of interest as specified by ``event_id`` will be marked as + ``IGNORED`` in the drop log.""", +) +docdict["events_epochstfr"] = _events_template.format( + or_none=" | None", + extra="""If ``None``, all integer + event codes are set to ``1`` (i.e., all epochs are assumed to be of the same + type) and their corresponding sample numbers are set as arbitrary, equally + spaced sample numbers with a step size of ``len(times)``.""", +) docdict["evoked_by_event_type_returns"] = """ evoked : instance of Evoked | list of Evoked @@ -1402,10 +1582,14 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): and if absent, falls back to ``'estimated'``. """ -docdict["fig_facecolor"] = """\ +docdict["fig_background"] = """ +fig_background : None | array + A background image for the figure. This must be a valid input to + :func:`matplotlib.pyplot.imshow`. Defaults to ``None``. +""" +docdict["fig_facecolor"] = """ fig_facecolor : str | tuple - A matplotlib-compatible color to use for the figure background. - Defaults to black. + A matplotlib-compatible color to use for the figure background. Defaults to black. """ docdict["filter_length"] = """ @@ -1511,6 +1695,11 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): ) docdict["fmin_fmax_psd_topo"] = _fmin_fmax.format("``fmin=0, fmax=100``.") +docdict["fmin_fmax_tfr"] = _fmin_fmax.format( + """``None`` + which is equivalent to ``fmin=0, fmax=np.inf`` (spans all frequencies + present in the data).""" +) docdict["fmin_fmid_fmax"] = """ fmin : float @@ -1560,17 +1749,37 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): mass of the visible bounds. """ +docdict["font_color"] = """ +font_color : color + The color of tick labels in the colorbar. Defaults to white. +""" + docdict["forward_set_eeg_reference"] = """ forward : instance of Forward | None Forward solution to use. Only used with ``ref_channels='REST'``. .. versionadded:: 0.21 """ - -docdict["freqs_tfr"] = """ -freqs : array of float, shape (n_freqs,) - The frequencies of interest in Hz. -""" +_freqs_tfr_template = """ +freqs : array-like |{auto} None + The frequencies at which to compute the power estimates. + {stockwell} be an array of shape (n_freqs,). ``None`` (the + default) only works when using ``__setstate__`` and will raise an error otherwise. +""" +docdict["freqs_tfr"] = _freqs_tfr_template.format(auto="", stockwell="Must") +docdict["freqs_tfr_array"] = """ +freqs : ndarray, shape (n_freqs,) + The frequencies in Hz. +""" +docdict["freqs_tfr_attr"] = """ +freqs : array + Frequencies at which power has been computed.""" +docdict["freqs_tfr_epochs"] = _freqs_tfr_template.format( + auto=" 'auto' | ", + stockwell="""If ``method='stockwell'`` this must be a length 2 iterable specifying lowest + and highest frequencies, or ``'auto'`` (to use all available frequencies). + For other methods, must""", # noqa E501 +) docdict["fullscreen"] = """ fullscreen : bool @@ -1660,17 +1869,28 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): (False, default). """ -_getitem_base = """\ +_getitem_spectrum_base = """ data : ndarray The selected spectral data. Shape will be - ``({}n_channels, n_freqs)`` for normal power spectra, - ``({}n_channels, n_freqs, n_segments)`` for unaggregated - Welch estimates, or ``({}n_channels, n_tapers, n_freqs)`` + ``({n_epo}n_channels, n_freqs)`` for normal power spectra, + ``({n_epo}n_channels, n_freqs, n_segments)`` for unaggregated + Welch estimates, or ``({n_epo}n_channels, n_tapers, n_freqs)`` for unaggregated multitaper estimates. """ -_fill_epochs = ["n_epochs, "] * 3 -docdict["getitem_epochspectrum_return"] = _getitem_base.format(*_fill_epochs) -docdict["getitem_spectrum_return"] = _getitem_base.format("", "", "") +_getitem_tfr_base = """ +data : ndarray + The selected time-frequency data. Shape will be + ``({n_epo}n_channels, n_freqs, n_times)`` for Morlet, Stockwell, and aggregated + (``output='power'``) multitaper methods, or + ``({n_epo}n_channels, n_tapers, n_freqs, n_times)`` for unaggregated + (``output='complex'``) multitaper method. +""" +n_epo = "n_epochs, " +docdict["getitem_epochspectrum_return"] = _getitem_spectrum_base.format(n_epo=n_epo) +docdict["getitem_epochstfr_return"] = _getitem_tfr_base.format(n_epo=n_epo) +docdict["getitem_spectrum_return"] = _getitem_spectrum_base.format(n_epo="") +docdict["getitem_tfr_return"] = _getitem_tfr_base.format(n_epo="") + docdict["group_by_browse"] = """ group_by : str @@ -1822,6 +2042,12 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): For more information, see :func:`mne.filter.construct_iir_filter`. """ +docdict["image_args"] = """ +image_args : dict | None + Keyword arguments to pass to :meth:`mne.time_frequency.AverageTFR.plot`. ``axes`` + and ``show`` are ignored. Defaults to ``None`` (i.e., and empty :class:`dict`). +""" + docdict["image_format_report"] = """ image_format : 'png' | 'svg' | 'gif' | None The image format to be used for the report, can be ``'png'``, @@ -1889,6 +2115,10 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): (e.g. :class:`mne.io.Raw`). """ +docdict["inst_tfr"] = """ +inst : instance of RawTFR, EpochsTFR, or AverageTFR +""" + docdict["int_order_maxwell"] = """ int_order : int Order of internal component of spherical expansion. @@ -1944,6 +2174,10 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): Defaults to ``'matrix'``. """ +docdict["item"] = """ +item : int | slice | array-like | str +""" + # %% # J @@ -2071,12 +2305,15 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): .. versionchanged:: 0.21.0 Support for volume source estimates. """ - +docdict["layout_scale"] = """ +layout_scale : float + Scaling factor for adjusting the relative size of the layout on the canvas. +""" docdict["layout_spectrum_plot_topo"] = """\ layout : instance of Layout | None Layout instance specifying sensor positions (does not need to be specified for Neuromag data). If ``None`` (default), the layout is - inferred from the data. + inferred from the data (if possible). """ docdict["line_alpha_plot_psd"] = """\ @@ -2157,14 +2394,24 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): with the parameters given in ``mask_params``. Defaults to ``None``, equivalent to an array of all ``False`` elements. """ - +docdict["mask_alpha_tfr_plot"] = """ +mask_alpha : float + Relative opacity of the masked region versus the unmasked region, given as a + :class:`float` between 0 and 1 (i.e., 0 means masked areas are not visible at all). + Defaults to ``0.1``. +""" +docdict["mask_cmap_tfr_plot"] = """ +mask_cmap : matplotlib colormap | str | None + Colormap to use for masked areas of the plot. If a :class:`str`, must be a valid + Matplotlib colormap name. If None, ``cmap`` is used for both masked and unmasked + areas. Ignored if ``mask`` is ``None``. Default is ``'Greys'``. +""" docdict["mask_evoked_topomap"] = _mask_base.format( shape="(n_channels, n_times)", shape_appendix="-time combinations", example=" (useful for, e.g. marking which channels at which times a " "statistical test of the data reaches significance)", ) - docdict["mask_params_topomap"] = """ mask_params : dict | None Additional plotting parameters for plotting significant sensors. @@ -2173,11 +2420,25 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): dict(marker='o', markerfacecolor='w', markeredgecolor='k', linewidth=0, markersize=4) """ - docdict["mask_patterns_topomap"] = _mask_base.format( shape="(n_channels, n_patterns)", shape_appendix="-pattern combinations", example="" ) - +docdict["mask_style_tfr_plot"] = """ +mask_style : None | 'both' | 'contour' | 'mask' + How to distinguish the masked/unmasked regions of the plot. If ``"contour"``, a + line is drawn around the areas where the mask is ``True``. If ``"mask"``, areas + where the mask is ``False`` will be (partially) transparent, as determined by + ``mask_alpha``. If ``"both"``, both a contour and transparency are used. Default is + ``None``, which is silently ignored if ``mask`` is ``None`` and is interpreted like + ``"both"`` otherwise. +""" +docdict["mask_tfr_plot"] = """ +mask : ndarray | None + An :class:`array ` of :class:`boolean ` values, of the same + shape as the data. Data that corresponds to ``False`` entries in the mask are + plotted differently, as determined by ``mask_style``, ``mask_alpha``, and + ``mask_cmap``. Useful for, e.g., highlighting areas of statistical significance. +""" docdict["mask_topomap"] = _mask_base.format( shape="(n_channels,)", shape_appendix="(s)", example="" ) @@ -2250,19 +2511,26 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): Added support for specifying alpha values as a dict. """ -docdict["metadata_epochs"] = """ +_metadata_attr_template = """ metadata : instance of pandas.DataFrame | None - A :class:`pandas.DataFrame` specifying metadata about each epoch. - If given, ``len(metadata)`` must equal ``len(events)``. The DataFrame - may only contain values of type (str | int | float | bool). - If metadata is given, then pandas-style queries may be used to select - subsets of data, see :meth:`mne.Epochs.__getitem__`. - When a subset of the epochs is created in this (or any other - supported) manner, the metadata object is subsetted accordingly, and - the row indices will be modified to match ``epochs.selection``. - - .. versionadded:: 0.16 -""" + A :class:`pandas.DataFrame` specifying metadata about each epoch{or_none}.{extra} +""" +_metadata_template = _metadata_attr_template.format( + or_none="", + extra=""" + If not ``None``, ``len(metadata)`` must equal ``len(events)``. For + save/load compatibility, the :class:`~pandas.DataFrame` may only contain + :class:`str`, :class:`int`, :class:`float`, and :class:`bool` values. + If not ``None``, then pandas-style queries may be used to select + subsets of data, see :meth:`mne.Epochs.__getitem__`. When the {obj} object + is subsetted, the metadata is subsetted accordingly, and the row indices + will be modified to match ``{obj}.selection``.""", +) +docdict["metadata_attr"] = _metadata_attr_template.format( + or_none=" (or ``None``)", extra="" +) +docdict["metadata_epochs"] = _metadata_template.format(obj="Epochs") +docdict["metadata_epochstfr"] = _metadata_template.format(obj="EpochsTFR") docdict["method_fir"] = """ method : str @@ -2270,6 +2538,20 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): forward-backward filtering (via :func:`~scipy.signal.filtfilt`). """ +_method_kw_tfr_template = """ +**method_kw + Additional keyword arguments passed to the spectrotemporal estimation function + (e.g., ``n_cycles, use_fft, zero_mean`` for Morlet method{stockwell} + or ``n_cycles, use_fft, zero_mean, time_bandwidth`` for multitaper method). + See :func:`~mne.time_frequency.tfr_array_morlet`{stockwell_crossref} + and :func:`~mne.time_frequency.tfr_array_multitaper` for additional details. +""" + +docdict["method_kw_epochs_tfr"] = _method_kw_tfr_template.format( + stockwell=", ``n_fft, width`` for Stockwell method,", + stockwell_crossref=", :func:`~mne.time_frequency.tfr_array_stockwell`,", +) + docdict["method_kw_psd"] = """\ **method_kw Additional keyword arguments passed to the spectral estimation @@ -2280,7 +2562,11 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): :func:`~mne.time_frequency.psd_array_multitaper` for details. """ -_method_psd = r""" +docdict["method_kw_tfr"] = _method_kw_tfr_template.format( + stockwell="", stockwell_crossref="" +) + +_method_psd = """ method : ``'welch'`` | ``'multitaper'``{} Spectral estimation method. ``'welch'`` uses Welch's method :footcite:p:`Welch1967`, ``'multitaper'`` uses DPSS @@ -2303,6 +2589,29 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): :func:`scipy.signal.resample` and :func:`scipy.signal.resample_poly`, respectively. """ +_method_tfr_template = """ +method : ``'morlet'`` | ``'multitaper'``{literals} | None + Spectrotemporal power estimation method. ``'morlet'`` uses Morlet wavelets, + ``'multitaper'`` uses DPSS tapers :footcite:p:`Slepian1978`{cites}. ``None`` (the + default) only works when using ``__setstate__`` and will raise an error otherwise. +""" +docdict["method_tfr"] = _method_tfr_template.format(literals="", cites="") +docdict["method_tfr_array"] = """ +method : str | None + Comment on the method used to compute the data, e.g., ``"hilbert"``. + Default is ``None``. +""" +docdict["method_tfr_attr"] = """ +method : str + The method used to compute the spectra (e.g., ``"morlet"``, ``"multitaper"`` + or ``"stockwell"``). +""" +docdict["method_tfr_epochs"] = _method_tfr_template.format( + literals=" | ``'stockwell'``", + cites=", and ``'stockwell'`` uses the S-transform " + ":footcite:p:`Stockwell2007,MoukademEtAl2014,WheatEtAl2010,JonesEtAl2006`", +) + docdict["mode_eltc"] = """ mode : str Extraction mode, see Notes. @@ -2322,6 +2631,23 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): n_comp first SVD components. """ +docdict["mode_tfr_plot"] = """ +mode : 'mean' | 'ratio' | 'logratio' | 'percent' | 'zscore' | 'zlogratio' + Perform baseline correction by + + - subtracting the mean of baseline values ('mean') (default) + - dividing by the mean of baseline values ('ratio') + - dividing by the mean of baseline values and taking the log + ('logratio') + - subtracting the mean of baseline values followed by dividing by + the mean of baseline values ('percent') + - subtracting the mean of baseline values and dividing by the + standard deviation of baseline values ('zscore') + - dividing by the mean of baseline values, taking the log, and + dividing by the standard deviation of log baseline values + ('zlogratio') +""" + docdict["montage"] = """ montage : None | str | DigMontage A montage containing channel positions. If a string or @@ -2446,6 +2772,13 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): names are plotted. """ +docdict["nave_tfr_attr"] = """ +nave : int + The number of epochs that were averaged to yield the result. This may reflect + epochs averaged *before* time-frequency analysis (as in + ``epochs.average(...).compute_tfr(...)``) or *after* time-frequency analysis (as + in ``epochs.compute_tfr(...).average(...)``). +""" docdict["nirx_notes"] = """ This function has only been tested with NIRScout and NIRSport devices, and with the NIRStar software version 15 and above and Aurora software @@ -2537,6 +2870,25 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): other things may also not work or be incorrect). """ +docdict["notes_timefreqs_tfr_plot_joint"] = """ +``timefreqs`` has three different modes: tuples, dicts, and auto. For (list of) tuple(s) +mode, each tuple defines a pair (time, frequency) in s and Hz on the TFR plot. +For example, to look at 10 Hz activity 1 second into the epoch and 3 Hz activity 300 ms +into the epoch, :: + + timefreqs=((1, 10), (.3, 3)) + +If provided as a dictionary, (time, frequency) tuples are keys and (time_window, +frequency_window) tuples are the values — indicating the width of the windows (centered +on the time and frequency indicated by the key) to be averaged over. For example, :: + + timefreqs={(1, 10): (0.1, 2)} + +would translate into a window that spans 0.95 to 1.05 seconds and 9 to 11 Hz. If +``None``, a single topomap will be plotted at the absolute peak across the +time-frequency representation. +""" + docdict["notes_tmax_included_by_default"] = """ Unlike Python slices, MNE time intervals by default include **both** their end points; ``crop(tmin, tmax)`` returns the interval @@ -2743,6 +3095,12 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): Defaults to 'head'. """ +docdict["output_compute_tfr"] = """ +output : str + What kind of estimate to return. Allowed values are ``"complex"``, ``"phase"``, + and ``"power"``. Default is ``"power"``. +""" + docdict["overview_mode"] = """ overview_mode : str | None Can be "channels", "empty", or "hidden" to set the overview bar mode @@ -3331,6 +3689,14 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): """ ) +docdict["reject_by_annotation_tfr"] = """ +reject_by_annotation : bool + Whether to omit bad spans of data before spectrotemporal power + estimation. If ``True``, spans with annotations whose description + begins with ``bad`` will be represented with ``np.nan`` in the + time-frequency representation. +""" + _reject_common = """\ Reject epochs based on **maximum** peak-to-peak signal amplitude (PTP), i.e. the absolute difference between the lowest and the highest signal @@ -3546,6 +3912,10 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): Iterable of indices of selected epochs. If ``None``, will be automatically generated, corresponding to all non-zero events. """ +docdict["selection_attr"] = """ +selection : ndarray + Array of indices of *selected* epochs (i.e., epochs that were not rejected, dropped, + or ignored).""" docdict["sensor_colors"] = """ sensor_colors : array-like of color | dict | None @@ -3619,6 +3989,13 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): .. footbibliography:: """ +docdict["sfreq_tfr_attr"] = """ +sfreq : int | float + The sampling frequency (read from ``info``).""" +docdict["shape_tfr_attr"] = """ +shape : tuple of int + The shape of the data.""" + docdict["show"] = """\ show : bool Show the figure if ``True``. @@ -4149,12 +4526,27 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): ``time_viewer=True`` and ``separate_canvas=False``. """ +docdict["timefreqs"] = """ +timefreqs : None | list of tuple | dict of tuple + The time-frequency point(s) for which topomaps will be plotted. See Notes. +""" + +docdict["times"] = """ +times : ndarray, shape (n_times,) + The time values in seconds. +""" + docdict["title_none"] = """ title : str | None The title of the generated figure. If ``None`` (default), no title is displayed. """ - +docdict["title_tfr_plot"] = """ +title : str | 'auto' | None + Title for the plot. If ``"auto"``, will use the channel name (if ``combine`` is + ``None``) or state the number and method of combined channels used to generate the + plot. If ``None``, no title is shown. Default is ``None``. +""" docdict["tmax_raw"] = """ tmax : float End time of the raw data to use in seconds (cannot exceed data duration). @@ -4210,10 +4602,20 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): same thresholding as :func:`scipy.linalg.orth`. """ -docdict["topomap_kwargs"] = """ -topomap_kwargs : dict | None - Keyword arguments to pass to the topomap-generating functions. +_topomap_args_template = """ +{param} : dict | None + Keyword arguments to pass to {func}.{extra} """ +docdict["topomap_args"] = _topomap_args_template.format( + param="topomap_args", + func=":func:`mne.viz.plot_topomap`", + extra=" ``axes`` and ``show`` are ignored. If ``times`` is not in this dict, " + "automatic peak detection is used. Beyond that, if ``None``, no customizable " + "arguments will be passed. Defaults to ``None`` (i.e., an empty :class:`dict`).", +) +docdict["topomap_kwargs"] = _topomap_args_template.format( + param="topomap_kwargs", func="the topomap-generating functions", extra="" +) _trans_base = """\ If str, the path to the head<->MRI transform ``*-trans.fif`` file produced @@ -4382,46 +4784,87 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): valid string options. """ -_vlim = """ -vlim : tuple of length 2{} - Colormap limits to use. If a :class:`tuple` of floats, specifies the - lower and upper bounds of the colormap (in that order); providing - ``None`` for either entry will set the corresponding boundary at the - min/max of the data{}. {}{}{}Defaults to ``(None, None)``. -""" -_vlim_joint = _vlim.format( - " | 'joint'", - " (separately for each {0})", - "{1}", - "If ``vlim='joint'``, will compute the colormap limits jointly across " - "all {0}s of the same channel type, using the min/max of the data for " - "that channel type. ", - "{2}", +_vlim = """\ +vlim : tuple of length 2{joint_param} + Lower and upper bounds of the colormap, typically a numeric value in the same + units as the data. {callable} + If both entries are ``None``, the bounds are set at {bounds}. + Providing ``None`` for just one entry will set the corresponding boundary at the + min/max of the data. {extra}Defaults to ``(None, None)``. +""" +_joint_param = ' | "joint"' +_callable_sentence = """Elements of the :class:`tuple` may also be callable functions + which take in a :class:`NumPy array ` and return a scalar. +""" +_bounds_symmetric = """± the maximum absolute value + of the data (yielding a colormap with midpoint at 0)""" +_bounds_minmax = "``(min(data), max(data))``" +_bounds_norm = "``(0, max(abs(data)))``" +_bounds_contingent = f"""{_bounds_symmetric}, or {_bounds_norm} + if the (possibly baselined) data are all-positive""" +_joint_sentence = """If ``vlim="joint"``, will compute the colormap limits + jointly across all {what}s of the same channel type (instead of separately + for each {what}), using the min/max of the data for that channel type. + {joint_extra}""" + +docdict["vlim_plot_topomap"] = _vlim.format( + joint_param="", callable="", bounds=_bounds_minmax, extra="" ) -_vlim_callable = ( - "Elements of the :class:`tuple` may also be callable functions which " - "take in a :class:`NumPy array ` and return a scalar. " +docdict["vlim_plot_topomap_proj"] = _vlim.format( + joint_param=_joint_param, + callable=_callable_sentence, + bounds=_bounds_contingent, + extra=_joint_sentence.format( + what="projector", + joint_extra='If vlim is ``"joint"``, ``info`` must not be ``None``. ', + ), ) - -docdict["vlim_plot_topomap"] = _vlim.format("", "", "", "", "") -docdict["vlim_plot_topomap_proj"] = _vlim_joint.format( - "projector", - _vlim_callable, - "If vlim is ``'joint'``, ``info`` must not be ``None``. ", +docdict["vlim_plot_topomap_psd"] = _vlim.format( + joint_param=_joint_param, + callable=_callable_sentence, + bounds=_bounds_contingent, + extra=_joint_sentence.format(what="topomap", joint_extra=""), +) +docdict["vlim_tfr_plot"] = _vlim.format( + joint_param="", callable="", bounds=_bounds_contingent, extra="" +) +docdict["vlim_tfr_plot_joint"] = _vlim.format( + joint_param="", + callable="", + bounds=_bounds_contingent, + extra="""To specify the colormap separately for the topomap annotations, + see ``topomap_args``. """, ) -docdict["vlim_plot_topomap_psd"] = _vlim_joint.format("topomap", _vlim_callable, "") -docdict["vmin_vmax_topomap"] = """ -vmin, vmax : float | callable | None +_vmin_vmax_template = """ +vmin, vmax : float | {allowed}None Lower and upper bounds of the colormap, in the same units as the data. - If ``vmin`` and ``vmax`` are both ``None``, they are set at ± the - maximum absolute value of the data (yielding a colormap with midpoint - at 0). If only one of ``vmin``, ``vmax`` is ``None``, will use - ``min(data)`` or ``max(data)``, respectively. If callable, should - accept a :class:`NumPy array ` of data and return a - float. + If ``vmin`` and ``vmax`` are both ``None``, the bounds are set at + {bounds}. If only one of ``vmin``, ``vmax`` is ``None``, will use + ``min(data)`` or ``max(data)``, respectively.{extra} """ +docdict["vmin_vmax_tfr_plot"] = """ +vmin, vmax : float | None + Lower and upper bounds of the colormap. See ``vlim``. + + .. deprecated:: 1.7 + ``vmin`` and ``vmax`` will be removed in version 1.8. + Use ``vlim`` parameter instead. +""" +# ↓↓↓ this one still used, needs helper func refactor before we can migrate to `vlim` +docdict["vmin_vmax_tfr_plot_topo"] = _vmin_vmax_template.format( + allowed="", bounds=_bounds_symmetric, extra="" +) +# ↓↓↓ this one still used in Evoked.animate_topomap(), should migrate to `vlim` +docdict["vmin_vmax_topomap"] = _vmin_vmax_template.format( + allowed="callable | ", + bounds=_bounds_symmetric, + extra=""" If callable, should accept + a :class:`NumPy array ` of data and return a :class:`float`.""", +) + + # %% # W @@ -4483,6 +4926,13 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): # %% # Y +docdict["yscale_tfr_plot"] = """ +yscale : 'auto' | 'linear' | 'log' + The scale of the y (frequency) axis. 'linear' gives linear y axis, 'log' gives + log-spaced y axis and 'auto' detects if frequencies are log-spaced and if so sets + the y axis to 'log'. Default is 'auto'. +""" + # %% # Z @@ -4554,12 +5004,12 @@ def copy_doc(source): Parameters ---------- source : function - Function to copy the docstring from + Function to copy the docstring from. Returns ------- wrapper : function - The decorated function + The decorated function. Examples -------- diff --git a/mne/utils/mixin.py b/mne/utils/mixin.py index f0fcf94de14..793e399a69f 100644 --- a/mne/utils/mixin.py +++ b/mne/utils/mixin.py @@ -80,13 +80,13 @@ def __getitem__(self, item): Parameters ---------- - item : slice, array-like, str, or list - See below for use cases. + item : int | slice | array-like | str + See Notes for use cases. Returns ------- epochs : instance of Epochs - See below for use cases. + The subset of epochs. Notes ----- @@ -197,10 +197,9 @@ def _getitem( `Epochs` or tuple(Epochs, np.ndarray) if `return_indices` is True subset of epochs (and optionally array with kept epoch indices) """ - data = self._data - self._data = None inst = self.copy() if copy else self - self._data = inst._data = data + if self._data is not None: + np.copyto(inst._data, self._data, casting="no") del self select = inst._item_to_select(item) @@ -681,10 +680,10 @@ def decimate(self, decim, offset=0, *, verbose=None): # appropriately filtered to avoid aliasing from ..epochs import BaseEpochs from ..evoked import Evoked - from ..time_frequency import AverageTFR, EpochsTFR + from ..time_frequency import BaseTFR # This should be the list of classes that inherit - _validate_type(self, (BaseEpochs, Evoked, EpochsTFR, AverageTFR), "inst") + _validate_type(self, (BaseEpochs, Evoked, BaseTFR), "inst") decim, offset, new_sfreq = _check_decim( self.info, decim, offset, check_filter=not hasattr(self, "freqs") ) @@ -755,7 +754,7 @@ def _prepare_write_metadata(metadata): """Convert metadata to JSON for saving.""" if metadata is not None: if not isinstance(metadata, list): - metadata = metadata.to_json(orient="records") + metadata = metadata.reset_index().to_json(orient="records") else: # Pandas DataFrame metadata = json.dumps(metadata) assert isinstance(metadata, str) @@ -772,5 +771,7 @@ def _prepare_read_metadata(metadata): assert isinstance(metadata, list) if pd: metadata = pd.DataFrame.from_records(metadata) + if "index" in metadata.columns: + metadata.set_index("index", inplace=True) assert isinstance(metadata, pd.DataFrame) return metadata diff --git a/mne/utils/numerics.py b/mne/utils/numerics.py index 9a7524505e7..2f09689917b 100644 --- a/mne/utils/numerics.py +++ b/mne/utils/numerics.py @@ -29,7 +29,12 @@ svd_flip, ) from ._logging import logger, verbose, warn -from .check import _ensure_int, _validate_type, check_random_state +from .check import ( + _check_pandas_installed, + _ensure_int, + _validate_type, + check_random_state, +) from .docs import fill_doc from .misc import _empty_hash @@ -255,9 +260,9 @@ def _get_inst_data(inst): from ..epochs import BaseEpochs from ..evoked import Evoked from ..io import BaseRaw - from ..time_frequency.tfr import _BaseTFR + from ..time_frequency.tfr import BaseTFR - _validate_type(inst, (BaseRaw, BaseEpochs, Evoked, _BaseTFR), "Instance") + _validate_type(inst, (BaseRaw, BaseEpochs, Evoked, BaseTFR), "Instance") if not inst.preload: inst.load_data() return inst._data @@ -776,6 +781,7 @@ def object_diff(a, b, pre="", *, allclose=False): diffs : str A string representation of the differences. """ + pd = _check_pandas_installed(strict=False) out = "" if type(a) != type(b): # Deal with NamedInt and NamedFloat @@ -835,6 +841,11 @@ def object_diff(a, b, pre="", *, allclose=False): c.eliminate_zeros() if c.nnz > 0: out += pre + (" sparse matrix a and b differ on %s " "elements" % c.nnz) + elif pd and isinstance(a, pd.DataFrame): + try: + pd.testing.assert_frame_equal(a, b) + except AssertionError: + out += pre + " DataFrame mismatch\n" elif hasattr(a, "__getstate__") and a.__getstate__() is not None: out += object_diff(a.__getstate__(), b.__getstate__(), pre, allclose=allclose) else: diff --git a/mne/utils/spectrum.py b/mne/utils/spectrum.py index 5abcb7e3378..67a68b344a7 100644 --- a/mne/utils/spectrum.py +++ b/mne/utils/spectrum.py @@ -1,3 +1,5 @@ +"""Utility functions for spectral and spectrotemporal analysis.""" + # License: BSD-3-Clause # Copyright the MNE-Python contributors. from inspect import currentframe, getargvalues, signature @@ -5,6 +7,26 @@ from ..utils import warn +def _get_instance_type_string(inst): + """Get string representation of the originating instance type.""" + from ..epochs import BaseEpochs + from ..evoked import Evoked, EvokedArray + from ..io import BaseRaw + + parent_classes = inst._inst_type.__bases__ + if BaseRaw in parent_classes: + inst_type_str = "Raw" + elif BaseEpochs in parent_classes: + inst_type_str = "Epochs" + elif inst._inst_type in (Evoked, EvokedArray): + inst_type_str = "Evoked" + else: + raise RuntimeError( + f"Unknown instance type {inst._inst_type} in {type(inst).__name__}" + ) + return inst_type_str + + def _pop_with_fallback(mapping, key, fallback_fun): """Pop from a dict and fallback to a function parameter's default value.""" fallback = signature(fallback_fun).parameters[key].default diff --git a/mne/viz/tests/test_topo.py b/mne/viz/tests/test_topo.py index 5830c647edb..344572dcfc9 100644 --- a/mne/viz/tests/test_topo.py +++ b/mne/viz/tests/test_topo.py @@ -18,7 +18,7 @@ from mne import Epochs, compute_proj_evoked, read_cov, read_events from mne.channels import read_layout from mne.io import read_raw_fif -from mne.time_frequency.tfr import AverageTFR +from mne.time_frequency.tfr import AverageTFRArray from mne.utils import _record_warnings from mne.viz import ( _get_presser, @@ -309,18 +309,20 @@ def test_plot_tfr_topo(): data = np.random.RandomState(0).randn( len(epochs.ch_names), n_freqs, len(epochs.times) ) - tfr = AverageTFR(epochs.info, data, epochs.times, np.arange(n_freqs), nave) - plt.close("all") - fig = tfr.plot_topo( - baseline=(None, 0), mode="ratio", title="Average power", vmin=0.0, vmax=14.0 + tfr = AverageTFRArray( + info=epochs.info, + data=data, + times=epochs.times, + freqs=np.arange(n_freqs), + nave=nave, ) + plt.close("all") + fig = tfr.plot_topo(baseline=(None, 0), mode="ratio", vmin=0.0, vmax=14.0) # test complex tfr.data = tfr.data * (1 + 1j) plt.close("all") - fig = tfr.plot_topo( - baseline=(None, 0), mode="ratio", title="Average power", vmin=0.0, vmax=14.0 - ) + fig = tfr.plot_topo(baseline=(None, 0), mode="ratio", vmin=0.0, vmax=14.0) # test opening tfr by clicking num_figures_before = len(plt.get_fignums()) @@ -335,14 +337,23 @@ def test_plot_tfr_topo(): # nonuniform freqs freqs = np.logspace(*np.log10([3, 10]), num=3) - tfr = AverageTFR(epochs.info, data, epochs.times, freqs, nave) - fig = tfr.plot([4], baseline=(None, 0), mode="mean", vmax=14.0, show=False) + tfr = AverageTFRArray( + info=epochs.info, data=data, times=epochs.times, freqs=freqs, nave=nave + ) + fig = tfr.plot([4], baseline=(None, 0), mode="mean", vlim=(None, 14.0), show=False) assert fig[0].axes[0].get_yaxis().get_scale() == "log" # one timesample - tfr = AverageTFR(epochs.info, data[:, :, [0]], epochs.times[[1]], freqs, nave) + tfr = AverageTFRArray( + info=epochs.info, + data=data[:, :, [0]], + times=epochs.times[[1]], + freqs=freqs, + nave=nave, + ) + with _record_warnings(): # matplotlib equal left/right - tfr.plot([4], baseline=None, vmax=14.0, show=False, yscale="linear") + tfr.plot([4], baseline=None, vlim=(None, 14.0), show=False, yscale="linear") # one frequency bin, log scale required: as it doesn't make sense # to plot log scale for one value, we test whether yscale is set to linear diff --git a/mne/viz/tests/test_topomap.py b/mne/viz/tests/test_topomap.py index 2774e198fe8..3ac6bb108a2 100644 --- a/mne/viz/tests/test_topomap.py +++ b/mne/viz/tests/test_topomap.py @@ -44,7 +44,7 @@ from mne.datasets import testing from mne.io import RawArray, read_info, read_raw_fif from mne.preprocessing import compute_bridged_electrodes -from mne.time_frequency.tfr import AverageTFR +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 from mne.viz.topomap import ( @@ -578,13 +578,21 @@ def test_plot_tfr_topomap(): data = rng.randn(len(picks), n_freqs, len(times)) # test complex numbers - tfr = AverageTFR(info, data * (1 + 1j), times, np.arange(n_freqs), nave) + tfr = AverageTFRArray( + info=info, + data=data * (1 + 1j), + times=times, + freqs=np.arange(n_freqs), + nave=nave, + ) tfr.plot_topomap( ch_type="mag", tmin=0.05, tmax=0.150, fmin=0, fmax=10, res=res, contours=0 ) # test real numbers - tfr = AverageTFR(info, data, times, np.arange(n_freqs), nave) + tfr = AverageTFRArray( + info=info, data=data, times=times, freqs=np.arange(n_freqs), nave=nave + ) tfr.plot_topomap( ch_type="mag", tmin=0.05, tmax=0.150, fmin=0, fmax=10, res=res, contours=0 ) diff --git a/mne/viz/topo.py b/mne/viz/topo.py index e23e60b9bca..11f6695e834 100644 --- a/mne/viz/topo.py +++ b/mne/viz/topo.py @@ -428,7 +428,6 @@ def _imshow_tfr( cnorm=None, ): """Show time-frequency map as two-dimensional image.""" - from matplotlib import pyplot as plt from matplotlib.widgets import RectangleSelector _check_option("yscale", yscale, ["auto", "linear", "log"]) @@ -460,7 +459,7 @@ def _imshow_tfr( if isinstance(colorbar, DraggableColorbar): cbar = colorbar.cbar # this happens with multiaxes case else: - cbar = plt.colorbar(mappable=img, ax=ax) + cbar = ax.get_figure().colorbar(mappable=img, ax=ax) if interactive_cmap: ax.CB = DraggableColorbar(cbar, img, kind="tfr_image", ch_type=None) ax.RS = RectangleSelector(ax, onselect=onselect) # reference must be kept diff --git a/mne/viz/topomap.py b/mne/viz/topomap.py index cca239f844d..5a6eac4f1ab 100644 --- a/mne/viz/topomap.py +++ b/mne/viz/topomap.py @@ -912,6 +912,7 @@ def _topomap_plot_sensors(pos_x, pos_y, sensors, ax): def _get_pos_outlines(info, picks, sphere, to_sphere=True): from ..channels.layout import _find_topomap_coords + picks = _picks_to_idx(info, picks, "all", exclude=(), allow_empty=False) ch_type = _get_plot_ch_type(pick_info(_simplify_info(info), picks), None) orig_sphere = sphere sphere, clip_origin = _adjust_meg_sphere(sphere, info, ch_type) @@ -1891,7 +1892,6 @@ def plot_tfr_topomap( tfr, ch_type, sphere=sphere ) outlines = _make_head_outlines(sphere, pos, outlines, clip_origin) - data = tfr.data[picks, :, :] # merging grads before rescaling makes ERDs visible @@ -1910,7 +1910,6 @@ def plot_tfr_topomap( itmin = idx[0] if tmax is not None: itmax = idx[-1] + 1 - # crop freqs ifmin, ifmax = None, None idx = np.where(_time_mask(tfr.freqs, fmin, fmax))[0] @@ -1918,8 +1917,7 @@ def plot_tfr_topomap( ifmax = idx[-1] + 1 data = data[:, ifmin:ifmax, itmin:itmax] - data = np.mean(np.mean(data, axis=2), axis=1)[:, np.newaxis] - + data = data.mean(axis=(1, 2))[:, np.newaxis] norm = False if np.min(data) < 0 else True vlim = _setup_vmin_vmax(data, *vlim, norm) cmap = _setup_cmap(cmap, norm=norm) diff --git a/mne/viz/utils.py b/mne/viz/utils.py index 9f622a2dd87..5d2f2d95617 100644 --- a/mne/viz/utils.py +++ b/mne/viz/utils.py @@ -2138,13 +2138,20 @@ def _set_title_multiple_electrodes( ch_type = _channel_type_prettyprint.get(ch_type, ch_type) if ch_type is None: ch_type = "sensor" - if len(ch_names) > 1: - ch_type += "s" - combine = combine.capitalize() if isinstance(combine, str) else "Combination" + ch_type = f"{ch_type}{_pl(ch_names)}" + if hasattr(combine, "func"): # functools.partial + combine = combine.func + if callable(combine): + combine = getattr(combine, "__name__", str(combine)) + if not isinstance(combine, str): + combine = "Combination" + # mean → Mean, but avoid RMS → Rms and GFP → Gfp + if combine[0].islower(): + combine = combine.capitalize() if all_: title = f"{combine} of {len(ch_names)} {ch_type}" elif len(ch_names) > max_chans and combine != "gfp": - logger.info("More than %i channels, truncating title ...", max_chans) + logger.info(f"More than {max_chans} channels, truncating title ...") title += f", ...\n({combine} of {len(ch_names)} {ch_type})" return title @@ -2373,10 +2380,16 @@ def _make_combine_callable( def _rms(data): return np.sqrt((data**2).mean(**kwargs)) + def _gfp(data): + return data.std(axis=axis, ddof=0) + + # make them play nice with _set_title_multiple_electrodes() + _rms.__name__ = "RMS" + _gfp.__name__ = "GFP" if "rms" in valid: combine_dict["rms"] = _rms if "gfp" in valid and ch_type == "eeg": - combine_dict["gfp"] = lambda data: data.std(axis=axis, ddof=0) + combine_dict["gfp"] = _gfp elif "gfp" in valid: combine_dict["gfp"] = _rms try: diff --git a/pyproject.toml b/pyproject.toml index 7bf34bf3fc8..23a2efeaf4b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -140,7 +140,7 @@ test_extra = [ "mne-bids", ] -# Dependencies for building the docuemntation +# Dependencies for building the documentation doc = [ "sphinx>=6", "numpydoc", diff --git a/tutorials/intro/10_overview.py b/tutorials/intro/10_overview.py index 94b659444b3..2c9a68a1baf 100644 --- a/tutorials/intro/10_overview.py +++ b/tutorials/intro/10_overview.py @@ -309,8 +309,8 @@ # frequency content. frequencies = np.arange(7, 30, 3) -power = mne.time_frequency.tfr_morlet( - aud_epochs, n_cycles=2, return_itc=False, freqs=frequencies, decim=3 +power = aud_epochs.compute_tfr( + "morlet", n_cycles=2, return_itc=False, freqs=frequencies, decim=3, average=True ) power.plot(["MEG 1332"]) diff --git a/tutorials/stats-sensor-space/40_cluster_1samp_time_freq.py b/tutorials/stats-sensor-space/40_cluster_1samp_time_freq.py index c32af4bcd97..0e7242e96d5 100644 --- a/tutorials/stats-sensor-space/40_cluster_1samp_time_freq.py +++ b/tutorials/stats-sensor-space/40_cluster_1samp_time_freq.py @@ -40,7 +40,6 @@ import mne from mne.datasets import sample from mne.stats import permutation_cluster_1samp_test -from mne.time_frequency import tfr_morlet # %% # Set parameters @@ -92,8 +91,8 @@ freqs = np.arange(8, 40, 2) # run the TFR decomposition -tfr_epochs = tfr_morlet( - epochs, +tfr_epochs = epochs.compute_tfr( + "morlet", freqs, n_cycles=4.0, decim=decim, diff --git a/tutorials/stats-sensor-space/50_cluster_between_time_freq.py b/tutorials/stats-sensor-space/50_cluster_between_time_freq.py index 3ced6a82463..0b4078ec883 100644 --- a/tutorials/stats-sensor-space/50_cluster_between_time_freq.py +++ b/tutorials/stats-sensor-space/50_cluster_between_time_freq.py @@ -32,7 +32,6 @@ import mne from mne.datasets import sample from mne.stats import permutation_cluster_test -from mne.time_frequency import tfr_morlet print(__doc__) @@ -104,24 +103,17 @@ decim = 2 freqs = np.arange(7, 30, 3) # define frequencies of interest n_cycles = 1.5 - -tfr_epochs_1 = tfr_morlet( - epochs_condition_1, - freqs, +tfr_kwargs = dict( + method="morlet", + freqs=freqs, n_cycles=n_cycles, decim=decim, return_itc=False, average=False, ) -tfr_epochs_2 = tfr_morlet( - epochs_condition_2, - freqs, - n_cycles=n_cycles, - decim=decim, - return_itc=False, - average=False, -) +tfr_epochs_1 = epochs_condition_1.compute_tfr(**tfr_kwargs) +tfr_epochs_2 = epochs_condition_2.compute_tfr(**tfr_kwargs) tfr_epochs_1.apply_baseline(mode="ratio", baseline=(None, 0)) tfr_epochs_2.apply_baseline(mode="ratio", baseline=(None, 0)) diff --git a/tutorials/stats-sensor-space/70_cluster_rmANOVA_time_freq.py b/tutorials/stats-sensor-space/70_cluster_rmANOVA_time_freq.py index 202c660575a..19a90decea8 100644 --- a/tutorials/stats-sensor-space/70_cluster_rmANOVA_time_freq.py +++ b/tutorials/stats-sensor-space/70_cluster_rmANOVA_time_freq.py @@ -36,7 +36,6 @@ import mne from mne.datasets import sample from mne.stats import f_mway_rm, f_threshold_mway_rm, fdr_correction -from mne.time_frequency import tfr_morlet print(__doc__) @@ -105,8 +104,8 @@ # --------------------------------------------- epochs_power = list() for condition in [epochs[k] for k in event_id]: - this_tfr = tfr_morlet( - condition, + this_tfr = condition.compute_tfr( + "morlet", freqs, n_cycles=n_cycles, decim=decim, diff --git a/tutorials/stats-sensor-space/75_cluster_ftest_spatiotemporal.py b/tutorials/stats-sensor-space/75_cluster_ftest_spatiotemporal.py index fedd88a568f..2ba8c55bf3d 100644 --- a/tutorials/stats-sensor-space/75_cluster_ftest_spatiotemporal.py +++ b/tutorials/stats-sensor-space/75_cluster_ftest_spatiotemporal.py @@ -41,7 +41,6 @@ from mne.channels import find_ch_adjacency from mne.datasets import sample from mne.stats import combine_adjacency, spatio_temporal_cluster_test -from mne.time_frequency import tfr_morlet from mne.viz import plot_compare_evokeds # %% @@ -269,9 +268,9 @@ epochs_power = list() for condition in [epochs[k] for k in ("Aud/L", "Vis/L")]: - this_tfr = tfr_morlet( - condition, - freqs, + this_tfr = condition.compute_tfr( + method="morlet", + freqs=freqs, n_cycles=n_cycles, decim=decim, average=False, diff --git a/tutorials/time-freq/20_sensors_time_frequency.py b/tutorials/time-freq/20_sensors_time_frequency.py index 247fdddfab1..9175e700041 100644 --- a/tutorials/time-freq/20_sensors_time_frequency.py +++ b/tutorials/time-freq/20_sensors_time_frequency.py @@ -10,7 +10,7 @@ We will use this dataset: :ref:`somato-dataset`. It contains so-called event related synchronizations (ERS) / desynchronizations (ERD) in the beta band. -""" +""" # noqa D400 # Authors: Alexandre Gramfort # Stefan Appelhoff # Richard Höchenberger @@ -24,7 +24,6 @@ import mne from mne.datasets import somato -from mne.time_frequency import tfr_morlet # %% # Set parameters @@ -190,14 +189,13 @@ # define frequencies of interest (log-spaced) freqs = np.logspace(*np.log10([6, 35]), num=8) n_cycles = freqs / 2.0 # different number of cycle per frequency -power, itc = tfr_morlet( - epochs, +power, itc = epochs.compute_tfr( + method="morlet", freqs=freqs, n_cycles=n_cycles, - use_fft=True, + average=True, return_itc=True, decim=3, - n_jobs=None, ) # %% @@ -210,7 +208,7 @@ # You can also select a portion in the time-frequency plane to # obtain a topomap for a certain time-frequency region. power.plot_topo(baseline=(-0.5, 0), mode="logratio", title="Average power") -power.plot([82], baseline=(-0.5, 0), mode="logratio", title=power.ch_names[82]) +power.plot(picks=[82], baseline=(-0.5, 0), mode="logratio", title=power.ch_names[82]) fig, axes = plt.subplots(1, 2, figsize=(7, 4), layout="constrained") topomap_kw = dict( From 6c4418c2dc00d8d84ee7fc29acc6d04dfc4e7fac Mon Sep 17 00:00:00 2001 From: Clemens Brunner Date: Mon, 25 Mar 2024 08:59:53 +0100 Subject: [PATCH 238/405] Add `physical_range="channelwise"` for EDF export (#12510) --- doc/changes/devel/12510.newfeature.rst | 1 + mne/export/_edf.py | 9 +++++--- mne/export/tests/test_export.py | 27 ++++++++++++++++++++++++ mne/utils/docs.py | 29 ++++++++++++++------------ 4 files changed, 50 insertions(+), 16 deletions(-) create mode 100644 doc/changes/devel/12510.newfeature.rst diff --git a/doc/changes/devel/12510.newfeature.rst b/doc/changes/devel/12510.newfeature.rst new file mode 100644 index 00000000000..3194e47e6a9 --- /dev/null +++ b/doc/changes/devel/12510.newfeature.rst @@ -0,0 +1 @@ +Add ``physical_range="channelwise"`` to :meth:`mne.io.Raw.export` for exporting to EDF, which can improve amplitude resolution if individual channels vary greatly in their offsets, by `Clemens Brunner`_. \ No newline at end of file diff --git a/mne/export/_edf.py b/mne/export/_edf.py index 68905d5ed7d..7f7111a36d8 100644 --- a/mne/export/_edf.py +++ b/mne/export/_edf.py @@ -79,6 +79,8 @@ def _export_raw(fname, raw, physical_range, add_ch_type): _data = raw.get_data(units=units, picks=_picks) ch_types_phys_max[_type] = _data.max() ch_types_phys_min[_type] = _data.min() + elif physical_range == "channelwise": + prange = None else: # get the physical min and max of the data in uV # Physical ranges of the data in uV are usually set by the manufacturer and @@ -101,6 +103,7 @@ def _export_raw(fname, raw, physical_range, add_ch_type): f" passed in {pmin}." ) data = np.clip(data, pmin, pmax) + prange = pmin, pmax signals = [] for idx, ch in enumerate(raw.ch_names): ch_type = ch_types[idx] @@ -112,10 +115,10 @@ def _export_raw(fname, raw, physical_range, add_ch_type): "before exporting to EDF." ) - if physical_range == "auto": - # take the channel type minimum and maximum + if physical_range == "auto": # per channel type pmin = ch_types_phys_min[ch_type] pmax = ch_types_phys_max[ch_type] + prange = pmin, pmax signals.append( EdfSignal( @@ -124,7 +127,7 @@ def _export_raw(fname, raw, physical_range, add_ch_type): label=signal_label, transducer_type="", physical_dimension="" if ch_type == "stim" else "uV", - physical_range=(pmin, pmax), + physical_range=prange, digital_range=(digital_min, digital_max), prefiltering=filter_str_info, ) diff --git a/mne/export/tests/test_export.py b/mne/export/tests/test_export.py index 62bbe57a87e..9ce72eb79de 100644 --- a/mne/export/tests/test_export.py +++ b/mne/export/tests/test_export.py @@ -189,6 +189,33 @@ def test_double_export_edf(tmp_path): assert_array_equal(orig_ch_types, read_ch_types) +@edfio_mark() +def test_edf_physical_range(tmp_path): + """Test exporting an EDF file with different physical range settings.""" + ch_types = ["eeg"] * 4 + ch_names = np.arange(len(ch_types)).astype(str).tolist() + fs = 1000 + info = create_info(len(ch_types), sfreq=fs, ch_types=ch_types) + data = np.tile( + np.sin(2 * np.pi * 10 * np.arange(0, 2, 1 / fs)) * 1e-5, (len(ch_names), 1) + ) + data = (data.T + [0.1, 0, 0, -0.1]).T # add offsets + raw = RawArray(data, info) + + # export with physical range per channel type (default) + temp_fname = tmp_path / "test_auto.edf" + raw.export(temp_fname) + raw_read = read_raw_edf(temp_fname, preload=True) + with pytest.raises(AssertionError, match="Arrays are not almost equal"): + assert_array_almost_equal(raw.get_data(), raw_read.get_data(), decimal=10) + + # export with physical range per channel + temp_fname = tmp_path / "test_per_channel.edf" + raw.export(temp_fname, physical_range="channelwise") + raw_read = read_raw_edf(temp_fname, preload=True) + assert_array_almost_equal(raw.get_data(), raw_read.get_data(), decimal=10) + + @edfio_mark() def test_export_edf_annotations(tmp_path): """Test that exporting EDF preserves annotations.""" diff --git a/mne/utils/docs.py b/mne/utils/docs.py index c82f9d74344..1c957505630 100644 --- a/mne/utils/docs.py +++ b/mne/utils/docs.py @@ -1401,15 +1401,19 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): ``EEG Fz`` or ``MISC E``), other software may not support this (optional) feature of the EDF standard. -If ``add_ch_type`` is True, then channel types are written based on what -they are currently set in MNE-Python. One should double check that all -their channels are set correctly. You can call -:attr:`raw.set_channel_types ` to set -channel types. +If ``add_ch_type`` is True, then channel types are written based on what they are +currently set in MNE-Python. One should double check that all their channels are set +correctly. You can call :meth:`mne.io.Raw.set_channel_types` to set channel types. -In addition, EDF does not support storing a montage. You will need -to store the montage separately and call :attr:`raw.set_montage() -`. +In addition, EDF does not support storing a montage. You will need to store the montage +separately and call :meth:`mne.io.Raw.set_montage`. + +The physical range of the signals is determined by signal type by default +(``physical_range="auto"``). However, if individual channel ranges vary significantly +due to the presence of e.g. drifts/offsets/biases, setting +``physical_range="channelwise"`` might be more appropriate. This will ensure a maximum +resolution for each individual channel, but some tools might not be able to handle this +appropriately (even though channel-wise ranges are covered by the EDF standard). """ docdict["export_eeglab_note"] = """ @@ -3201,11 +3205,10 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): docdict["physical_range_export_params"] = """ physical_range : str | tuple - The physical range of the data. If 'auto' (default), then - it will infer the physical min and max from the data itself, - taking the minimum and maximum values per channel type. - If it is a 2-tuple of minimum and maximum limit, then those - physical ranges will be used. Only used for exporting EDF files. + The physical range of the data. If 'auto' (default), the physical range is inferred + from the data, taking the minimum and maximum values per channel type. If + 'channelwise', the range will be defined per channel. If a tuple of minimum and + maximum, this manual physical range will be used. Only used for exporting EDF files. """ _pick_ori_novec = """ From 169372da67dc243b817f024820b349495a5aa109 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Mon, 25 Mar 2024 15:16:03 -0400 Subject: [PATCH 239/405] MAINT: Reenable NumPy 2.0 (#12511) --- mne/utils/config.py | 2 ++ tools/azure_dependencies.sh | 8 +++++--- tools/github_actions_dependencies.sh | 13 ++++++++++--- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/mne/utils/config.py b/mne/utils/config.py index 549f2d9547a..ded70b55650 100644 --- a/mne/utils/config.py +++ b/mne/utils/config.py @@ -659,6 +659,8 @@ def sys_info( "openmeeg", "cupy", "pandas", + "h5io", + "h5py", "", "# Visualization (optional)", "pyvista", diff --git a/tools/azure_dependencies.sh b/tools/azure_dependencies.sh index 47eae988efb..c680fb100d6 100755 --- a/tools/azure_dependencies.sh +++ b/tools/azure_dependencies.sh @@ -9,9 +9,9 @@ elif [ "${TEST_MODE}" == "pip-pre" ]; then # python -m pip install $STD_ARGS --only-binary ":all:" --extra-index-url "https://www.riverbankcomputing.com/pypi/simple" "PyQt6!=6.6.1,!=6.6.2" PyQt6-sip PyQt6-Qt6 "PyQt6-Qt6!=6.6.1,!=6.6.2" python -m pip install $STD_ARGS --only-binary ":all:" "PyQt6!=6.6.1,!=6.6.2" PyQt6-sip PyQt6-Qt6 "PyQt6-Qt6!=6.6.1,!=6.6.2" echo "Numpy etc." - python -m pip install $STD_ARGS --only-binary ":all:" --default-timeout=60 --extra-index-url "https://pypi.anaconda.org/scientific-python-nightly-wheels/simple" "numpy<2.0.0.dev0" "scipy>=1.14.0.dev0" "scikit-learn>=1.5.dev0" matplotlib pillow statsmodels pyarrow h5py - # echo "dipy" - # python -m pip install $STD_ARGS --only-binary ":all:" --extra-index-url "https://pypi.anaconda.org/scipy-wheels-nightly/simple" dipy + # No pyarrow yet https://github.com/apache/arrow/issues/40216 + # No h5py (and thus dipy) yet until they improve/refactor thier wheel building infrastructure for Windows + python -m pip install $STD_ARGS --only-binary ":all:" --default-timeout=60 --extra-index-url "https://pypi.anaconda.org/scientific-python-nightly-wheels/simple" "numpy>=2.1.0.dev0" "scipy>=1.14.0.dev0" "scikit-learn>=1.5.dev0" matplotlib pillow statsmodels echo "OpenMEEG" pip install $STD_ARGS --only-binary ":all:" --extra-index-url "https://test.pypi.org/simple" "openmeeg>=2.6.0.dev4" echo "vtk" @@ -29,6 +29,8 @@ elif [ "${TEST_MODE}" == "pip-pre" ]; then python -m pip install --progress-bar off git+https://github.com/joblib/joblib@master echo "EDFlib-Python" python -m pip install $STD_ARGS git+https://github.com/the-siesta-group/edfio + # echo "pysnirf2" # Needs h5py + # python -m pip install $STD_ARGS git+https://github.com/BUNPC/pysnirf2 ./tools/check_qt_import.sh PyQt6 python -m pip install $STD_ARGS -e .[test] else diff --git a/tools/github_actions_dependencies.sh b/tools/github_actions_dependencies.sh index 0902b3f0afa..c41e4c8e2d8 100755 --- a/tools/github_actions_dependencies.sh +++ b/tools/github_actions_dependencies.sh @@ -30,8 +30,11 @@ else # pip install $STD_ARGS --only-binary ":all:" --default-timeout=60 --extra-index-url https://www.riverbankcomputing.com/pypi/simple "PyQt6!=6.6.1,!=6.6.2" "PyQt6-Qt6!=6.6.1,!=6.6.2" pip install $STD_ARGS --only-binary ":all:" --default-timeout=60 "PyQt6!=6.6.1,!=6.6.2" "PyQt6-Qt6!=6.6.1,!=6.6.2" echo "NumPy/SciPy/pandas etc." - pip install $STD_ARGS --only-binary ":all:" --default-timeout=60 --extra-index-url "https://pypi.anaconda.org/scientific-python-nightly-wheels/simple" "numpy<2.0.0.dev0" "scipy>=1.14.0.dev0" "scikit-learn>=1.5.dev0" matplotlib pillow statsmodels pyarrow pandas h5py - # No dipy, python-picard (needs numexpr) until they update to NumPy 2.0 compat + # No pyarrow yet https://github.com/apache/arrow/issues/40216 + # No dipy yet https://github.com/dipy/dipy/issues/2979 + pip install $STD_ARGS --only-binary ":all:" --default-timeout=60 --index-url "https://pypi.anaconda.org/scientific-python-nightly-wheels/simple" "numpy>=2.1.0.dev0" h5py + pip install $STD_ARGS --only-binary ":all:" --default-timeout=60 --extra-index-url "https://pypi.anaconda.org/scientific-python-nightly-wheels/simple" "numpy>=2.1.0.dev0" "scipy>=1.14.0.dev0" "scikit-learn>=1.5.dev0" matplotlib pillow statsmodels pandas + # No python-picard (needs numexpr) until they update to NumPy 2.0 compat INSTALL_KIND="test_extra" echo "OpenMEEG" pip install $STD_ARGS --only-binary ":all:" --extra-index-url "https://test.pypi.org/simple" "openmeeg>=2.6.0.dev4" @@ -55,8 +58,12 @@ else pip install $STD_ARGS git+https://github.com/joblib/joblib@master echo "edfio" pip install $STD_ARGS git+https://github.com/the-siesta-group/edfio + echo "h5io" + pip install $STD_ARGS git+https://github.com/h5io/h5io + echo "pysnirf2" + pip install $STD_ARGS git+https://github.com/BUNPC/pysnirf2 # Make sure we're on a NumPy 2.0 variant - # python -c "import numpy as np; assert np.__version__[0] == '2', np.__version__" + python -c "import numpy as np; assert np.__version__[0] == '2', np.__version__" fi echo "" From 8ee98c95f4bc9866679fd49111f9b041b5b0e2fa Mon Sep 17 00:00:00 2001 From: Daniel McCloy Date: Mon, 25 Mar 2024 16:43:37 -0500 Subject: [PATCH 240/405] don't rely on inst_type to triage class constructors (#12514) --- mne/time_frequency/tests/test_tfr.py | 12 ++++++++++++ mne/time_frequency/tfr.py | 8 ++++++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/mne/time_frequency/tests/test_tfr.py b/mne/time_frequency/tests/test_tfr.py index 5087a8c46a9..9383aaec824 100644 --- a/mne/time_frequency/tests/test_tfr.py +++ b/mne/time_frequency/tests/test_tfr.py @@ -659,6 +659,18 @@ def test_tfr_io(inst, average_tfr, request, tmp_path): tfr.save(fname, overwrite=False) +def test_roundtrip_from_legacy_func(epochs, tmp_path): + """Test save/load with TFRs generated by legacy method (gh-12512).""" + pytest.importorskip("h5io") + + fname = tmp_path / "temp_tfr.hdf5" + tfr = tfr_morlet( + epochs, freqs=freqs_linspace, n_cycles=7, average=True, return_itc=False + ) + tfr.save(fname, overwrite=True) + assert read_tfrs(fname) == tfr + + def test_raw_tfr_init(raw): """Test the RawTFR and RawTFRArray constructors.""" one = RawTFR(inst=raw, method="morlet", freqs=freqs_linspace) diff --git a/mne/time_frequency/tfr.py b/mne/time_frequency/tfr.py index 97df892ad46..5a16cac80ed 100644 --- a/mne/time_frequency/tfr.py +++ b/mne/time_frequency/tfr.py @@ -4222,8 +4222,12 @@ def read_tfrs(fname, condition=None, *, verbose=None): hdf5_dict = read_hdf5(fname, title="mnepython", slash="replace") # single TFR from TFR.save() if "inst_type_str" in hdf5_dict: - inst_type_str = hdf5_dict["inst_type_str"] - Klass = dict(Epochs=EpochsTFR, Raw=RawTFR, Evoked=AverageTFR)[inst_type_str] + if hdf5_dict["data"].ndim == 4: + Klass = EpochsTFR + elif "nave" in hdf5_dict: + Klass = AverageTFR + else: + Klass = RawTFR out = Klass(inst=hdf5_dict) if getattr(out, "metadata", None) is not None: out.metadata = _prepare_read_metadata(out.metadata) From b56420f601a0675cdc211c24aa5d52e722ece099 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 25 Mar 2024 22:29:18 +0000 Subject: [PATCH 241/405] [pre-commit.ci] pre-commit autoupdate (#12515) 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 f08e4a367c1..a79245f366f 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.3 + rev: v0.3.4 hooks: - id: ruff name: ruff lint mne From eee8e6fe580034f4a3a4fb13bdca3bfc99240708 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Tue, 26 Mar 2024 06:44:03 -0400 Subject: [PATCH 242/405] ENH: Allow report reordering (#12513) --- doc/changes/devel/12513.newfeature.rst | 2 + mne/report/report.py | 98 +++++++++++++++++++------- mne/report/tests/test_report.py | 54 ++++++++++---- 3 files changed, 115 insertions(+), 39 deletions(-) create mode 100644 doc/changes/devel/12513.newfeature.rst diff --git a/doc/changes/devel/12513.newfeature.rst b/doc/changes/devel/12513.newfeature.rst new file mode 100644 index 00000000000..7189adaf3c0 --- /dev/null +++ b/doc/changes/devel/12513.newfeature.rst @@ -0,0 +1,2 @@ +Added the ability to reorder report contents via :meth:`mne.Report.reorder` (with +helper to get contents with :meth:`mne.Report.get_contents`), by `Eric Larson`_. diff --git a/mne/report/report.py b/mne/report/report.py index 30fd2e691fd..b2fafe5b446 100644 --- a/mne/report/report.py +++ b/mne/report/report.py @@ -8,6 +8,7 @@ # Copyright the MNE-Python contributors. import base64 +import copy import dataclasses import fnmatch import io @@ -965,6 +966,64 @@ def _validate_input(self, items, captions, tag, comments=None): ) return items, captions, comments + def copy(self): + """Return a deepcopy of the report. + + Returns + ------- + report : instance of Report + The copied report. + """ + return copy.deepcopy(self) + + def get_contents(self): + """Get the content of the report. + + Returns + ------- + titles : list of str + The title of each content element. + tags : list of list of str + The tags for each content element, one list per element. + htmls : list of str + The HTML contents for each element. + + Notes + ----- + .. versionadded:: 1.7 + """ + htmls, _, titles, tags = self._content_as_html() + return titles, tags, htmls + + def reorder(self, order): + """Reorder the report content. + + Parameters + ---------- + order : array-like of int + The indices of the new order (as if you were reordering an array). + For example if there are 4 elements in the report, + ``order=[3, 0, 1, 2]`` would take the last element and move it to + the front. In other words, ``elements = [elements[ii] for ii in order]]``. + + Notes + ----- + .. versionadded:: 1.7 + """ + _validate_type(order, "array-like", "order") + order = np.array(order) + if order.dtype.kind != "i" or order.ndim != 1: + raise ValueError( + "order must be an array of integers, got " + f"{order.ndim}D array of dtype {order.dtype}" + ) + n_elements = len(self._content) + if not np.array_equal(np.sort(order), np.arange(n_elements)): + raise ValueError( + f"order must be a permutation of range({n_elements}), got:\n{order}" + ) + self._content = [self._content[ii] for ii in order] + def _content_as_html(self): """Generate HTML representations based on the added content & sections. @@ -1039,18 +1098,12 @@ def _content_as_html(self): @property def html(self): """A list of HTML representations for all content elements.""" - htmls, _, _, _ = self._content_as_html() - return htmls + return self._content_as_html()[0] @property def tags(self): - """All tags currently used in the report.""" - tags = [] - for c in self._content: - tags.extend(c.tags) - - tags = tuple(sorted(set(tags))) - return tags + """A sorted tuple of all tags currently used in the report.""" + return tuple(sorted(set(sum(self._content_as_html()[3], ())))) def add_custom_css(self, css): """Add custom CSS to the report. @@ -2875,7 +2928,7 @@ def parse_folder( ) if sort_content: - self._content = self._sort(content=self._content, order=CONTENT_ORDER) + self._sort(order=CONTENT_ORDER) def __getstate__(self): """Get the state of the report as a dictionary.""" @@ -2954,7 +3007,7 @@ def save( fname = op.realpath(fname) # resolve symlinks if sort_content: - self._content = self._sort(content=self._content, order=CONTENT_ORDER) + self._sort(order=CONTENT_ORDER) if not overwrite and op.isfile(fname): msg = ( @@ -3017,30 +3070,23 @@ def __exit__(self, exception_type, value, traceback): if self.fname is not None: self.save(self.fname, open_browser=False, overwrite=True) - @staticmethod - def _sort(content, order): + def _sort(self, *, order): """Reorder content to reflect "natural" ordering.""" - content_unsorted = content.copy() - content_sorted = [] content_sorted_idx = [] - del content # First arrange content with known tags in the predefined order for tag in order: - for idx, content in enumerate(content_unsorted): + for idx, content in enumerate(self._content): if tag in content.tags: content_sorted_idx.append(idx) - content_sorted.append(content) # Now simply append the rest (custom tags) - content_remaining = [ - content - for idx, content in enumerate(content_unsorted) - if idx not in content_sorted_idx - ] - - content_sorted = [*content_sorted, *content_remaining] - return content_sorted + self.reorder( + np.r_[ + content_sorted_idx, + np.setdiff1d(np.arange(len(self._content)), content_sorted_idx), + ] + ) def _render_one_bem_axis( self, diff --git a/mne/report/tests/test_report.py b/mne/report/tests/test_report.py index 437cfec3cc7..3860e227318 100644 --- a/mne/report/tests/test_report.py +++ b/mne/report/tests/test_report.py @@ -5,7 +5,6 @@ # Copyright the MNE-Python contributors. import base64 -import copy import glob import os import pickle @@ -638,7 +637,7 @@ def test_remove(): r.add_figure(fig=fig2, title="figure2", tags=("slider",)) # Test removal by title - r2 = copy.deepcopy(r) + r2 = r.copy() removed_index = r2.remove(title="figure1") assert removed_index == 2 assert len(r2.html) == 3 @@ -647,7 +646,7 @@ def test_remove(): assert r2.html[2] == r.html[3] # Test restricting to section - r2 = copy.deepcopy(r) + r2 = r.copy() removed_index = r2.remove(title="figure1", tags=("othertag",)) assert removed_index == 1 assert len(r2.html) == 3 @@ -692,7 +691,7 @@ def test_add_or_replace(tags): assert len(r.html) == 4 assert len(r._content) == 4 - old_r = copy.deepcopy(r) + old_r = r.copy() # Replace our last occurrence of title='duplicate' r.add_figure( @@ -765,7 +764,7 @@ def test_add_or_replace_section(): assert len(r.html) == 3 assert len(r._content) == 3 - old_r = copy.deepcopy(r) + old_r = r.copy() assert r.html[0] == old_r.html[0] assert r.html[1] == old_r.html[1] assert r.html[2] == old_r.html[2] @@ -1108,24 +1107,53 @@ def test_sorting(tmp_path): """Test that automated ordering based on tags works.""" r = Report() - r.add_code(code="E = m * c**2", title="intelligence >9000", tags=("bem",)) - r.add_code(code="a**2 + b**2 = c**2", title="Pythagoras", tags=("evoked",)) - r.add_code(code="🧠", title="source of truth", tags=("source-estimate",)) - r.add_code(code="🥦", title="veggies", tags=("raw",)) + titles = ["intelligence >9000", "Pythagoras", "source of truth", "veggies"] + r.add_code(code="E = m * c**2", title=titles[0], tags=("bem",)) + r.add_code(code="a**2 + b**2 = c**2", title=titles[1], tags=("evoked",)) + r.add_code(code="🧠", title=titles[2], tags=("source-estimate",)) + r.add_code(code="🥦", title=titles[3], tags=("raw",)) # Check that repeated calls of add_* actually continuously appended to # the report orig_order = ["bem", "evoked", "source-estimate", "raw"] assert [c.tags[0] for c in r._content] == orig_order + # tags property behavior and get_contents + assert list(r.tags) == sorted(orig_order) + titles, tags, htmls = r.get_contents() + assert set(sum(tags, ())) == set(r.tags) + assert len(titles) == len(tags) == len(htmls) == len(r._content) + for title, tag, html in zip(titles, tags, htmls): + title = title.replace(">", ">") + assert title in html + for t in tag: + assert t in html + # Now check the actual sorting - content_sorted = r._sort(content=r._content, order=CONTENT_ORDER) + r_sorted = r.copy() + r_sorted._sort(order=CONTENT_ORDER) expected_order = ["raw", "evoked", "bem", "source-estimate"] - assert content_sorted != r._content - assert [c.tags[0] for c in content_sorted] == expected_order + assert r_sorted._content != r._content + assert [c.tags[0] for c in r_sorted._content] == expected_order + assert [c.tags[0] for c in r._content] == orig_order + + r.copy().save(fname=tmp_path / "report.html", sort_content=True, open_browser=False) + + # Manual sorting should be the same + r_sorted = r.copy() + order = np.argsort([CONTENT_ORDER.index(t) for t in orig_order]) + r_sorted.reorder(order) + + assert r_sorted._content != r._content + got_order = [c.tags[0] for c in r_sorted._content] + assert [c.tags[0] for c in r._content] == orig_order # original unmodified + assert got_order == expected_order - r.save(fname=tmp_path / "report.html", sort_content=True, open_browser=False) + with pytest.raises(ValueError, match="order must be a permutation"): + r.reorder(np.arange(len(r._content) + 1)) + with pytest.raises(ValueError, match="array of integers"): + r.reorder([1.0]) @pytest.mark.parametrize( From c4d20c37d0c3f2a337428811f0e627ccdb078a09 Mon Sep 17 00:00:00 2001 From: Nabil Alibou <63203348+nabilalibou@users.noreply.github.com> Date: Fri, 29 Mar 2024 15:35:18 +0100 Subject: [PATCH 243/405] Improve consistency of sensor types in code and documentation (#12509) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Eric Larson --- doc/_includes/channel_types.rst | 90 ++++++++++++++++++++---------- doc/changes/devel/12509.other.rst | 2 + doc/documentation/glossary.rst | 19 +++++-- doc/sphinxext/mne_substitutions.py | 24 +++++++- mne/_fiff/meas_info.py | 35 +++++++----- mne/_fiff/pick.py | 7 +-- mne/defaults.py | 6 ++ tutorials/raw/10_raw_overview.py | 9 ++- 8 files changed, 138 insertions(+), 54 deletions(-) create mode 100644 doc/changes/devel/12509.other.rst diff --git a/doc/_includes/channel_types.rst b/doc/_includes/channel_types.rst index 647dab25ba4..0a2ea0ab007 100644 --- a/doc/_includes/channel_types.rst +++ b/doc/_includes/channel_types.rst @@ -10,6 +10,11 @@ Supported channel types from the include: channel-types-begin-content +.. NOTE: In the future, this table should be automatically synchronized with + the sensor types listed in the glossary. Perhaps a table showing data type + channels as well as non-data type channels should be added to the glossary + and displayed here too. + Channel types are represented in MNE-Python with shortened or abbreviated names. This page lists all supported channel types, their abbreviated names, and the measurement unit used to represent data of that type. Where channel @@ -23,50 +28,77 @@ parentheses. More information about measurement units is given in the .. cssclass:: table-bordered .. rst-class:: midvalign -============= ========================================= ================= -Channel type Description Measurement unit -============= ========================================= ================= -eeg scalp electroencephalography (EEG) Volts +================= ========================================= ================= +Channel type Description Measurement unit +================= ========================================= ================= +eeg scalp electroencephalography (EEG) Volts + +meg (mag) Magnetoencephalography (magnetometers) Teslas + +meg (grad) Magnetoencephalography (gradiometers) Teslas/meter + +ecg Electrocardiography (ECG) Volts + +seeg Stereotactic EEG channels Volts + +dbs Deep brain stimulation (DBS) Volts + +ecog Electrocorticography (ECoG) Volts + +fnirs (hbo) Functional near-infrared spectroscopy Moles/liter + (oxyhemoglobin) + +fnirs (hbr) Functional near-infrared spectroscopy Moles/liter + (deoxyhemoglobin) + +emg Electromyography (EMG) Volts + +eog Electrooculography (EOG) Volts + +bio Miscellaneous biological channels (e.g., Arbitrary units + skin conductance) -meg (mag) Magnetoencephalography (magnetometers) Teslas +stim stimulus (a.k.a. trigger) channels Arbitrary units -meg (grad) Magnetoencephalography (gradiometers) Teslas/meter +resp respiration monitoring channel Volts -ecg Electrocardiography (ECG) Volts +chpi continuous head position indicator Teslas + (HPI) coil channels -seeg Stereotactic EEG channels Volts +exci Flux excitation channel -dbs Deep brain stimulation (DBS) Volts +ias Internal Active Shielding data + (Triux systems only?) -ecog Electrocorticography (ECoG) Volts +syst System status channel information + (Triux systems only) -fnirs (hbo) Functional near-infrared spectroscopy Moles/liter - (oxyhemoglobin) +temperature Temperature Degrees Celsius -fnirs (hbr) Functional near-infrared spectroscopy Moles/liter - (deoxyhemoglobin) +gsr Galvanic skin response Siemens -emg Electromyography (EMG) Volts +ref_meg Reference Magnetometers Teslas -bio Miscellaneous biological channels (e.g., Arbitrary units - skin conductance) +dipole Dipole amplitude Amperes -stim stimulus (a.k.a. trigger) channels Arbitrary units +gof Goodness of fit (GOF) Goodness-of-fit -resp respiration monitoring channel Volts +cw-nirs (amp) Continuous-wave functional near-infrared Volts + spectroscopy (CW-fNIRS) (CW amplitude) -chpi continuous head position indicator Teslas - (HPI) coil channels +fd-nirs (ac amp) Frequency-domain near-infrared Volts + spectroscopy (FD-NIRS AC amplitude) -exci Flux excitation channel +fd-nirs (phase) Frequency-domain near-infrared Radians + spectroscopy (FD-NIRS phase) -ias Internal Active Shielding data - (Triux systems only?) +fnirs (od) Functional near-infrared spectroscopy Volts + (optical density) -syst System status channel information - (Triux systems only) +csd Current source density Volts per square + meter -temperature Temperature Degrees Celsius +eyegaze Eye-tracking (gaze position) Arbitrary units -gsr Galvanic skin response Siemens -============= ========================================= ================= +pupil Eye-tracking (pupil size) Arbitrary units +================= ========================================= ================= \ No newline at end of file diff --git a/doc/changes/devel/12509.other.rst b/doc/changes/devel/12509.other.rst new file mode 100644 index 00000000000..e3709653025 --- /dev/null +++ b/doc/changes/devel/12509.other.rst @@ -0,0 +1,2 @@ +Update the list of sensor types in docstrings, tutorials and the glossary by `Nabil Alibou`_. + diff --git a/doc/documentation/glossary.rst b/doc/documentation/glossary.rst index 91b8922e8c6..89a5c477a75 100644 --- a/doc/documentation/glossary.rst +++ b/doc/documentation/glossary.rst @@ -41,15 +41,15 @@ general neuroimaging concepts. If you think a term is missing, please consider Channels refer to MEG sensors, EEG electrodes or other sensors such as EOG, ECG, sEEG, ECoG, etc. Channels usually have a type (such as gradiometer), and a unit (such as T/m) used e.g. for - plotting. See also :term:`data channels`. + plotting. See also :term:`data channels` and :term:`non-data channels`. data channels Many functions in MNE-Python operate on "data channels" by default. These are channels that contain electrophysiological data from the brain, as opposed to other channel types such as EOG, ECG, stimulus/trigger, - or acquisition system status data. The set of channels considered - "data channels" in MNE contains the following types (together with scale - factors for plotting): + or acquisition system status data (see :term:`non-data channels`). + The set of channels considered "data channels" in MNE contains the + following types (together with scale factors for plotting): .. mne:: data channels list @@ -287,6 +287,13 @@ general neuroimaging concepts. If you think a term is missing, please consider data into a common space for statistical analysis. See :ref:`ch_morph` for more details. + non-data channels + All types of channels other than :term:`data channels`. + The set of channels considered "non-data channels" in MNE contains the + following types (together with scale factors for plotting): + + .. mne:: non-data channels list + OPM optically pumped magnetometer An optically pumped magnetometer (OPM) is a type of magnetometer @@ -350,6 +357,10 @@ general neuroimaging concepts. If you think a term is missing, please consider A selection is a set of picked channels (for example, all sensors falling within a :term:`region of interest`). + sensor types + All the sensors handled by MNE-Python can be divided into two categories: + :term:`data channels` and :term:`non-data channels`. + STC source estimate source time course diff --git a/doc/sphinxext/mne_substitutions.py b/doc/sphinxext/mne_substitutions.py index 23196e795f6..0c4f9a2f3dd 100644 --- a/doc/sphinxext/mne_substitutions.py +++ b/doc/sphinxext/mne_substitutions.py @@ -7,6 +7,7 @@ from mne._fiff.pick import ( _DATA_CH_TYPES_ORDER_DEFAULT, _DATA_CH_TYPES_SPLIT, + _EYETRACK_CH_TYPES_SPLIT, _PICK_TYPES_DATA_DICT, ) from mne.defaults import DEFAULTS @@ -30,10 +31,31 @@ def run(self, **kwargs): # noqa: D102 keys.append(key) rst = "- " + "\n- ".join( f"``{repr(key)}``: **{DEFAULTS['titles'][key]}** " - f"(scaled by {DEFAULTS['scalings'][key]} to " + f"(scaled by {DEFAULTS['scalings'][key]:g} to " f"plot in *{DEFAULTS['units'][key]}*)" for key in keys ) + elif self.arguments[0] == "non-data channels list": + keys = list() + rst = "" + for key in _DATA_CH_TYPES_ORDER_DEFAULT: + if ( + not _PICK_TYPES_DATA_DICT.get(key, True) + or key in _EYETRACK_CH_TYPES_SPLIT + or key in ("ref_meg", "whitened") + ): + keys.append(key) + for key in keys: + if DEFAULTS["scalings"].get(key, False) and DEFAULTS["units"].get( + key, False + ): + rst += ( + f"- ``{repr(key)}``: **{DEFAULTS['titles'][key]}** " + f"(scaled by {DEFAULTS['scalings'][key]:g} to " + f"plot in *{DEFAULTS['units'][key]}*)\n" + ) + else: + rst += f"- ``{repr(key)}``: **{DEFAULTS['titles'][key]}**\n" else: raise self.error( "MNE directive unknown in %s: %r" # noqa: UP031 diff --git a/mne/_fiff/meas_info.py b/mne/_fiff/meas_info.py index 797e3d4bbaa..a2928a9f2a6 100644 --- a/mne/_fiff/meas_info.py +++ b/mne/_fiff/meas_info.py @@ -545,12 +545,12 @@ def set_channel_types(self, mapping, *, on_unit_change="warn", verbose=None): Notes ----- - The following sensor types are accepted: + The following :term:`sensor types` are accepted: - ecg, eeg, emg, eog, exci, ias, misc, resp, seeg, dbs, stim, syst, - ecog, hbo, hbr, fnirs_cw_amplitude, fnirs_fd_ac_amplitude, - fnirs_fd_phase, fnirs_od, eyetrack_pos, eyetrack_pupil, - temperature, gsr + bio, chpi, csd, dbs, dipole, ecg, ecog, eeg, emg, eog, exci, + eyegaze, fnirs_cw_amplitude, fnirs_fd_ac_amplitude, fnirs_fd_phase, + fnirs_od, gof, gsr, hbo, hbr, ias, misc, pupil, ref_meg, resp, + seeg, stim, syst, temperature. .. versionadded:: 0.9.0 """ @@ -3155,11 +3155,14 @@ def create_info(ch_names, sfreq, ch_types="misc", verbose=None): sfreq : float Sample rate of the data. ch_types : list of str | str - Channel types, default is ``'misc'`` which is not a - :term:`data channel `. - Currently supported fields are 'ecg', 'bio', 'stim', 'eog', 'misc', - 'seeg', 'dbs', 'ecog', 'mag', 'eeg', 'ref_meg', 'grad', 'emg', 'hbr' - 'eyetrack' or 'hbo'. + Channel types, default is ``'misc'`` which is a + :term:`non-data channel `. + Currently supported fields are 'bio', 'chpi', 'csd', 'dbs', 'dipole', + 'ecg', 'ecog', 'eeg', 'emg', 'eog', 'exci', 'eyegaze', + 'fnirs_cw_amplitude', 'fnirs_fd_ac_amplitude', 'fnirs_fd_phase', + 'fnirs_od', 'gof', 'gsr', 'hbo', 'hbr', 'ias', 'misc', 'pupil', + 'ref_meg', 'resp', 'seeg', 'stim', 'syst', 'temperature' (see also + :term:`sensor types`). If str, then all channels are assumed to be of the same type. %(verbose)s @@ -3179,12 +3182,18 @@ def create_info(ch_names, sfreq, ch_types="misc", verbose=None): Proper units of measure: - * V: eeg, eog, seeg, dbs, emg, ecg, bio, ecog - * T: mag + * V: eeg, eog, seeg, dbs, emg, ecg, bio, ecog, resp, fnirs_fd_ac_amplitude, + fnirs_cw_amplitude, fnirs_od + * T: mag, chpi, ref_meg * T/m: grad * M: hbo, hbr + * rad: fnirs_fd_phase * Am: dipole - * AU: misc + * S: gsr + * C: temperature + * V/m²: csd + * GOF: gof + * AU: misc, stim, eyegaze, pupil """ try: ch_names = operator.index(ch_names) # int-like diff --git a/mne/_fiff/pick.py b/mne/_fiff/pick.py index 9e2e369ab71..c6acd26ade0 100644 --- a/mne/_fiff/pick.py +++ b/mne/_fiff/pick.py @@ -237,10 +237,9 @@ def channel_type(info, idx): type : str Type of channel. Will be one of:: - {'grad', 'mag', 'eeg', 'csd', 'stim', 'eog', 'emg', 'ecg', - 'ref_meg', 'resp', 'exci', 'ias', 'syst', 'misc', 'seeg', 'dbs', - 'bio', 'chpi', 'dipole', 'gof', 'ecog', 'hbo', 'hbr', - 'temperature', 'gsr', 'eyetrack'} + {'bio', 'chpi', 'dbs', 'dipole', 'ecg', 'ecog', 'eeg', 'emg', + 'eog', 'exci', 'eyetrack', 'fnirs', 'gof', 'gsr', 'ias', 'misc', + 'meg', 'ref_meg', 'resp', 'seeg', 'stim', 'syst', 'temperature'} """ # This is faster than the original _channel_type_old now in test_pick.py # because it uses (at most!) two dict lookups plus one conditional diff --git a/mne/defaults.py b/mne/defaults.py index 31fc53299e9..0418feb6788 100644 --- a/mne/defaults.py +++ b/mne/defaults.py @@ -216,6 +216,12 @@ temperature="Temperature", eyegaze="Eye-tracking (Gaze position)", pupil="Eye-tracking (Pupil size)", + resp="Respiration monitoring channel", + chpi="Continuous head position indicator (HPI) coil channels", + exci="Flux excitation channel", + ias="Internal Active Shielding data (Triux systems)", + syst="System status channel information (Triux systems)", + whitened="Whitened data", ), mask_params=dict( marker="o", diff --git a/tutorials/raw/10_raw_overview.py b/tutorials/raw/10_raw_overview.py index dbfb2b28467..bf8fe20effd 100644 --- a/tutorials/raw/10_raw_overview.py +++ b/tutorials/raw/10_raw_overview.py @@ -291,9 +291,12 @@ # inaccurate, you can change the type of any channel with the # :meth:`~mne.io.Raw.set_channel_types` method. The method takes a # :class:`dictionary ` mapping channel names to types; allowed types are -# ``ecg, eeg, emg, eog, exci, ias, misc, resp, seeg, dbs, stim, syst, ecog, -# hbo, hbr``. A common use case for changing channel type is when using frontal -# EEG electrodes as makeshift EOG channels: +# ``bio, chpi, csd, dbs, dipole, ecg, ecog, eeg, emg, eog, exci, eyegaze, +# fnirs_cw_amplitude, fnirs_fd_ac_amplitude, fnirs_fd_phase, fnirs_od, gof, +# gsr, hbo, hbr, ias, misc, pupil, ref_meg, resp, seeg, stim, syst, +# temperature`` (see :term:`sensor types` for more information about them). +# A common use case for changing channel type is when using frontal EEG +# electrodes as makeshift EOG channels: raw.set_channel_types({"EEG_001": "eog"}) print(raw.copy().pick(picks="eog").ch_names) From 5a1d009d02080f594414e45fc94543c035740e37 Mon Sep 17 00:00:00 2001 From: rcmdnk Date: Fri, 29 Mar 2024 23:50:09 +0900 Subject: [PATCH 244/405] Add exclude_after_unique option to mne.io.read_raw_edf/read_raw_edf to search for exclude channels after making channel names unique (#12518) --- doc/changes/devel/12518.newfeature.rst | 1 + mne/io/edf/edf.py | 55 ++++++++++++++++---- mne/io/edf/tests/test_edf.py | 70 +++++++++++++++++++++++++- mne/utils/docs.py | 9 ++++ 4 files changed, 124 insertions(+), 11 deletions(-) create mode 100644 doc/changes/devel/12518.newfeature.rst diff --git a/doc/changes/devel/12518.newfeature.rst b/doc/changes/devel/12518.newfeature.rst new file mode 100644 index 00000000000..306254ee6be --- /dev/null +++ b/doc/changes/devel/12518.newfeature.rst @@ -0,0 +1 @@ +Add ``exclude_after_unique`` option to :meth:`mne.io.read_raw_edf` and :meth:`mne.io.read_raw_edf` to search for exclude channels after making channels names unique, by `Michiru Kaneda`_ diff --git a/mne/io/edf/edf.py b/mne/io/edf/edf.py index 7a329b6af64..ec62eee168a 100644 --- a/mne/io/edf/edf.py +++ b/mne/io/edf/edf.py @@ -87,6 +87,7 @@ class RawEDF(BaseRaw): %(preload)s %(units_edf_bdf_io)s %(encoding_edf)s + %(exclude_after_unique)s %(verbose)s See Also @@ -148,13 +149,22 @@ def __init__( include=None, units=None, encoding="utf8", + exclude_after_unique=False, *, verbose=None, ): logger.info(f"Extracting EDF parameters from {input_fname}...") input_fname = os.path.abspath(input_fname) info, edf_info, orig_units = _get_info( - input_fname, stim_channel, eog, misc, exclude, infer_types, preload, include + input_fname, + stim_channel, + eog, + misc, + exclude, + infer_types, + preload, + include, + exclude_after_unique, ) logger.info("Creating raw.info structure...") @@ -473,7 +483,8 @@ def _read_segment_file(data, idx, fi, start, stop, raw_extras, filenames, cals, return tal_data -def _read_header(fname, exclude, infer_types, include=None): +@fill_doc +def _read_header(fname, exclude, infer_types, include=None, exclude_after_unique=False): """Unify EDF, BDF and GDF _read_header call. Parameters @@ -495,6 +506,7 @@ def _read_header(fname, exclude, infer_types, include=None): include : list of str | str Channel names to be included. A str is interpreted as a regular expression. 'exclude' must be empty if include is assigned. + %(exclude_after_unique)s Returns ------- @@ -503,7 +515,9 @@ def _read_header(fname, exclude, infer_types, include=None): ext = os.path.splitext(fname)[1][1:].lower() logger.info("%s file detected" % ext.upper()) if ext in ("bdf", "edf"): - return _read_edf_header(fname, exclude, infer_types, include) + return _read_edf_header( + fname, exclude, infer_types, include, exclude_after_unique + ) elif ext == "gdf": return _read_gdf_header(fname, exclude, include), None else: @@ -513,13 +527,23 @@ def _read_header(fname, exclude, infer_types, include=None): def _get_info( - fname, stim_channel, eog, misc, exclude, infer_types, preload, include=None + fname, + stim_channel, + eog, + misc, + exclude, + infer_types, + preload, + include=None, + exclude_after_unique=False, ): """Extract information from EDF+, BDF or GDF file.""" eog = eog if eog is not None else [] misc = misc if misc is not None else [] - edf_info, orig_units = _read_header(fname, exclude, infer_types, include) + edf_info, orig_units = _read_header( + fname, exclude, infer_types, include, exclude_after_unique + ) # XXX: `tal_ch_names` to pass to `_check_stim_channel` should be computed # from `edf_info['ch_names']` and `edf_info['tal_idx']` but 'tal_idx' @@ -790,7 +814,9 @@ def _edf_str_num(x): return _edf_str(x).replace(",", ".") -def _read_edf_header(fname, exclude, infer_types, include=None): +def _read_edf_header( + fname, exclude, infer_types, include=None, exclude_after_unique=False +): """Read header information from EDF+ or BDF file.""" edf_info = {"events": []} @@ -913,8 +939,12 @@ def _read_edf_header(fname, exclude, infer_types, include=None): else: ch_types, ch_names = ["EEG"] * nchan, ch_labels - exclude = _find_exclude_idx(ch_names, exclude, include) tal_idx = _find_tal_idx(ch_names) + if exclude_after_unique: + # make sure channel names are unique + ch_names = _unique_channel_names(ch_names) + + exclude = _find_exclude_idx(ch_names, exclude, include) exclude = np.concatenate([exclude, tal_idx]) sel = np.setdiff1d(np.arange(len(ch_names)), exclude) for ch in channels: @@ -936,8 +966,9 @@ def _read_edf_header(fname, exclude, infer_types, include=None): ch_names = [ch_names[idx] for idx in sel] units = [units[idx] for idx in sel] - # make sure channel names are unique - ch_names = _unique_channel_names(ch_names) + if not exclude_after_unique: + # make sure channel names are unique + ch_names = _unique_channel_names(ch_names) orig_units = dict(zip(ch_names, units)) physical_min = np.array([float(_edf_str_num(fid.read(8))) for ch in channels])[ @@ -1570,6 +1601,7 @@ def read_raw_edf( preload=False, units=None, encoding="utf8", + exclude_after_unique=False, *, verbose=None, ) -> RawEDF: @@ -1614,6 +1646,7 @@ def read_raw_edf( %(preload)s %(units_edf_bdf_io)s %(encoding_edf)s + %(exclude_after_unique)s %(verbose)s Returns @@ -1688,6 +1721,7 @@ def read_raw_edf( include=include, units=units, encoding=encoding, + exclude_after_unique=exclude_after_unique, verbose=verbose, ) @@ -1704,6 +1738,7 @@ def read_raw_bdf( preload=False, units=None, encoding="utf8", + exclude_after_unique=False, *, verbose=None, ) -> RawEDF: @@ -1748,6 +1783,7 @@ def read_raw_bdf( %(preload)s %(units_edf_bdf_io)s %(encoding_edf)s + %(exclude_after_unique)s %(verbose)s Returns @@ -1819,6 +1855,7 @@ def read_raw_bdf( include=include, units=units, encoding=encoding, + exclude_after_unique=exclude_after_unique, verbose=verbose, ) diff --git a/mne/io/edf/tests/test_edf.py b/mne/io/edf/tests/test_edf.py index bc6250e28a6..8ae55fdcc11 100644 --- a/mne/io/edf/tests/test_edf.py +++ b/mne/io/edf/tests/test_edf.py @@ -731,9 +731,23 @@ def test_edf_stim_ch_pick_up(test_input, EXPECTED): @testing.requires_testing_data -def test_bdf_multiple_annotation_channels(): +@pytest.mark.parametrize( + "exclude_after_unique, warns", + [ + (False, False), + (True, True), + ], +) +def test_bdf_multiple_annotation_channels(exclude_after_unique, warns): """Test BDF with multiple annotation channels.""" - raw = read_raw_bdf(bdf_multiple_annotations_path) + if warns: + ctx = pytest.warns(RuntimeWarning, match="Channel names are not unique") + else: + ctx = nullcontext() + with ctx: + raw = read_raw_bdf( + bdf_multiple_annotations_path, exclude_after_unique=exclude_after_unique + ) assert len(raw.annotations) == 10 descriptions = np.array( [ @@ -886,6 +900,32 @@ def test_exclude(): assert ch not in raw.ch_names +@pytest.mark.parametrize( + "EXPECTED, exclude, exclude_after_unique, warns", + [ + (["EEG F2-Ref"], "EEG F1-Ref", False, False), + (["EEG F1-Ref-0", "EEG F2-Ref", "EEG F1-Ref-1"], "EEG F1-Ref-1", False, True), + (["EEG F2-Ref"], ["EEG F1-Ref"], False, False), + (["EEG F2-Ref"], "EEG F1-Ref", True, True), + (["EEG F1-Ref-0", "EEG F2-Ref"], "EEG F1-Ref-1", True, True), + (["EEG F1-Ref-0", "EEG F2-Ref", "EEG F1-Ref-1"], ["EEG F1-Ref"], True, True), + ], +) +def test_exclude_duplicate_channel_data(exclude, exclude_after_unique, warns, EXPECTED): + """Test exclude parameter for duplicate channel data.""" + if warns: + ctx = pytest.warns(RuntimeWarning, match="Channel names are not unique") + else: + ctx = nullcontext() + with ctx: + raw = read_raw_edf( + duplicate_channel_labels_path, + exclude=exclude, + exclude_after_unique=exclude_after_unique, + ) + assert raw.ch_names == EXPECTED + + def test_include(): """Test include parameter.""" raw = read_raw_edf(edf_path, include=["I1", "I2"]) @@ -899,6 +939,32 @@ def test_include(): assert str(e.value) == "'exclude' must be empty" "if 'include' is assigned." +@pytest.mark.parametrize( + "EXPECTED, include, exclude_after_unique, warns", + [ + (["EEG F1-Ref-0", "EEG F1-Ref-1"], "EEG F1-Ref", False, True), + ([], "EEG F1-Ref-1", False, False), + (["EEG F1-Ref-0", "EEG F1-Ref-1"], ["EEG F1-Ref"], False, True), + (["EEG F1-Ref-0", "EEG F1-Ref-1"], "EEG F1-Ref", True, True), + (["EEG F1-Ref-1"], "EEG F1-Ref-1", True, True), + ([], ["EEG F1-Ref"], True, True), + ], +) +def test_include_duplicate_channel_data(include, exclude_after_unique, warns, EXPECTED): + """Test include parameter for duplicate channel data.""" + if warns: + ctx = pytest.warns(RuntimeWarning, match="Channel names are not unique") + else: + ctx = nullcontext() + with ctx: + raw = read_raw_edf( + duplicate_channel_labels_path, + include=include, + exclude_after_unique=exclude_after_unique, + ) + assert raw.ch_names == EXPECTED + + @testing.requires_testing_data def test_ch_types(): """Test reading of channel types from EDF channel label.""" diff --git a/mne/utils/docs.py b/mne/utils/docs.py index 1c957505630..3b4811548cf 100644 --- a/mne/utils/docs.py +++ b/mne/utils/docs.py @@ -1370,6 +1370,15 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): dictionary. """ +docdict["exclude_after_unique"] = """ +exclude_after_unique : bool + If True, exclude channels are searched for after they have been made + unique. This is useful to choose channels that have been made unique + by adding a suffix. If False, the original names are checked. + + .. versionchanged:: 1.7 +""" + docdict["exclude_clust"] = """ exclude : bool array or None Mask to apply to the data to exclude certain points from clustering From 58a02c25998f46b9de587cc76a51d6351d96e415 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Fri, 29 Mar 2024 15:12:23 -0400 Subject: [PATCH 245/405] MAINT: Restore 2 jobs on Windows (#12520) --- .github/workflows/tests.yml | 9 ++- azure-pipelines.yml | 6 +- mne/export/_edf.py | 19 ++++++- mne/export/tests/test_export.py | 2 +- mne/morph.py | 19 ++++--- mne/utils/dataframe.py | 13 ++++- tools/azure_dependencies.sh | 38 +++---------- tools/github_actions_dependencies.sh | 44 +-------------- tools/install_pre_requirements.sh | 82 ++++++++++++++++++++++++++++ 9 files changed, 138 insertions(+), 94 deletions(-) create mode 100755 tools/install_pre_requirements.sh diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index d491a97029c..68979e20033 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -20,7 +20,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: - python-version: '3.11' + python-version: '3.12' - uses: pre-commit/action@v3.0.1 bandit: @@ -54,17 +54,16 @@ jobs: matrix: include: - os: ubuntu-latest - python: '3.11' + python: '3.12' kind: pip-pre - os: ubuntu-latest python: '3.12' kind: conda - # 3.12 needs https://github.com/conda-forge/dipy-feedstock/pull/50 - os: macos-14 # arm64 - python: '3.11' + python: '3.12' kind: mamba - os: macos-latest # intel - python: '3.11' + python: '3.12' kind: mamba - os: windows-latest python: '3.10' diff --git a/azure-pipelines.yml b/azure-pipelines.yml index b6f2fd679a1..09e2f2648c7 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -238,7 +238,7 @@ stages: variables: MNE_LOGGING_LEVEL: 'warning' MNE_FORCE_SERIAL: 'true' - OPENBLAS_NUM_THREADS: '1' # deal with OpenBLAS conflicts safely on Windows + OPENBLAS_NUM_THREADS: '2' OMP_DYNAMIC: 'false' PYTHONUNBUFFERED: 1 PYTHONIOENCODING: 'utf-8' @@ -251,9 +251,9 @@ stages: 3.9 pip: TEST_MODE: 'pip' PYTHON_VERSION: '3.9' - 3.11 pip pre: + 3.12 pip pre: TEST_MODE: 'pip-pre' - PYTHON_VERSION: '3.11' + PYTHON_VERSION: '3.12' steps: - task: UsePythonVersion@0 inputs: diff --git a/mne/export/_edf.py b/mne/export/_edf.py index 7f7111a36d8..3f7e55b3d77 100644 --- a/mne/export/_edf.py +++ b/mne/export/_edf.py @@ -4,6 +4,7 @@ # Copyright the MNE-Python contributors. import datetime as dt +from typing import Callable import numpy as np @@ -11,7 +12,21 @@ _check_edfio_installed() from edfio import Edf, EdfAnnotation, EdfSignal, Patient, Recording # noqa: E402 -from edfio._utils import round_float_to_8_characters # noqa: E402 + + +# copied from edfio (Apache license) +def _round_float_to_8_characters( + value: float, + round_func: Callable[[float], int], +) -> float: + if isinstance(value, int) or value.is_integer(): + return value + length = 8 + integer_part_length = str(value).find(".") + if integer_part_length == length: + return round_func(value) + factor = 10 ** (length - 1 - integer_part_length) + return round_func(value * factor) / factor def _export_raw(fname, raw, physical_range, add_ch_type): @@ -52,7 +67,7 @@ def _export_raw(fname, raw, physical_range, add_ch_type): ) data = np.pad(data, (0, int(pad_width))) else: - data_record_duration = round_float_to_8_characters( + data_record_duration = _round_float_to_8_characters( np.floor(sfreq) / sfreq, round ) out_sfreq = np.floor(sfreq) / data_record_duration diff --git a/mne/export/tests/test_export.py b/mne/export/tests/test_export.py index 9ce72eb79de..9c8a60f50bb 100644 --- a/mne/export/tests/test_export.py +++ b/mne/export/tests/test_export.py @@ -145,7 +145,7 @@ def _create_raw_for_edf_tests(stim_channel_index=None): edfio_mark = pytest.mark.skipif( - not _check_edfio_installed(strict=False), reason="edfio not installed" + not _check_edfio_installed(strict=False), reason="unsafe use of private module" ) diff --git a/mne/morph.py b/mne/morph.py index 812ba23e095..5b8bfba41a7 100644 --- a/mne/morph.py +++ b/mne/morph.py @@ -190,7 +190,7 @@ def compute_source_morph( .. footbibliography:: """ src_data, kind, src_subject = _get_src_data(src) - subject_from = _check_subject_src(subject_from, src_subject) + subject_from = _check_subject_src(subject_from, src_subject, warn_none=True) del src _validate_type(src_to, (SourceSpaces, None), "src_to") _validate_type(subject_to, (str, None), "subject_to") @@ -823,19 +823,22 @@ def _resample_from_to(img, affine, to_vox_map): ############################################################################### # I/O -def _check_subject_src(subject, src, name="subject_from", src_name="src"): +def _check_subject_src( + subject, src, name="subject_from", src_name="src", *, warn_none=False +): if isinstance(src, str): subject_check = src elif src is None: # assume it's correct although dangerous but unlikely subject_check = subject else: subject_check = src._subject - if subject_check is None: - warn( - "The source space does not contain the subject name, we " - "recommend regenerating the source space (and forward / " - "inverse if applicable) for better code reliability" - ) + warn_none = True + if subject_check is None and warn_none: + warn( + "The source space does not contain the subject name, we " + "recommend regenerating the source space (and forward / " + "inverse if applicable) for better code reliability" + ) if subject is None: subject = subject_check elif subject_check is not None and subject != subject_check: diff --git a/mne/utils/dataframe.py b/mne/utils/dataframe.py index e4012c4d45d..95618c614fa 100644 --- a/mne/utils/dataframe.py +++ b/mne/utils/dataframe.py @@ -10,6 +10,7 @@ from ..defaults import _handle_default from ._logging import logger, verbose +from .check import check_version @verbose @@ -50,9 +51,17 @@ def _convert_times(times, time_format, meas_date=None, first_time=0): def _inplace(df, method, **kwargs): - """Handle transition: inplace=True (pandas <1.5) → copy=False (>=1.5).""" + # Handle transition: inplace=True (pandas <1.5) → copy=False (>=1.5) + # and 3.0 warning: + # E DeprecationWarning: The copy keyword is deprecated and will be removed in a + # future version. Copy-on-Write is active in pandas since 3.0 which utilizes a + # lazy copy mechanism that defers copies until necessary. Use .copy() to make + # an eager copy if necessary. _meth = getattr(df, method) # used for set_index() and rename() - if "copy" in signature(_meth).parameters: + + if check_version("pandas", "3.0"): + return _meth(**kwargs) + elif "copy" in signature(_meth).parameters: return _meth(**kwargs, copy=False) else: _meth(**kwargs, inplace=True) diff --git a/tools/azure_dependencies.sh b/tools/azure_dependencies.sh index c680fb100d6..cf4dd4726b9 100755 --- a/tools/azure_dependencies.sh +++ b/tools/azure_dependencies.sh @@ -1,38 +1,14 @@ -#!/bin/bash -ef +#!/bin/bash +set -eo pipefail +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) STD_ARGS="--progress-bar off --upgrade" -python -m pip install $STD_ARGS pip setuptools wheel packaging setuptools_scm +python -m pip install $STD_ARGS pip setuptools wheel if [ "${TEST_MODE}" == "pip" ]; then - python -m pip install --only-binary="numba,llvmlite,numpy,scipy,vtk" -e .[test,full] + python -m pip install $STD_ARGS --only-binary="numba,llvmlite,numpy,scipy,vtk" -e .[test,full] elif [ "${TEST_MODE}" == "pip-pre" ]; then - STD_ARGS="$STD_ARGS --pre" - # python -m pip install $STD_ARGS --only-binary ":all:" --extra-index-url "https://www.riverbankcomputing.com/pypi/simple" "PyQt6!=6.6.1,!=6.6.2" PyQt6-sip PyQt6-Qt6 "PyQt6-Qt6!=6.6.1,!=6.6.2" - python -m pip install $STD_ARGS --only-binary ":all:" "PyQt6!=6.6.1,!=6.6.2" PyQt6-sip PyQt6-Qt6 "PyQt6-Qt6!=6.6.1,!=6.6.2" - echo "Numpy etc." - # No pyarrow yet https://github.com/apache/arrow/issues/40216 - # No h5py (and thus dipy) yet until they improve/refactor thier wheel building infrastructure for Windows - python -m pip install $STD_ARGS --only-binary ":all:" --default-timeout=60 --extra-index-url "https://pypi.anaconda.org/scientific-python-nightly-wheels/simple" "numpy>=2.1.0.dev0" "scipy>=1.14.0.dev0" "scikit-learn>=1.5.dev0" matplotlib pillow statsmodels - echo "OpenMEEG" - pip install $STD_ARGS --only-binary ":all:" --extra-index-url "https://test.pypi.org/simple" "openmeeg>=2.6.0.dev4" - echo "vtk" - python -m pip install $STD_ARGS --only-binary ":all:" --extra-index-url "https://wheels.vtk.org" vtk - echo "nilearn" - python -m pip install $STD_ARGS git+https://github.com/nilearn/nilearn - echo "pyvista/pyvistaqt" - python -m pip install --progress-bar off git+https://github.com/pyvista/pyvista - python -m pip install --progress-bar off git+https://github.com/pyvista/pyvistaqt - echo "misc" - python -m pip install $STD_ARGS imageio-ffmpeg xlrd mffpy pillow traitlets pybv eeglabio - echo "nibabel with workaround" - python -m pip install --progress-bar off git+https://github.com/nipy/nibabel.git - echo "joblib" - python -m pip install --progress-bar off git+https://github.com/joblib/joblib@master - echo "EDFlib-Python" - python -m pip install $STD_ARGS git+https://github.com/the-siesta-group/edfio - # echo "pysnirf2" # Needs h5py - # python -m pip install $STD_ARGS git+https://github.com/BUNPC/pysnirf2 - ./tools/check_qt_import.sh PyQt6 - python -m pip install $STD_ARGS -e .[test] + ${SCRIPT_DIR}/install_pre_requirements.sh + python -m pip install $STD_ARGS --pre -e .[test] else echo "Unknown run type ${TEST_MODE}" exit 1 diff --git a/tools/github_actions_dependencies.sh b/tools/github_actions_dependencies.sh index c41e4c8e2d8..36a0e16d4fb 100755 --- a/tools/github_actions_dependencies.sh +++ b/tools/github_actions_dependencies.sh @@ -2,6 +2,7 @@ set -o pipefail +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) STD_ARGS="--progress-bar off --upgrade" INSTALL_ARGS="-e" INSTALL_KIND="test_extra,hdf5" @@ -19,51 +20,10 @@ elif [ ! -z "$CONDA_DEPENDENCIES" ]; then STD_ARGS="--progress-bar off" INSTALL_KIND="test" else - echo "Install pip-pre dependencies" test "${MNE_CI_KIND}" == "pip-pre" STD_ARGS="$STD_ARGS --pre" - python -m pip install $STD_ARGS pip - echo "Numpy" - pip uninstall -yq numpy - echo "PyQt6" - # Now broken in latest release and in the pre release: - # pip install $STD_ARGS --only-binary ":all:" --default-timeout=60 --extra-index-url https://www.riverbankcomputing.com/pypi/simple "PyQt6!=6.6.1,!=6.6.2" "PyQt6-Qt6!=6.6.1,!=6.6.2" - pip install $STD_ARGS --only-binary ":all:" --default-timeout=60 "PyQt6!=6.6.1,!=6.6.2" "PyQt6-Qt6!=6.6.1,!=6.6.2" - echo "NumPy/SciPy/pandas etc." - # No pyarrow yet https://github.com/apache/arrow/issues/40216 - # No dipy yet https://github.com/dipy/dipy/issues/2979 - pip install $STD_ARGS --only-binary ":all:" --default-timeout=60 --index-url "https://pypi.anaconda.org/scientific-python-nightly-wheels/simple" "numpy>=2.1.0.dev0" h5py - pip install $STD_ARGS --only-binary ":all:" --default-timeout=60 --extra-index-url "https://pypi.anaconda.org/scientific-python-nightly-wheels/simple" "numpy>=2.1.0.dev0" "scipy>=1.14.0.dev0" "scikit-learn>=1.5.dev0" matplotlib pillow statsmodels pandas - # No python-picard (needs numexpr) until they update to NumPy 2.0 compat + ${SCRIPT_DIR}/install_pre_requirements.sh INSTALL_KIND="test_extra" - echo "OpenMEEG" - pip install $STD_ARGS --only-binary ":all:" --extra-index-url "https://test.pypi.org/simple" "openmeeg>=2.6.0.dev4" - # No Numba because it forces an old NumPy version - echo "nilearn" - pip install $STD_ARGS git+https://github.com/nilearn/nilearn - echo "VTK" - pip install $STD_ARGS --only-binary ":all:" --extra-index-url "https://wheels.vtk.org" vtk - python -c "import vtk" - echo "PyVista" - pip install $STD_ARGS git+https://github.com/drammock/pyvista@numpy-2-compat - echo "pyvistaqt" - pip install $STD_ARGS git+https://github.com/pyvista/pyvistaqt - echo "imageio-ffmpeg, xlrd, mffpy" - pip install $STD_ARGS imageio-ffmpeg xlrd mffpy patsy traitlets pybv eeglabio - echo "mne-qt-browser" - pip install $STD_ARGS git+https://github.com/mne-tools/mne-qt-browser - echo "nibabel with workaround" - pip install $STD_ARGS git+https://github.com/nipy/nibabel.git - echo "joblib" - pip install $STD_ARGS git+https://github.com/joblib/joblib@master - echo "edfio" - pip install $STD_ARGS git+https://github.com/the-siesta-group/edfio - echo "h5io" - pip install $STD_ARGS git+https://github.com/h5io/h5io - echo "pysnirf2" - pip install $STD_ARGS git+https://github.com/BUNPC/pysnirf2 - # Make sure we're on a NumPy 2.0 variant - python -c "import numpy as np; assert np.__version__[0] == '2', np.__version__" fi echo "" diff --git a/tools/install_pre_requirements.sh b/tools/install_pre_requirements.sh new file mode 100755 index 00000000000..1f54da654fe --- /dev/null +++ b/tools/install_pre_requirements.sh @@ -0,0 +1,82 @@ +#!/bin/bash + +set -eo pipefail + +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) +PLATFORM=$(python -c 'import platform; print(platform.system())') + +echo "Installing pip-pre dependencies on ${PLATFORM}" +STD_ARGS="--progress-bar off --upgrade --pre" + +# Dependencies of scientific-python-nightly-wheels are installed here so that +# we can use strict --index-url (instead of --extra-index-url) below +python -m pip install $STD_ARGS pip setuptools packaging \ + threadpoolctl cycler fonttools kiwisolver pyparsing pillow python-dateutil \ + patsy pytz tzdata nibabel tqdm trx-python joblib +echo "PyQt6" +# Now broken in latest release and in the pre release: +# pip install $STD_ARGS --only-binary ":all:" --default-timeout=60 --extra-index-url https://www.riverbankcomputing.com/pypi/simple "PyQt6!=6.6.1,!=6.6.2" "PyQt6-Qt6!=6.6.1,!=6.6.2" +python -m pip install $STD_ARGS --only-binary ":all:" --default-timeout=60 "PyQt6!=6.6.1,!=6.6.2" "PyQt6-Qt6!=6.6.1,!=6.6.2" +echo "NumPy/SciPy/pandas etc." +python -m pip uninstall -yq numpy +# No pyarrow yet https://github.com/apache/arrow/issues/40216 +# No h5py (and thus dipy) yet until they improve/refactor thier wheel building infrastructure for Windows +OTHERS="" +if [[ "${PLATFORM}" == "Linux" ]]; then + OTHERS="h5py dipy" +fi +python -m pip install $STD_ARGS --only-binary ":all:" --default-timeout=60 \ + --index-url "https://pypi.anaconda.org/scientific-python-nightly-wheels/simple" \ + "numpy>=2.1.0.dev0" "scipy>=1.14.0.dev0" "scikit-learn>=1.5.dev0" \ + matplotlib statsmodels pandas \ + $OTHERS + +# No python-picard (needs numexpr) until they update to NumPy 2.0 compat +# No Numba because it forces an old NumPy version + +echo "OpenMEEG" +python -m pip install $STD_ARGS --only-binary ":all:" --extra-index-url "https://test.pypi.org/simple" "openmeeg>=2.6.0.dev4" + +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 +python -c "import vtk" + +echo "PyVista" +python -m pip install $STD_ARGS git+https://github.com/pyvista/pyvista + +echo "pyvistaqt" +pip install $STD_ARGS git+https://github.com/pyvista/pyvistaqt + +echo "imageio-ffmpeg, xlrd, mffpy" +pip install $STD_ARGS imageio-ffmpeg xlrd mffpy traitlets pybv eeglabio + +echo "mne-qt-browser" +pip install $STD_ARGS git+https://github.com/mne-tools/mne-qt-browser + +echo "nibabel" +pip install $STD_ARGS git+https://github.com/nipy/nibabel + +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 + +if [[ "${PLATFORM}" == "Linux" ]]; then + echo "h5io" + pip install $STD_ARGS git+https://github.com/h5io/h5io + + echo "pysnirf2" + pip install $STD_ARGS git+https://github.com/BUNPC/pysnirf2 +fi + +# Make sure we're on a NumPy 2.0 variant +echo "Checking NumPy version" +python -c "import numpy as np; assert np.__version__[0] == '2', np.__version__" + +# And that Qt works +echo "Checking Qt" +${SCRIPT_DIR}/check_qt_import.sh PyQt6 From 3c101cdd88b1e83017190ae490654ed290785b94 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Mon, 1 Apr 2024 13:17:35 -0400 Subject: [PATCH 246/405] MAINT: Bump to large resource class (#12522) --- .circleci/config.yml | 1 + doc/changes/names.inc | 4 ++-- doc/conf.py | 4 ++-- doc/install/mne_tools_suite.rst | 1 - 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 93874db5441..9f30ce574f5 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -402,6 +402,7 @@ jobs: default: "false" docker: - image: cimg/base:current-22.04 + resource_class: large steps: - restore_cache: keys: diff --git a/doc/changes/names.inc b/doc/changes/names.inc index 076c5933568..557005d1ed3 100644 --- a/doc/changes/names.inc +++ b/doc/changes/names.inc @@ -190,7 +190,7 @@ .. _George O'Neill: https://georgeoneill.github.io -.. _Gonzalo Reina: https://github.com/Gon-reina +.. _Gonzalo Reina: https://orcid.org/0000-0003-4219-2306 .. _Guillaume Dumas: https://mila.quebec/en/person/guillaume-dumas @@ -466,7 +466,7 @@ .. _Rasmus Aagaard: https://github.com/rasgaard -.. _Rasmus Zetter: https://people.aalto.fi/rasmus.zetter +.. _Rasmus Zetter: https://github.com/rzetter .. _Reza Nasri: https://github.com/rznas diff --git a/doc/conf.py b/doc/conf.py index cc2a25d7089..83277ceb267 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -736,8 +736,8 @@ def append_attr_meth_examples(app, what, name, obj, options, lines): "https://doi.org/10.3109/", # www.tandfonline.com "https://www.researchgate.net/profile/", "https://www.intel.com/content/www/us/en/developer/tools/oneapi/onemkl.html", - "https://scholar.google.com/scholar?cites=12188330066413208874&as_ylo=2014", - "https://scholar.google.com/scholar?cites=1521584321377182930&as_ylo=2013", + r"https://scholar.google.com/scholar\?cites=12188330066413208874&as_ylo=2014", + r"https://scholar.google.com/scholar\?cites=1521584321377182930&as_ylo=2013", # 500 server error "https://openwetware.org/wiki/Beauchamp:FreeSurfer", # 503 Server error diff --git a/doc/install/mne_tools_suite.rst b/doc/install/mne_tools_suite.rst index fac33b20b51..4b82b0c16fb 100644 --- a/doc/install/mne_tools_suite.rst +++ b/doc/install/mne_tools_suite.rst @@ -62,7 +62,6 @@ MNE-Python, including packages for: - automatic multi-dipole localization and uncertainty quantification with the Bayesian algorithm SESAME (`sesameeg`_) - GLM and group level analysis of near-infrared spectroscopy data (`MNE-NIRS`_) -- high-level EEG Python library for all kinds of EEG inverse solutions (`invertmeeg`_) - All-Resolutions Inference (ARI) for statistically valid circular inference and effect localization (`MNE-ARI`_) - real-time analysis (`MNE-Realtime`_) From 8590d35cabeae22b0bda41d3b774587426ae961c Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Tue, 2 Apr 2024 09:19:22 -0400 Subject: [PATCH 247/405] MAINT: Reenable picard in pre testing (#12525) --- tools/install_pre_requirements.sh | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tools/install_pre_requirements.sh b/tools/install_pre_requirements.sh index 1f54da654fe..30e72466b41 100755 --- a/tools/install_pre_requirements.sh +++ b/tools/install_pre_requirements.sh @@ -12,7 +12,7 @@ STD_ARGS="--progress-bar off --upgrade --pre" # we can use strict --index-url (instead of --extra-index-url) below python -m pip install $STD_ARGS pip setuptools packaging \ threadpoolctl cycler fonttools kiwisolver pyparsing pillow python-dateutil \ - patsy pytz tzdata nibabel tqdm trx-python joblib + patsy pytz tzdata nibabel tqdm trx-python joblib numexpr echo "PyQt6" # Now broken in latest release and in the pre release: # pip install $STD_ARGS --only-binary ":all:" --default-timeout=60 --extra-index-url https://www.riverbankcomputing.com/pypi/simple "PyQt6!=6.6.1,!=6.6.2" "PyQt6-Qt6!=6.6.1,!=6.6.2" @@ -31,7 +31,6 @@ python -m pip install $STD_ARGS --only-binary ":all:" --default-timeout=60 \ matplotlib statsmodels pandas \ $OTHERS -# No python-picard (needs numexpr) until they update to NumPy 2.0 compat # No Numba because it forces an old NumPy version echo "OpenMEEG" @@ -47,6 +46,9 @@ python -c "import vtk" echo "PyVista" python -m pip install $STD_ARGS git+https://github.com/pyvista/pyvista +echo "picard" +python -m pip install $STD_ARGS git+https://github.com/pierreablin/picard + echo "pyvistaqt" pip install $STD_ARGS git+https://github.com/pyvista/pyvistaqt From a02838fcf9673e4ed955fa79abbe79b07c968d63 Mon Sep 17 00:00:00 2001 From: "Seyed (Yahya) Shirazi" Date: Tue, 2 Apr 2024 09:22:59 -0700 Subject: [PATCH 248/405] Fix file format check in _check_eeglab_fname function (#12523) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Eric Larson Co-authored-by: Richard Höchenberger --- doc/changes/devel/12523.bugfix.rst | 1 + doc/changes/names.inc | 2 ++ mne/io/eeglab/eeglab.py | 2 -- 3 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 doc/changes/devel/12523.bugfix.rst diff --git a/doc/changes/devel/12523.bugfix.rst b/doc/changes/devel/12523.bugfix.rst new file mode 100644 index 00000000000..3ce8cea9d11 --- /dev/null +++ b/doc/changes/devel/12523.bugfix.rst @@ -0,0 +1 @@ +Remove FDT file format check for strings in EEGLAB's EEG.data in :func:`mne.io.read_raw_eeglab` and related functions by :newcontrib:`Seyed Yahya Shirazi` diff --git a/doc/changes/names.inc b/doc/changes/names.inc index 557005d1ed3..7a4ea591144 100644 --- a/doc/changes/names.inc +++ b/doc/changes/names.inc @@ -611,3 +611,5 @@ .. _Zhi Zhang: https://github.com/tczhangzhi/ .. _Zvi Baratz: https://github.com/ZviBaratz + +.. _Seyed Yahya Shirazi: https://neuromechanist.github.io diff --git a/mne/io/eeglab/eeglab.py b/mne/io/eeglab/eeglab.py index e2f1ce320c5..905e9620010 100644 --- a/mne/io/eeglab/eeglab.py +++ b/mne/io/eeglab/eeglab.py @@ -52,8 +52,6 @@ def _check_eeglab_fname(fname, dataname): "Old data format .dat detected. Please update your EEGLAB " "version and resave the data in .fdt format" ) - elif fmt != ".fdt": - raise OSError("Expected .fdt file format. Found %s format" % fmt) basedir = op.dirname(fname) data_fname = op.join(basedir, dataname) From 0aef0795afdb1543dfee8fe76485d3a1ff40ed31 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 2 Apr 2024 17:24:07 +0000 Subject: [PATCH 249/405] [pre-commit.ci] pre-commit autoupdate (#12524) 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 a79245f366f..40b85a11eb4 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.4 + rev: v0.3.5 hooks: - id: ruff name: ruff lint mne From 675f38a036804b0c1259d05f2ff6b0131d69fc81 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Tue, 2 Apr 2024 15:20:43 -0400 Subject: [PATCH 250/405] BUG: Fix bug with reading his_id from snirf (#12526) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- doc/changes/devel/12526.bugfix.rst | 1 + mne/io/snirf/_snirf.py | 9 ++++++++- mne/io/snirf/tests/test_snirf.py | 7 +++++++ 3 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 doc/changes/devel/12526.bugfix.rst diff --git a/doc/changes/devel/12526.bugfix.rst b/doc/changes/devel/12526.bugfix.rst new file mode 100644 index 00000000000..b853cdc751a --- /dev/null +++ b/doc/changes/devel/12526.bugfix.rst @@ -0,0 +1 @@ +Correct reading of ``info["subject_info"]["his_id"]`` in :func:`mne.io.read_raw_snirf`, by `Eric Larson`_. diff --git a/mne/io/snirf/_snirf.py b/mne/io/snirf/_snirf.py index 0974394a700..bde3e045528 100644 --- a/mne/io/snirf/_snirf.py +++ b/mne/io/snirf/_snirf.py @@ -285,7 +285,8 @@ def natural_keys(text): subject_info = {} names = np.array(dat.get("nirs/metaDataTags/SubjectID")) - subject_info["first_name"] = _correct_shape(names)[0].decode("UTF-8") + names = _correct_shape(names)[0].decode("UTF-8") + subject_info["his_id"] = names # Read non standard (but allowed) custom metadata tags if "lastName" in dat.get("nirs/metaDataTags/"): ln = dat.get("/nirs/metaDataTags/lastName")[0].decode("UTF-8") @@ -293,6 +294,12 @@ def natural_keys(text): if "middleName" in dat.get("nirs/metaDataTags/"): m = dat.get("/nirs/metaDataTags/middleName")[0].decode("UTF-8") subject_info["middle_name"] = m + if "firstName" in dat.get("nirs/metaDataTags/"): + fn = dat.get("/nirs/metaDataTags/firstName")[0].decode("UTF-8") + subject_info["first_name"] = fn + else: + # MNE < 1.7 used to not write the firstName tag, so pull it from names + subject_info["first_name"] = names.split("_")[0] if "sex" in dat.get("nirs/metaDataTags/"): s = dat.get("/nirs/metaDataTags/sex")[0].decode("UTF-8") if s in {"M", "Male", "1", "m"}: diff --git a/mne/io/snirf/tests/test_snirf.py b/mne/io/snirf/tests/test_snirf.py index f6eb5765fab..f298a030bea 100644 --- a/mne/io/snirf/tests/test_snirf.py +++ b/mne/io/snirf/tests/test_snirf.py @@ -133,6 +133,7 @@ def test_snirf_gowerlabs(): def test_snirf_basic(): """Test reading SNIRF files.""" raw = read_raw_snirf(sfnirs_homer_103_wShort, preload=True) + assert raw.info["subject_info"]["his_id"] == "default" # Test data import assert raw._data.shape == (26, 145) @@ -247,9 +248,15 @@ def test_snirf_nonstandard(tmp_path): f.create_dataset("nirs/metaDataTags/lastName", data=[b"Y"]) f.create_dataset("nirs/metaDataTags/sex", data=[b"1"]) raw = read_raw_snirf(fname, preload=True) + assert raw.info["subject_info"]["first_name"] == "default" # pull from his_id + with h5py.File(fname, "r+") as f: + f.create_dataset("nirs/metaDataTags/firstName", data=[b"W"]) + raw = read_raw_snirf(fname, preload=True) + assert raw.info["subject_info"]["first_name"] == "W" assert raw.info["subject_info"]["middle_name"] == "X" assert raw.info["subject_info"]["last_name"] == "Y" assert raw.info["subject_info"]["sex"] == 1 + assert raw.info["subject_info"]["his_id"] == "default" with h5py.File(fname, "r+") as f: del f["nirs/metaDataTags/sex"] f.create_dataset("nirs/metaDataTags/sex", data=[b"2"]) From 026e2622e9f32741ac20bc4c051bdc89bbbd3785 Mon Sep 17 00:00:00 2001 From: tom Date: Fri, 5 Apr 2024 11:29:35 -0400 Subject: [PATCH 251/405] Update README badge links (#12529) --- README.rst | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/README.rst b/README.rst index 806f5469e1d..153dcf0a5ef 100644 --- a/README.rst +++ b/README.rst @@ -1,6 +1,6 @@ .. -*- mode: rst -*- -|MNE|_ +|MNE| MNE-Python ========== @@ -121,13 +121,13 @@ About ^^^^^ +---------+------------+----------------+ -| CI | |Codecov|_ | |Bandit|_ | +| CI | |Codecov| | |Bandit| | +---------+------------+----------------+ -| Package | |PyPI|_ | |conda-forge|_ | +| Package | |PyPI| | |conda-forge| | +---------+------------+----------------+ -| Docs | |Docs|_ | |Discourse|_ | +| Docs | |Docs| | |Discourse| | +---------+------------+----------------+ -| Meta | |Zenodo|_ | |OpenSSF|_ | +| Meta | |Zenodo| | |OpenSSF| | +---------+------------+----------------+ @@ -143,28 +143,28 @@ MNE-Python is licensed under the BSD-3-Clause license. .. _pip: https://pip.pypa.io/en/stable/ .. |PyPI| image:: https://img.shields.io/pypi/dm/mne.svg?label=PyPI -.. _PyPI: https://pypi.org/project/mne/ + :target: https://pypi.org/project/mne/ .. |conda-forge| image:: https://img.shields.io/conda/dn/conda-forge/mne.svg?label=Conda -.. _conda-forge: https://anaconda.org/conda-forge/mne + :target: https://anaconda.org/conda-forge/mne .. |Docs| image:: https://img.shields.io/badge/Docs-online-green?label=Documentation -.. _Docs: https://mne.tools/dev/ + :target: https://mne.tools/dev/ .. |Zenodo| image:: https://zenodo.org/badge/DOI/10.5281/zenodo.592483.svg -.. _Zenodo: https://doi.org/10.5281/zenodo.592483 + :target: https://doi.org/10.5281/zenodo.592483 .. |Discourse| image:: https://img.shields.io/discourse/status?label=Forum&server=https%3A%2F%2Fmne.discourse.group%2F -.. _Discourse: https://mne.discourse.group/ + :target: https://mne.discourse.group/ .. |Codecov| image:: https://img.shields.io/codecov/c/github/mne-tools/mne-python?label=Coverage -.. _Codecov: https://codecov.io/gh/mne-tools/mne-python + :target: https://codecov.io/gh/mne-tools/mne-python .. |Bandit| image:: https://img.shields.io/badge/Security-Bandit-yellow.svg -.. _Bandit: https://github.com/PyCQA/bandit + :target: https://github.com/PyCQA/bandit .. |OpenSSF| image:: https://www.bestpractices.dev/projects/7783/badge -.. _OpenSSF: https://www.bestpractices.dev/projects/7783 + :target: https://www.bestpractices.dev/projects/7783 .. |MNE| image:: https://mne.tools/dev/_static/mne_logo_gray.svg -.. _MNE: https://mne.tools/dev/ + :target: https://mne.tools/dev/ From 24c2e8f809eaae87c8e4c8d574fcf867b742f755 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Fri, 5 Apr 2024 13:23:58 -0400 Subject: [PATCH 252/405] MAINT: Remove scipy.signal.morlet2 (#12531) --- azure-pipelines.yml | 4 ++-- doc/conf.py | 2 -- mne/conftest.py | 3 --- mne/decoding/tests/test_search_light.py | 14 ++++++++++++-- mne/gui/tests/test_coreg.py | 3 --- mne/time_frequency/tests/test_tfr.py | 12 ++++++++++-- mne/time_frequency/tfr.py | 25 +++++++------------------ mne/utils/config.py | 9 +++++++++ mne/utils/tests/test_config.py | 24 +++++++++++++++++++++--- pyproject.toml | 9 ++++----- tools/install_pre_requirements.sh | 7 ++++--- tools/pyqt6_requirements.txt | 2 ++ 12 files changed, 71 insertions(+), 43 deletions(-) create mode 100644 tools/pyqt6_requirements.txt diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 09e2f2648c7..a7cf6c3d612 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -104,7 +104,7 @@ stages: - bash: | set -e python -m pip install --progress-bar off --upgrade pip - python -m pip install --progress-bar off "mne-qt-browser[opengl] @ git+https://github.com/mne-tools/mne-qt-browser.git@main" pyvista scikit-learn pytest-error-for-skips python-picard "PyQt6!=6.5.1,!=6.6.1,!=6.6.2" "PyQt6-Qt6!=6.6.1,!=6.6.2" qtpy nibabel sphinx-gallery + python -m pip install --progress-bar off "mne-qt-browser[opengl] @ git+https://github.com/mne-tools/mne-qt-browser.git@main" pyvista scikit-learn pytest-error-for-skips python-picard qtpy nibabel sphinx-gallery -r tools/pyqt6_requirements.txt python -m pip uninstall -yq mne python -m pip install --progress-bar off --upgrade -e .[test] displayName: 'Install dependencies with pip' @@ -183,7 +183,7 @@ stages: displayName: 'Get test data' - bash: | set -e - python -m pip install "PyQt6!=6.6.1,!=6.6.2" "PyQt6-Qt6!=6.6.1,!=6.6.2" + python -m pip install -r tools/pyqt6_requirements.txt LD_DEBUG=libs python -c "from PyQt6.QtWidgets import QApplication, QWidget; app = QApplication([]); import matplotlib; matplotlib.use('QtAgg'); import matplotlib.pyplot as plt; plt.figure()" - bash: | mne sys_info -pd diff --git a/doc/conf.py b/doc/conf.py index 83277ceb267..b4d0ca36f16 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -1345,8 +1345,6 @@ def reset_warnings(gallery_conf, fname): # nilearn "pkg_resources is deprecated as an API", r"The .* was deprecated in Matplotlib 3\.7", - # scipy - r"scipy.signal.morlet2 is deprecated in SciPy 1\.12", # Matplotlib->tz r"datetime\.datetime\.utcfromtimestamp", # joblib diff --git a/mne/conftest.py b/mne/conftest.py index 7dd02366ace..93657339b26 100644 --- a/mne/conftest.py +++ b/mne/conftest.py @@ -178,9 +178,6 @@ def pytest_configure(config): ignore:numpy\.core\.numeric is deprecated.*:DeprecationWarning ignore:numpy\.core\.multiarray is deprecated.*:DeprecationWarning ignore:The numpy\.fft\.helper has been made private.*:DeprecationWarning - # TODO: Should actually fix these two - ignore:scipy.signal.morlet2 is deprecated in SciPy.*:DeprecationWarning - ignore:The `needs_threshold` and `needs_proba`.*:FutureWarning # tqdm (Fedora) ignore:.*'tqdm_asyncio' object has no attribute 'last_print_t':pytest.PytestUnraisableExceptionWarning # Until mne-qt-browser > 0.5.2 is released diff --git a/mne/decoding/tests/test_search_light.py b/mne/decoding/tests/test_search_light.py index 296c4ba4bea..21d4eda6d0f 100644 --- a/mne/decoding/tests/test_search_light.py +++ b/mne/decoding/tests/test_search_light.py @@ -94,8 +94,13 @@ def test_search_light(): with pytest.raises(ValueError, match="for two-class"): sl.score(X, y) # But check that valid ones should work with new enough sklearn + kwargs = dict() + if check_version("sklearn", "1.4"): + kwargs["response_method"] = "predict_proba" + else: + kwargs["needs_proba"] = True if "multi_class" in signature(roc_auc_score).parameters: - scoring = make_scorer(roc_auc_score, needs_proba=True, multi_class="ovo") + scoring = make_scorer(roc_auc_score, multi_class="ovo", **kwargs) sl = SlidingEstimator(logreg, scoring=scoring) sl.fit(X, y) sl.score(X, y) # smoke test @@ -125,7 +130,12 @@ def test_search_light(): assert score_sl.dtype == np.dtype(float) # Check that scoring was applied adequately - scoring = make_scorer(roc_auc_score, needs_threshold=True) + kwargs = dict() + if check_version("sklearn", "1.4"): + kwargs["response_method"] = ("decision_function", "predict_proba") + else: + kwargs["needs_threshold"] = True + scoring = make_scorer(roc_auc_score, **kwargs) score_manual = [ scoring(est, x, y) for est, x in zip(sl1.estimators_, X.transpose(2, 0, 1)) ] diff --git a/mne/gui/tests/test_coreg.py b/mne/gui/tests/test_coreg.py index f2372f4f3d6..aea6fba08ff 100644 --- a/mne/gui/tests/test_coreg.py +++ b/mne/gui/tests/test_coreg.py @@ -93,9 +93,6 @@ def test_coreg_gui_pyvista_file_support( """Test reading supported files.""" from mne.gui import coregistration - if Path(inst_path).suffix == ".snirf": - pytest.importorskip("snirf") - if inst_path == "gen_montage": # generate a montage fig to use as inst. tmp_info = read_info(raw_path) diff --git a/mne/time_frequency/tests/test_tfr.py b/mne/time_frequency/tests/test_tfr.py index 9383aaec824..33cfdb96b7b 100644 --- a/mne/time_frequency/tests/test_tfr.py +++ b/mne/time_frequency/tests/test_tfr.py @@ -15,7 +15,6 @@ assert_array_equal, assert_equal, ) -from scipy.signal import morlet2 import mne from mne import ( @@ -98,6 +97,15 @@ def test_tfr_ctf(): method(epochs, [10], 1) # smoke test +# Copied from SciPy before it was removed +def _morlet2(M, s, w=5): + x = np.arange(0, M) - (M - 1.0) / 2 + x = x / s + wavelet = np.exp(1j * w * x) * np.exp(-0.5 * x**2) * np.pi ** (-0.25) + output = np.sqrt(1 / s) * wavelet + return output + + @pytest.mark.parametrize("sfreq", [1000.0, 100 + np.pi]) @pytest.mark.parametrize("freq", [10.0, np.pi]) @pytest.mark.parametrize("n_cycles", [7, 2]) @@ -118,7 +126,7 @@ def test_morlet(sfreq, freq, n_cycles): M = len(W) w = n_cycles s = w * sfreq / (2 * freq * np.pi) # from SciPy docs - Ws = morlet2(M, s, w) * np.sqrt(2) + Ws = _morlet2(M, s, w) * np.sqrt(2) assert_allclose(W, Ws) # Check FWHM diff --git a/mne/time_frequency/tfr.py b/mne/time_frequency/tfr.py index 5a16cac80ed..00aed68e92f 100644 --- a/mne/time_frequency/tfr.py +++ b/mne/time_frequency/tfr.py @@ -132,13 +132,11 @@ def morlet(sfreq, freqs, n_cycles=7.0, sigma=None, zero_mean=False): Examples -------- Let's show a simple example of the relationship between ``n_cycles`` and - the FWHM using :func:`mne.time_frequency.fwhm`, as well as the equivalent - call using :func:`scipy.signal.morlet2`: + the FWHM using :func:`mne.time_frequency.fwhm`: .. plot:: import numpy as np - from scipy.signal import morlet2 as sp_morlet import matplotlib.pyplot as plt from mne.time_frequency import morlet, fwhm @@ -147,24 +145,15 @@ def morlet(sfreq, freqs, n_cycles=7.0, sigma=None, zero_mean=False): wavelet = morlet(sfreq=sfreq, freqs=freq, n_cycles=n_cycles) M, w = len(wavelet), n_cycles # convert to SciPy convention s = w * sfreq / (2 * freq * np.pi) # from SciPy docs - wavelet_sp = sp_morlet(M, s, w) * np.sqrt(2) # match our normalization _, ax = plt.subplots(layout="constrained") - colors = { - ('MNE', 'real'): '#66CCEE', - ('SciPy', 'real'): '#4477AA', - ('MNE', 'imag'): '#EE6677', - ('SciPy', 'imag'): '#AA3377', - } - lw = dict(MNE=2, SciPy=4) - zorder = dict(MNE=5, SciPy=4) + colors = dict(real="#66CCEE", imag="#EE6677") t = np.arange(-M // 2 + 1, M // 2 + 1) / sfreq - for name, w in (('MNE', wavelet), ('SciPy', wavelet_sp)): - for kind in ('real', 'imag'): - ax.plot(t, getattr(w, kind), label=f'{name} {kind}', - lw=lw[name], color=colors[(name, kind)], - zorder=zorder[name]) - ax.plot(t, np.abs(wavelet), label=f'MNE abs', color='k', lw=1., zorder=6) + for kind in ('real', 'imag'): + ax.plot( + t, getattr(wavelet, kind), label=kind, color=colors[kind], + ) + ax.plot(t, np.abs(wavelet), label=f'abs', color='k', lw=1., zorder=6) half_max = np.max(np.abs(wavelet)) / 2. ax.plot([-this_fwhm / 2., this_fwhm / 2.], [half_max, half_max], color='k', linestyle='-', label='FWHM', zorder=6) diff --git a/mne/utils/config.py b/mne/utils/config.py index ded70b55650..9fab1015040 100644 --- a/mne/utils/config.py +++ b/mne/utils/config.py @@ -685,6 +685,10 @@ def sys_info( "mne-icalabel", "mne-bids-pipeline", "neo", + "eeglabio", + "edfio", + "mffpy", + "pybv", "", ) if dependencies == "developer": @@ -692,9 +696,14 @@ def sys_info( "# Testing", "pytest", "nbclient", + "statsmodels", "numpydoc", "flake8", "pydocstyle", + "nitime", + "imageio", + "imageio-ffmpeg", + "snirf", "", "# Documentation", "sphinx", diff --git a/mne/utils/tests/test_config.py b/mne/utils/tests/test_config.py index ffae55ad08a..e0155638b0d 100644 --- a/mne/utils/tests/test_config.py +++ b/mne/utils/tests/test_config.py @@ -100,17 +100,35 @@ def test_config(tmp_path): pytest.raises(TypeError, _get_stim_channel, [1], None) -def test_sys_info(): +def test_sys_info_basic(): """Test info-showing utility.""" out = ClosingStringIO() sys_info(fid=out, check_version=False) out = out.getvalue() assert "numpy" in out + # replace all in-line whitespace with single space + out = "\n".join(" ".join(o.split()) for o in out.splitlines()) if platform.system() == "Darwin": - assert "Platform macOS-" in out + assert "Platform macOS-" in out elif platform.system() == "Linux": - assert "Platform Linux" in out + assert "Platform Linux" in out + + +def test_sys_info_complete(): + """Test that sys_info is sufficiently complete.""" + tomllib = pytest.importorskip("tomllib") # python 3.11+ + pyproject = Path(__file__).parents[3] / "pyproject.toml" + if not pyproject.is_file(): + pytest.skip("Does not appear to be a dev installation") + out = ClosingStringIO() + sys_info(fid=out, check_version=False, dependencies="developer") + out = out.getvalue() + pyproject = tomllib.loads(pyproject.read_text("utf-8")) + deps = pyproject["project"]["optional-dependencies"]["test_extra"] + for dep in deps: + dep = dep.split("[")[0].split(">")[0] + assert f" {dep}" in out, f"Missing in dev config: {dep}" def test_sys_info_qt_browser(): diff --git a/pyproject.toml b/pyproject.toml index 23a2efeaf4b..eee0a3aa5ea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,8 +62,8 @@ hdf5 = ["h5io", "pymatreader"] full = [ "mne[hdf5]", "qtpy", - "PyQt6!=6.6.1,!=6.6.2", - "PyQt6-Qt6!=6.6.1,!=6.6.2", + "PyQt6!=6.6.1", + "PyQt6-Qt6!=6.6.1,!=6.6.2,!=6.6.3", "pyobjc-framework-Cocoa>=5.2.0; platform_system=='Darwin'", "sip", "scikit-learn", @@ -73,10 +73,8 @@ full = [ "h5py", "pandas", "pyarrow", # only needed to avoid a deprecation warning in pandas - "numexpr", "jupyter", "python-picard", - "statsmodels", "joblib", "psutil", "dipy", @@ -100,6 +98,7 @@ full = [ "qdarkstyle!=3.2.2", "threadpoolctl", # duplicated in test_extra: + "statsmodels", "eeglabio", "edfio>=0.2.1", "pybv", @@ -130,6 +129,7 @@ test_extra = [ "nitime", "nbclient", "sphinx-gallery", + "statsmodels", "eeglabio", "edfio>=0.2.1", "pybv", @@ -213,7 +213,6 @@ ignore = [ "D100", # Missing docstring in public module "D104", # Missing docstring in public package "D413", # Missing blank line after last section - ] [tool.ruff.lint.pydocstyle] diff --git a/tools/install_pre_requirements.sh b/tools/install_pre_requirements.sh index 30e72466b41..bd4d7a6e4c1 100755 --- a/tools/install_pre_requirements.sh +++ b/tools/install_pre_requirements.sh @@ -15,8 +15,8 @@ python -m pip install $STD_ARGS pip setuptools packaging \ patsy pytz tzdata nibabel tqdm trx-python joblib numexpr echo "PyQt6" # Now broken in latest release and in the pre release: -# pip install $STD_ARGS --only-binary ":all:" --default-timeout=60 --extra-index-url https://www.riverbankcomputing.com/pypi/simple "PyQt6!=6.6.1,!=6.6.2" "PyQt6-Qt6!=6.6.1,!=6.6.2" -python -m pip install $STD_ARGS --only-binary ":all:" --default-timeout=60 "PyQt6!=6.6.1,!=6.6.2" "PyQt6-Qt6!=6.6.1,!=6.6.2" +# pip install $STD_ARGS --only-binary ":all:" --default-timeout=60 --extra-index-url https://www.riverbankcomputing.com/pypi/simple -r $SCRIPT_DIR/pyqt6_requirements.txt +python -m pip install $STD_ARGS --only-binary ":all:" --default-timeout=60 -r $SCRIPT_DIR/pyqt6_requirements.txt echo "NumPy/SciPy/pandas etc." python -m pip uninstall -yq numpy # No pyarrow yet https://github.com/apache/arrow/issues/40216 @@ -28,9 +28,10 @@ fi python -m pip install $STD_ARGS --only-binary ":all:" --default-timeout=60 \ --index-url "https://pypi.anaconda.org/scientific-python-nightly-wheels/simple" \ "numpy>=2.1.0.dev0" "scipy>=1.14.0.dev0" "scikit-learn>=1.5.dev0" \ - matplotlib statsmodels pandas \ + matplotlib pandas \ $OTHERS +# No statsmodels: https://github.com/statsmodels/statsmodels/issues/9198 # No Numba because it forces an old NumPy version echo "OpenMEEG" diff --git a/tools/pyqt6_requirements.txt b/tools/pyqt6_requirements.txt new file mode 100644 index 00000000000..26ec8315141 --- /dev/null +++ b/tools/pyqt6_requirements.txt @@ -0,0 +1,2 @@ +PyQt6!=6.6.1 +PyQt6-Qt6!=6.6.1,!=6.6.2,!=6.6.3 From 067715af11030c3dbe54362a957de132170c3f43 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Tue, 9 Apr 2024 10:36:41 -0400 Subject: [PATCH 253/405] MAINT: Reinstall statsmodels and improve logging (#12532) --- azure-pipelines.yml | 2 +- mne/fixes.py | 2 +- mne/proj.py | 9 ++++++--- tools/install_pre_requirements.sh | 3 +-- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index a7cf6c3d612..7e2fa2bd397 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -193,7 +193,7 @@ stages: displayName: 'PyQt6' - bash: | set -e - python -m pip install PySide6 + python -m pip install "PySide6!=6.7.0" mne sys_info -pd mne sys_info -pd | grep "qtpy .* (PySide6=.*)$" PYTEST_QT_API=PySide6 pytest -m "not slowtest" ${TEST_OPTIONS} diff --git a/mne/fixes.py b/mne/fixes.py index 6d874be8805..98b1ce805cd 100644 --- a/mne/fixes.py +++ b/mne/fixes.py @@ -866,7 +866,7 @@ def pinvh(a, rtol=None): def pinv(a, rtol=None): """Compute a pseudo-inverse of a matrix.""" - u, s, vh = np.linalg.svd(a, full_matrices=False) + u, s, vh = _safe_svd(a, full_matrices=False) del a maxS = np.max(s) if rtol is None: diff --git a/mne/proj.py b/mne/proj.py index 71cd3de85bf..d72bbd27e06 100644 --- a/mne/proj.py +++ b/mne/proj.py @@ -152,7 +152,7 @@ def _compute_proj( ncol=u.size, ) desc = f"{kind}-{desc_prefix}-PCA-{k + 1:02d}" - logger.info("Adding projection: %s", desc) + logger.info(f"Adding projection: {desc} (exp var={100 * float(var):0.1f}%)") proj = Projection( active=False, data=proj_data, @@ -221,13 +221,16 @@ def compute_proj_epochs( return _compute_proj(data, epochs.info, n_grad, n_mag, n_eeg, desc_prefix, meg=meg) -def _compute_cov_epochs(epochs, n_jobs): +def _compute_cov_epochs(epochs, n_jobs, *, log_drops=False): """Compute epochs covariance.""" parallel, p_fun, n_jobs = parallel_func(np.dot, n_jobs) + n_start = len(epochs.events) data = parallel(p_fun(e, e.T) for e in epochs) n_epochs = len(data) if n_epochs == 0: raise RuntimeError("No good epochs found") + if log_drops: + logger.info(f"Dropped {n_start - n_epochs}/{n_start} epochs") n_chan, n_samples = epochs.info["nchan"], len(epochs.times) _check_n_samples(n_samples * n_epochs, n_chan) @@ -351,7 +354,7 @@ def compute_proj_raw( baseline=None, proj=False, ) - data = _compute_cov_epochs(epochs, n_jobs) + data = _compute_cov_epochs(epochs, n_jobs, log_drops=True) info = epochs.info if not stop: stop = raw.n_times / raw.info["sfreq"] diff --git a/tools/install_pre_requirements.sh b/tools/install_pre_requirements.sh index bd4d7a6e4c1..47c7087ac8d 100755 --- a/tools/install_pre_requirements.sh +++ b/tools/install_pre_requirements.sh @@ -28,10 +28,9 @@ fi python -m pip install $STD_ARGS --only-binary ":all:" --default-timeout=60 \ --index-url "https://pypi.anaconda.org/scientific-python-nightly-wheels/simple" \ "numpy>=2.1.0.dev0" "scipy>=1.14.0.dev0" "scikit-learn>=1.5.dev0" \ - matplotlib pandas \ + matplotlib pandas statsmodels \ $OTHERS -# No statsmodels: https://github.com/statsmodels/statsmodels/issues/9198 # No Numba because it forces an old NumPy version echo "OpenMEEG" From 0d514882be5e255c6c31639f06bc845eea770cfe Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Tue, 9 Apr 2024 11:50:51 -0400 Subject: [PATCH 254/405] MAINT: Complete API change of ordered (#12534) Co-authored-by: Daniel McCloy --- mne/_fiff/pick.py | 41 ++++++++---------------------------- mne/_fiff/tests/test_pick.py | 3 +-- mne/channels/channels.py | 2 +- mne/cov.py | 2 +- mne/forward/forward.py | 2 +- mne/time_frequency/csd.py | 2 +- mne/utils/docs.py | 6 +++--- 7 files changed, 17 insertions(+), 41 deletions(-) diff --git a/mne/_fiff/pick.py b/mne/_fiff/pick.py index c6acd26ade0..2af49c7b921 100644 --- a/mne/_fiff/pick.py +++ b/mne/_fiff/pick.py @@ -17,7 +17,6 @@ fill_doc, logger, verbose, - warn, ) from .constants import FIFF @@ -258,7 +257,7 @@ def channel_type(info, idx): @verbose -def pick_channels(ch_names, include, exclude=(), ordered=None, *, verbose=None): +def pick_channels(ch_names, include, exclude=(), ordered=True, *, verbose=None): """Pick channels by names. Returns the indices of ``ch_names`` in ``include`` but not in ``exclude``. @@ -290,7 +289,7 @@ def pick_channels(ch_names, include, exclude=(), ordered=None, *, verbose=None): """ if len(np.unique(ch_names)) != len(ch_names): raise RuntimeError("ch_names is not a unique list, picking is unsafe") - _validate_type(ordered, (bool, None), "ordered") + _validate_type(ordered, bool, "ordered") _check_excludes_includes(include) _check_excludes_includes(exclude) if not isinstance(include, list): @@ -306,34 +305,12 @@ def pick_channels(ch_names, include, exclude=(), ordered=None, *, verbose=None): sel.append(ch_names.index(name)) else: missing.append(name) - dep_msg = ( - "The default for pick_channels will change from ordered=False to " - "ordered=True in 1.5" - ) - if len(missing): - if ordered is None: - warn( - f"{dep_msg} and this will result in an error because the " - f"following channel names are missing:\n{missing}\n" - "Either fix your included names or explicitly pass " - "ordered=False.", - FutureWarning, - ) - elif ordered: - raise ValueError( - f"Missing channels from ch_names required by include:\n{missing}" - ) + if len(missing) and ordered: + raise ValueError( + f"Missing channels from ch_names required by include:\n{missing}" + ) if not ordered: - out_sel = np.unique(sel) - if ordered is None and not np.array_equal(out_sel, sel): - warn( - f"{dep_msg} and this will result in a change of behavior " - "because the resulting channel order will not match. Either " - "use a channel order that matches your instance or " - "pass ordered=False.", - FutureWarning, - ) - sel = out_sel + sel = np.unique(sel) return np.array(sel, int) @@ -715,7 +692,7 @@ def _has_kit_refs(info, picks): @verbose def pick_channels_forward( - orig, include=(), exclude=(), ordered=None, copy=True, *, verbose=None + orig, include=(), exclude=(), ordered=True, copy=True, *, verbose=None ): """Pick channels from forward operator. @@ -902,7 +879,7 @@ def channel_indices_by_type(info, picks=None): @verbose def pick_channels_cov( - orig, include=(), exclude="bads", ordered=None, copy=True, *, verbose=None + orig, include=(), exclude="bads", ordered=True, copy=True, *, verbose=None ): """Pick channels from covariance matrix. diff --git a/mne/_fiff/tests/test_pick.py b/mne/_fiff/tests/test_pick.py index fa503a04ab3..ab9edeaec15 100644 --- a/mne/_fiff/tests/test_pick.py +++ b/mne/_fiff/tests/test_pick.py @@ -522,8 +522,7 @@ def test_picks_by_channels(): # duplicate check names = ["MEG 002", "MEG 002"] assert len(pick_channels(raw.info["ch_names"], names, ordered=False)) == 1 - with pytest.warns(FutureWarning, match="ordered=False"): - assert len(raw.copy().pick_channels(names)[0][0]) == 1 # legacy method OK here + assert len(raw.copy().pick_channels(names, ordered=False)[0][0]) == 1 # missing ch_name bad_names = names + ["BAD"] diff --git a/mne/channels/channels.py b/mne/channels/channels.py index 341e355f363..6ad43f32ee5 100644 --- a/mne/channels/channels.py +++ b/mne/channels/channels.py @@ -447,7 +447,7 @@ def pick_types( @verbose @legacy(alt="inst.pick(...)") - def pick_channels(self, ch_names, ordered=None, *, verbose=None): + def pick_channels(self, ch_names, ordered=True, *, verbose=None): """Pick some channels. Parameters diff --git a/mne/cov.py b/mne/cov.py index 60e2f21c893..5c0e455a52c 100644 --- a/mne/cov.py +++ b/mne/cov.py @@ -453,7 +453,7 @@ def plot_topomap( ) @verbose - def pick_channels(self, ch_names, ordered=None, *, verbose=None): + def pick_channels(self, ch_names, ordered=True, *, verbose=None): """Pick channels from this covariance matrix. Parameters diff --git a/mne/forward/forward.py b/mne/forward/forward.py index e3ab3d238f4..ebe005787fb 100644 --- a/mne/forward/forward.py +++ b/mne/forward/forward.py @@ -525,7 +525,7 @@ def _merge_fwds(fwds, *, verbose=None): @verbose -def read_forward_solution(fname, include=(), exclude=(), *, ordered=None, verbose=None): +def read_forward_solution(fname, include=(), exclude=(), *, ordered=True, verbose=None): """Read a forward solution a.k.a. lead field. Parameters diff --git a/mne/time_frequency/csd.py b/mne/time_frequency/csd.py index 8744a77f376..e2ea5ac1ba7 100644 --- a/mne/time_frequency/csd.py +++ b/mne/time_frequency/csd.py @@ -35,7 +35,7 @@ @verbose def pick_channels_csd( - csd, include=(), exclude=(), ordered=None, copy=True, *, verbose=None + csd, include=(), exclude=(), ordered=True, copy=True, *, verbose=None ): """Pick channels from cross-spectral density matrix. diff --git a/mne/utils/docs.py b/mne/utils/docs.py index 3b4811548cf..e8695499742 100644 --- a/mne/utils/docs.py +++ b/mne/utils/docs.py @@ -3064,12 +3064,12 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): docdict["ordered"] = """ ordered : bool - If True (default False), ensure that the order of the channels in + If True (default), ensure that the order of the channels in the modified instance matches the order of ``ch_names``. .. versionadded:: 0.20.0 - .. versionchanged:: 1.5 - The default changed from False in 1.4 to True in 1.5. + .. versionchanged:: 1.7 + The default changed from False in 1.6 to True in 1.7. """ docdict["origin_maxwell"] = """ From 940ac9553ce42c15b4c16ecd013824ca3ea7244a Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Tue, 9 Apr 2024 12:53:57 -0400 Subject: [PATCH 255/405] MAINT: Clean up PyVista contexts (#12533) --- mne/viz/_3d_overlay.py | 6 +- mne/viz/backends/_pyvista.py | 559 ++++++++++++++++------------------- 2 files changed, 262 insertions(+), 303 deletions(-) diff --git a/mne/viz/_3d_overlay.py b/mne/viz/_3d_overlay.py index 48baff23d1e..203cb686360 100644 --- a/mne/viz/_3d_overlay.py +++ b/mne/viz/_3d_overlay.py @@ -150,11 +150,7 @@ def remove_overlay(self, names): def _apply(self): if self._current_colors is None or self._renderer is None: return - self._renderer._set_mesh_scalars( - mesh=self._polydata, - scalars=self._current_colors, - name=self._default_scalars_name, - ) + self._polydata[self._default_scalars_name] = self._current_colors def update(self, colors=None): if colors is not None and self._cached_colors is not None: diff --git a/mne/viz/backends/_pyvista.py b/mne/viz/backends/_pyvista.py index 3e5062b593b..b94163b2ec8 100644 --- a/mne/viz/backends/_pyvista.py +++ b/mne/viz/backends/_pyvista.py @@ -256,15 +256,13 @@ def __init__( if pyvista.OFF_SCREEN: self.figure.store["off_screen"] = True - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=FutureWarning) - # pyvista theme may enable depth peeling by default so - # we disable it initially to better control the value afterwards - with _disabled_depth_peeling(): - self.plotter = self.figure._build() - self._hide_axes() - self._toggle_antialias() - self._enable_depth_peeling() + # pyvista theme may enable depth peeling by default so + # we disable it initially to better control the value afterwards + with _disabled_depth_peeling(): + self.plotter = self.figure._build() + self._hide_axes() + self._toggle_antialias() + self._enable_depth_peeling() # FIX: https://github.com/pyvista/pyvistaqt/pull/68 if not hasattr(self.plotter, "iren"): @@ -307,9 +305,7 @@ def _loc_to_index(self, loc): def subplot(self, x, y): x = np.max([0, np.min([x, self.figure._nrows - 1])]) y = np.max([0, np.min([y, self.figure._ncols - 1])]) - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=FutureWarning) - self.plotter.subplot(x, y) + self.plotter.subplot(x, y) def scene(self): return self.figure @@ -370,58 +366,56 @@ def polydata( ): from matplotlib.colors import to_rgba_array - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=FutureWarning) - rgba = False - if color is not None: - # See if we need to convert or not - check_color = to_rgba_array(color) - if len(check_color) == mesh.n_points: - scalars = (check_color * 255).astype("ubyte") - color = None - rgba = True - if isinstance(colormap, np.ndarray): - if colormap.dtype == np.uint8: - colormap = colormap.astype(np.float64) / 255.0 - from matplotlib.colors import ListedColormap - - colormap = ListedColormap(colormap) - if normals is not None: - mesh.point_data["Normals"] = normals - mesh.GetPointData().SetActiveNormals("Normals") - else: - _compute_normals(mesh) - smooth_shading = self.smooth_shading - if representation == "wireframe": - smooth_shading = False # never use smooth shading for wf - rgba = kwargs.pop("rgba", rgba) - actor = _add_mesh( - plotter=self.plotter, - mesh=mesh, - color=color, - scalars=scalars, - edge_color=color, - opacity=opacity, - cmap=colormap, - backface_culling=backface_culling, - rng=[vmin, vmax], - show_scalar_bar=False, - rgba=rgba, - smooth_shading=smooth_shading, - interpolate_before_map=interpolate_before_map, - style=representation, - line_width=line_width, - **kwargs, - ) + rgba = False + if color is not None: + # See if we need to convert or not + check_color = to_rgba_array(color) + if len(check_color) == mesh.n_points: + scalars = (check_color * 255).astype("ubyte") + color = None + rgba = True + if isinstance(colormap, np.ndarray): + if colormap.dtype == np.uint8: + colormap = colormap.astype(np.float64) / 255.0 + from matplotlib.colors import ListedColormap + + colormap = ListedColormap(colormap) + if normals is not None: + mesh.point_data["Normals"] = normals + mesh.GetPointData().SetActiveNormals("Normals") + else: + _compute_normals(mesh) + smooth_shading = self.smooth_shading + if representation == "wireframe": + smooth_shading = False # never use smooth shading for wf + rgba = kwargs.pop("rgba", rgba) + actor = _add_mesh( + plotter=self.plotter, + mesh=mesh, + color=color, + scalars=scalars, + edge_color=color, + opacity=opacity, + cmap=colormap, + backface_culling=backface_culling, + rng=[vmin, vmax], + show_scalar_bar=False, + rgba=rgba, + smooth_shading=smooth_shading, + interpolate_before_map=interpolate_before_map, + style=representation, + line_width=line_width, + **kwargs, + ) - if polygon_offset is not None: - mapper = actor.GetMapper() - mapper.SetResolveCoincidentTopologyToPolygonOffset() - mapper.SetRelativeCoincidentTopologyPolygonOffsetParameters( - polygon_offset, polygon_offset - ) + if polygon_offset is not None: + mapper = actor.GetMapper() + mapper.SetResolveCoincidentTopologyToPolygonOffset() + mapper.SetRelativeCoincidentTopologyPolygonOffsetParameters( + polygon_offset, polygon_offset + ) - return actor, mesh + return actor, mesh def mesh( self, @@ -444,11 +438,9 @@ def mesh( polygon_offset=None, **kwargs, ): - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=FutureWarning) - vertices = np.c_[x, y, z].astype(float) - triangles = np.c_[np.full(len(triangles), 3), triangles] - mesh = PolyData(vertices, triangles) + vertices = np.c_[x, y, z].astype(float) + triangles = np.c_[np.full(len(triangles), 3), triangles] + mesh = PolyData(vertices, triangles) return self.polydata( mesh=mesh, color=color, @@ -480,33 +472,31 @@ def contour( kind="line", color=None, ): - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=FutureWarning) - if colormap is not None: - colormap = _get_colormap_from_array(colormap, normalized_colormap) - vertices = np.array(surface["rr"]) - triangles = np.array(surface["tris"]) - n_triangles = len(triangles) - triangles = np.c_[np.full(n_triangles, 3), triangles] - mesh = PolyData(vertices, triangles) - mesh.point_data["scalars"] = scalars - contour = mesh.contour(isosurfaces=contours) - line_width = width - if kind == "tube": - contour = contour.tube(radius=width, n_sides=self.tube_n_sides) - line_width = 1.0 - actor = _add_mesh( - plotter=self.plotter, - mesh=contour, - show_scalar_bar=False, - line_width=line_width, - color=color, - rng=[vmin, vmax], - cmap=colormap, - opacity=opacity, - smooth_shading=self.smooth_shading, - ) - return actor, contour + if colormap is not None: + colormap = _get_colormap_from_array(colormap, normalized_colormap) + vertices = np.array(surface["rr"]) + triangles = np.array(surface["tris"]) + n_triangles = len(triangles) + triangles = np.c_[np.full(n_triangles, 3), triangles] + mesh = PolyData(vertices, triangles) + mesh.point_data["scalars"] = scalars + contour = mesh.contour(isosurfaces=contours) + line_width = width + if kind == "tube": + contour = contour.tube(radius=width, n_sides=self.tube_n_sides) + line_width = 1.0 + actor = _add_mesh( + plotter=self.plotter, + mesh=contour, + show_scalar_bar=False, + line_width=line_width, + color=color, + rng=[vmin, vmax], + cmap=colormap, + opacity=opacity, + smooth_shading=self.smooth_shading, + ) + return actor, contour def surface( self, @@ -521,13 +511,11 @@ def surface( backface_culling=False, polygon_offset=None, ): - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=FutureWarning) - normals = surface.get("nn", None) - vertices = np.array(surface["rr"]) - triangles = np.array(surface["tris"]) - triangles = np.c_[np.full(len(triangles), 3), triangles] - mesh = PolyData(vertices, triangles) + normals = surface.get("nn", None) + vertices = np.array(surface["rr"]) + triangles = np.array(surface["tris"]) + triangles = np.c_[np.full(len(triangles), 3), triangles] + mesh = PolyData(vertices, triangles) colormap = _get_colormap_from_array(colormap, normalized_colormap) if scalars is not None: mesh.point_data["scalars"] = scalars @@ -562,26 +550,24 @@ def sphere( return None, None _check_option("center.ndim", center.ndim, (1, 2)) _check_option("center.shape[-1]", center.shape[-1], (3,)) - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=FutureWarning) - sphere = vtkSphereSource() - sphere.SetThetaResolution(resolution) - sphere.SetPhiResolution(resolution) - if radius is not None: - sphere.SetRadius(radius) - sphere.Update() - geom = sphere.GetOutput() - mesh = PolyData(center) - glyph = mesh.glyph(orient=False, scale=False, factor=factor, geom=geom) - actor = _add_mesh( - self.plotter, - mesh=glyph, - color=color, - opacity=opacity, - backface_culling=backface_culling, - smooth_shading=self.smooth_shading, - ) - return actor, glyph + sphere = vtkSphereSource() + sphere.SetThetaResolution(resolution) + sphere.SetPhiResolution(resolution) + if radius is not None: + sphere.SetRadius(radius) + sphere.Update() + geom = sphere.GetOutput() + mesh = PolyData(center) + glyph = mesh.glyph(orient=False, scale=False, factor=factor, geom=geom) + actor = _add_mesh( + self.plotter, + mesh=glyph, + color=color, + opacity=opacity, + backface_culling=backface_culling, + smooth_shading=self.smooth_shading, + ) + return actor, glyph def tube( self, @@ -597,30 +583,28 @@ def tube( reverse_lut=False, opacity=None, ): - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=FutureWarning) - cmap = _get_colormap_from_array(colormap, normalized_colormap) - for pointa, pointb in zip(origin, destination): - line = Line(pointa, pointb) - if scalars is not None: - line.point_data["scalars"] = scalars[0, :] - scalars = "scalars" - color = None - else: - scalars = None - tube = line.tube(radius, n_sides=self.tube_n_sides) - actor = _add_mesh( - plotter=self.plotter, - mesh=tube, - scalars=scalars, - flip_scalars=reverse_lut, - rng=[vmin, vmax], - color=color, - show_scalar_bar=False, - cmap=cmap, - smooth_shading=self.smooth_shading, - opacity=opacity, - ) + cmap = _get_colormap_from_array(colormap, normalized_colormap) + for pointa, pointb in zip(origin, destination): + line = Line(pointa, pointb) + if scalars is not None: + line.point_data["scalars"] = scalars[0, :] + scalars = "scalars" + color = None + else: + scalars = None + tube = line.tube(radius, n_sides=self.tube_n_sides) + actor = _add_mesh( + plotter=self.plotter, + mesh=tube, + scalars=scalars, + flip_scalars=reverse_lut, + rng=[vmin, vmax], + color=color, + show_scalar_bar=False, + cmap=cmap, + smooth_shading=self.smooth_shading, + opacity=opacity, + ) return actor, tube def quiver3d( @@ -656,85 +640,83 @@ def quiver3d( _validate_type(scale_mode, str, "scale_mode") scale_map = dict(none=False, scalar="scalars", vector="vec") _check_option("scale_mode", scale_mode, list(scale_map)) - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=FutureWarning) - factor = scale - vectors = np.c_[u, v, w] - points = np.vstack(np.c_[x, y, z]) - n_points = len(points) - cell_type = np.full(n_points, VTK_VERTEX) - cells = np.c_[np.full(n_points, 1), range(n_points)] - args = (cells, cell_type, points) - grid = UnstructuredGrid(*args) - if scalars is None: - scalars = np.ones((n_points,)) - mesh_scalars = None - else: - mesh_scalars = "scalars" - grid.point_data["scalars"] = np.array(scalars, float) - grid.point_data["vec"] = vectors - if mode == "2darrow": - return _arrow_glyph(grid, factor), grid - elif mode == "arrow": - alg = _glyph(grid, orient="vec", scalars="scalars", factor=factor) - mesh = pyvista.wrap(alg.GetOutput()) + factor = scale + vectors = np.c_[u, v, w] + points = np.vstack(np.c_[x, y, z]) + n_points = len(points) + cell_type = np.full(n_points, VTK_VERTEX) + cells = np.c_[np.full(n_points, 1), range(n_points)] + args = (cells, cell_type, points) + grid = UnstructuredGrid(*args) + if scalars is None: + scalars = np.ones((n_points,)) + mesh_scalars = None + else: + mesh_scalars = "scalars" + grid.point_data["scalars"] = np.array(scalars, float) + grid.point_data["vec"] = vectors + if mode == "2darrow": + return _arrow_glyph(grid, factor), grid + elif mode == "arrow": + alg = _glyph(grid, orient="vec", scalars="scalars", factor=factor) + mesh = pyvista.wrap(alg.GetOutput()) + else: + tr = None + if mode == "cone": + glyph = vtkConeSource() + glyph.SetCenter(0.5, 0, 0) + if glyph_radius is not None: + glyph.SetRadius(glyph_radius) + elif mode == "cylinder": + glyph = vtkCylinderSource() + if glyph_radius is not None: + glyph.SetRadius(glyph_radius) + elif mode == "oct": + glyph = vtkPlatonicSolidSource() + glyph.SetSolidTypeToOctahedron() else: - tr = None - if mode == "cone": - glyph = vtkConeSource() - glyph.SetCenter(0.5, 0, 0) - if glyph_radius is not None: - glyph.SetRadius(glyph_radius) - elif mode == "cylinder": - glyph = vtkCylinderSource() - if glyph_radius is not None: - glyph.SetRadius(glyph_radius) - elif mode == "oct": - glyph = vtkPlatonicSolidSource() - glyph.SetSolidTypeToOctahedron() - else: - assert mode == "sphere", mode # guaranteed above - glyph = vtkSphereSource() - if mode == "cylinder": - if glyph_height is not None: - glyph.SetHeight(glyph_height) - if glyph_center is not None: - glyph.SetCenter(glyph_center) - if glyph_resolution is not None: - glyph.SetResolution(glyph_resolution) + assert mode == "sphere", mode # guaranteed above + glyph = vtkSphereSource() + if mode == "cylinder": + if glyph_height is not None: + glyph.SetHeight(glyph_height) + if glyph_center is not None: + glyph.SetCenter(glyph_center) + if glyph_resolution is not None: + glyph.SetResolution(glyph_resolution) + tr = vtkTransform() + tr.RotateWXYZ(90, 0, 0, 1) + elif mode == "oct": + if solid_transform is not None: + assert solid_transform.shape == (4, 4) tr = vtkTransform() - tr.RotateWXYZ(90, 0, 0, 1) - elif mode == "oct": - if solid_transform is not None: - assert solid_transform.shape == (4, 4) - tr = vtkTransform() - tr.SetMatrix(solid_transform.astype(np.float64).ravel()) - if tr is not None: - # fix orientation - glyph.Update() - trp = vtkTransformPolyDataFilter() - trp.SetInputData(glyph.GetOutput()) - trp.SetTransform(tr) - glyph = trp + tr.SetMatrix(solid_transform.astype(np.float64).ravel()) + if tr is not None: + # fix orientation glyph.Update() - geom = glyph.GetOutput() - mesh = grid.glyph( - orient="vec", - scale=scale_map[scale_mode], - factor=factor, - geom=geom, - ) - actor = _add_mesh( - self.plotter, - mesh=mesh, - color=color, - opacity=opacity, - scalars=mesh_scalars if colormap is not None else None, - colormap=colormap, - show_scalar_bar=False, - backface_culling=backface_culling, - clim=clim, + trp = vtkTransformPolyDataFilter() + trp.SetInputData(glyph.GetOutput()) + trp.SetTransform(tr) + glyph = trp + glyph.Update() + geom = glyph.GetOutput() + mesh = grid.glyph( + orient="vec", + scale=scale_map[scale_mode], + factor=factor, + geom=geom, ) + actor = _add_mesh( + self.plotter, + mesh=mesh, + color=color, + opacity=opacity, + scalars=mesh_scalars if colormap is not None else None, + colormap=colormap, + show_scalar_bar=False, + backface_culling=backface_culling, + clim=clim, + ) return actor, mesh def text2d( @@ -742,41 +724,37 @@ def text2d( ): size = 14 if size is None else size position = (x_window, y_window) - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=FutureWarning) - actor = self.plotter.add_text( - text, position=position, font_size=size, color=color, viewport=True - ) - if isinstance(justification, str): - if justification == "left": - actor.GetTextProperty().SetJustificationToLeft() - elif justification == "center": - actor.GetTextProperty().SetJustificationToCentered() - elif justification == "right": - actor.GetTextProperty().SetJustificationToRight() - else: - raise ValueError( - "Expected values for `justification` are `left`, `center` or " - f"`right` but got {justification} instead." - ) + actor = self.plotter.add_text( + text, position=position, font_size=size, color=color, viewport=True + ) + if isinstance(justification, str): + if justification == "left": + actor.GetTextProperty().SetJustificationToLeft() + elif justification == "center": + actor.GetTextProperty().SetJustificationToCentered() + elif justification == "right": + actor.GetTextProperty().SetJustificationToRight() + else: + raise ValueError( + "Expected values for `justification` are `left`, `center` or " + f"`right` but got {justification} instead." + ) _hide_testing_actor(actor) return actor def text3d(self, x, y, z, text, scale, color="white"): - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=FutureWarning) - kwargs = dict( - points=np.array([x, y, z]).astype(float), - labels=[text], - point_size=scale, - text_color=color, - font_family=self.font_family, - name=text, - shape_opacity=0, - ) - if "always_visible" in signature(self.plotter.add_point_labels).parameters: - kwargs["always_visible"] = True - actor = self.plotter.add_point_labels(**kwargs) + kwargs = dict( + points=np.array([x, y, z]).astype(float), + labels=[text], + point_size=scale, + text_color=color, + font_family=self.font_family, + name=text, + shape_opacity=0, + ) + if "always_visible" in signature(self.plotter.add_point_labels).parameters: + kwargs["always_visible"] = True + actor = self.plotter.add_point_labels(**kwargs) _hide_testing_actor(actor) return actor @@ -795,26 +773,24 @@ def scalarbar( mapper = source.GetMapper() else: mapper = None - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=FutureWarning) - kwargs = dict( - color=color, - title=title, - n_labels=n_labels, - use_opacity=False, - n_colors=256, - position_x=0.15, - position_y=0.05, - width=0.7, - shadow=False, - bold=True, - label_font_size=22, - font_family=self.font_family, - background_color=bgcolor, - mapper=mapper, - ) - kwargs.update(extra_kwargs) - actor = self.plotter.add_scalar_bar(**kwargs) + kwargs = dict( + color=color, + title=title, + n_labels=n_labels, + use_opacity=False, + n_colors=256, + position_x=0.15, + position_y=0.05, + width=0.7, + shadow=False, + bold=True, + label_font_size=22, + font_family=self.font_family, + background_color=bgcolor, + mapper=mapper, + ) + kwargs.update(extra_kwargs) + actor = self.plotter.add_scalar_bar(**kwargs) _hide_testing_actor(actor) return actor @@ -919,13 +895,6 @@ def _update_picking_callback( self.plotter.picker.AddObserver(vtkCommand.EndPickEvent, on_pick) self.plotter.picker.SetVolumeOpacityIsovalue(0.0) - def _set_mesh_scalars(self, mesh, scalars, name): - # Catch: FutureWarning: Conversion of the second argument of - # issubdtype from `complex` to `np.complexfloating` is deprecated. - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=FutureWarning) - mesh.point_data[name] = scalars - def _set_colormap_range( self, actor, ctable, scalar_bar, rng=None, background_color=None ): @@ -1237,9 +1206,7 @@ def _set_3d_view( def _set_3d_title(figure, title, size=16): - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=FutureWarning) - figure.plotter.add_text(title, font_size=size, color="white", name="title") + figure.plotter.add_text(title, font_size=size, color="white", name="title") figure.plotter.update() _process_events(figure.plotter) @@ -1249,26 +1216,22 @@ def _check_3d_figure(figure): def _close_3d_figure(figure): - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=FutureWarning) - # copy the plotter locally because figure.plotter is modified - plotter = figure.plotter - # close the window - plotter.close() # additional cleaning following signal_close - _process_events(plotter) - # free memory and deregister from the scraper - plotter.deep_clean() # remove internal references - _ALL_PLOTTERS.pop(plotter._id_name, None) - _process_events(plotter) + # copy the plotter locally because figure.plotter is modified + plotter = figure.plotter + # close the window + plotter.close() # additional cleaning following signal_close + _process_events(plotter) + # free memory and deregister from the scraper + plotter.deep_clean() # remove internal references + _ALL_PLOTTERS.pop(plotter._id_name, None) + _process_events(plotter) def _take_3d_screenshot(figure, mode="rgb", filename=None): - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=FutureWarning) - _process_events(figure.plotter) - return figure.plotter.screenshot( - transparent_background=(mode == "rgba"), filename=filename - ) + _process_events(figure.plotter) + return figure.plotter.screenshot( + transparent_background=(mode == "rgba"), filename=filename + ) def _process_events(plotter): From 105c8b819c08d24262f65d870b55bf0b305340d7 Mon Sep 17 00:00:00 2001 From: Daniel McCloy Date: Tue, 9 Apr 2024 16:00:17 -0500 Subject: [PATCH 256/405] explicitly disallow multitaper in presence of bad annotations (#12535) --- doc/changes/devel/12535.bugfix.rst | 1 + mne/io/base.py | 4 +++- mne/time_frequency/spectrum.py | 6 ++++++ mne/time_frequency/tests/test_spectrum.py | 3 +++ 4 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 doc/changes/devel/12535.bugfix.rst diff --git a/doc/changes/devel/12535.bugfix.rst b/doc/changes/devel/12535.bugfix.rst new file mode 100644 index 00000000000..eeeda0bffac --- /dev/null +++ b/doc/changes/devel/12535.bugfix.rst @@ -0,0 +1 @@ +Calling :meth:`~mne.io.Raw.compute_psd` with ``method="multitaper"`` is now expressly disallowed when ``reject_by_annotation=True`` and ``bad_*`` annotations are present (previously this was nominally allowed but resulted in ``nan`` values in the PSD). By `Daniel McCloy`_. diff --git a/mne/io/base.py b/mne/io/base.py index c7fb5e4ddd0..b22b760101e 100644 --- a/mne/io/base.py +++ b/mne/io/base.py @@ -2197,7 +2197,9 @@ def compute_psd( Parameters ---------- %(method_psd)s - Default is ``'welch'``. + Note that ``"multitaper"`` cannot be used if ``reject_by_annotation=True`` + and there are ``"bad_*"`` annotations in the :class:`~mne.io.Raw` data; + in such cases use ``"welch"``. Default is ``'welch'``. %(fmin_fmax_psd)s %(tmin_tmax_psd)s %(picks_good_data_noref)s diff --git a/mne/time_frequency/spectrum.py b/mne/time_frequency/spectrum.py index 7300753c584..a9006ac443f 100644 --- a/mne/time_frequency/spectrum.py +++ b/mne/time_frequency/spectrum.py @@ -1132,6 +1132,12 @@ def __init__( data = self.inst.get_data( self._picks, start, stop + 1, reject_by_annotation=rba ) + if np.any(np.isnan(data)) and method == "multitaper": + raise NotImplementedError( + 'Cannot use method="multitaper" when reject_by_annotation=True. ' + 'Please use method="welch" instead.' + ) + else: # Evoked data = self.inst.data[self._picks][:, self._time_mask] # set nave diff --git a/mne/time_frequency/tests/test_spectrum.py b/mne/time_frequency/tests/test_spectrum.py index 752e1d000a1..2b612406f54 100644 --- a/mne/time_frequency/tests/test_spectrum.py +++ b/mne/time_frequency/tests/test_spectrum.py @@ -24,6 +24,9 @@ def test_compute_psd_errors(raw): raw.compute_psd(foo=None, bar=None) with pytest.raises(ValueError, match="Complex output is not supported in "): raw.compute_psd(output="complex") + raw.set_annotations(Annotations(onset=0.01, duration=0.01, description="bad_foo")) + with pytest.raises(NotImplementedError, match='Cannot use method="multitaper"'): + raw.compute_psd(method="multitaper", reject_by_annotation=True) @pytest.mark.parametrize("method", ("welch", "multitaper")) From a92d798744035a304e307f0c5170db6268d75079 Mon Sep 17 00:00:00 2001 From: Daniel McCloy Date: Tue, 9 Apr 2024 16:51:03 -0500 Subject: [PATCH 257/405] align FFT windows to good data spans in psd_array_welch (#12536) --- doc/changes/devel/12536.bugfix.rst | 1 + mne/time_frequency/psd.py | 23 +++++++++++++++++++++-- mne/time_frequency/tests/test_psd.py | 20 ++++++++++++++++++++ mne/time_frequency/tests/test_spectrum.py | 9 +++++---- 4 files changed, 47 insertions(+), 6 deletions(-) create mode 100644 doc/changes/devel/12536.bugfix.rst diff --git a/doc/changes/devel/12536.bugfix.rst b/doc/changes/devel/12536.bugfix.rst new file mode 100644 index 00000000000..2b4a709186f --- /dev/null +++ b/doc/changes/devel/12536.bugfix.rst @@ -0,0 +1 @@ +:meth:`~mne.io.Raw.compute_psd` and :func:`~mne.time_frequency.psd_array_welch` will now use FFT windows aligned to the onsets of good data spans when ``bad_*`` annotations are present. By `Daniel McCloy`_. diff --git a/mne/time_frequency/psd.py b/mne/time_frequency/psd.py index 33bcd16df8c..b2123f933fb 100644 --- a/mne/time_frequency/psd.py +++ b/mne/time_frequency/psd.py @@ -11,6 +11,7 @@ from ..parallel import parallel_func from ..utils import _check_option, _ensure_int, logger, verbose +from ..utils.numerics import _mask_to_onsets_offsets # adapted from SciPy @@ -224,12 +225,30 @@ def psd_array_welch( window=window, mode=mode, ) - x_splits = [arr for arr in np.array_split(x, n_jobs) if arr.size != 0] + if np.any(np.isnan(x)): + good_mask = ~np.isnan(x) + # NaNs originate from annot, so must match for all channels. Note that we CANNOT + # use np.testing.assert_allclose() here; it is strict about shapes/broadcasting + assert np.allclose(good_mask, good_mask[[0]], equal_nan=True) + t_onsets, t_offsets = _mask_to_onsets_offsets(good_mask[0]) + x_splits = [x[..., t_ons:t_off] for t_ons, t_off in zip(t_onsets, t_offsets)] + weights = [ + split.shape[-1] for split in x_splits if split.shape[-1] >= n_per_seg + ] + agg_func = partial(np.average, weights=weights) + if n_jobs > 1: + logger.info( + f"Data split into {len(x_splits)} (probably unequal) chunks due to " + '"bad_*" annotations. Parallelization may be sub-optimal.' + ) + else: + x_splits = [arr for arr in np.array_split(x, n_jobs) if arr.size != 0] + agg_func = np.concatenate f_spect = parallel( my_spect_func(d, func=func, freq_sl=freq_sl, average=average, output=output) for d in x_splits ) - psds = np.concatenate(f_spect, axis=0) + psds = agg_func(f_spect, axis=0) shape = dshape + (len(freqs),) if average is None: shape = shape + (-1,) diff --git a/mne/time_frequency/tests/test_psd.py b/mne/time_frequency/tests/test_psd.py index e02e561384f..363a4207ce9 100644 --- a/mne/time_frequency/tests/test_psd.py +++ b/mne/time_frequency/tests/test_psd.py @@ -36,6 +36,26 @@ def test_psd_nan(): assert "hamming window" in log +def test_bad_annot_handling(): + """Make sure results equivalent with/without Annotations.""" + n_per_seg = 256 + n_chan = 3 + n_times = 5 * n_per_seg + x = np.random.default_rng(seed=42).standard_normal(size=(n_chan, n_times)) + want = psd_array_welch(x, sfreq=100) + # now simulate an annotation that breaks up the array into unequal spans. Using + # `n_per_seg` as the cut point is unrealistic/idealized, but it allows us to test + # whether we get results ~identical to `want` (which we should in this edge case) + x2 = np.concatenate( + (x[..., :n_per_seg], np.full((n_chan, 1), np.nan), x[..., n_per_seg:]), axis=-1 + ) + got = psd_array_welch(x2, sfreq=100) + # freqs should be identical + np.testing.assert_array_equal(got[1], want[1]) + # powers should be very very close + np.testing.assert_allclose(got[0], want[0], rtol=1e-15, atol=0) + + def _make_psd_data(): """Make noise data with sinusoids in 2 out of 7 channels.""" rng = np.random.default_rng(0) diff --git a/mne/time_frequency/tests/test_spectrum.py b/mne/time_frequency/tests/test_spectrum.py index 2b612406f54..a6ea0be9739 100644 --- a/mne/time_frequency/tests/test_spectrum.py +++ b/mne/time_frequency/tests/test_spectrum.py @@ -166,12 +166,13 @@ def test_spectrum_reject_by_annot(raw): Cannot use raw_spectrum fixture here because we're testing reject_by_annotation in .compute_psd() method. """ - spect_no_annot = raw.compute_psd() + kw = dict(n_per_seg=512) # smaller than shortest good span, to avoid warning + spect_no_annot = raw.compute_psd(**kw) raw.set_annotations(Annotations([1, 5], [3, 3], ["test", "test"])) - spect_benign_annot = raw.compute_psd() + spect_benign_annot = raw.compute_psd(**kw) raw.annotations.description = np.array(["bad_test", "bad_test"]) - spect_reject_annot = raw.compute_psd() - spect_ignored_annot = raw.compute_psd(reject_by_annotation=False) + spect_reject_annot = raw.compute_psd(**kw) + spect_ignored_annot = raw.compute_psd(**kw, reject_by_annotation=False) # the only one that should be different is `spect_reject_annot` assert spect_no_annot == spect_benign_annot assert spect_no_annot == spect_ignored_annot From b33e7a1906bb12617f0483abc770ff0ca91ba201 Mon Sep 17 00:00:00 2001 From: Daniel McCloy Date: Tue, 9 Apr 2024 20:40:01 -0500 Subject: [PATCH 258/405] Fix phase loading (#12537) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Eric Larson --- doc/changes/devel/12537.bugfix.rst | 1 + mne/time_frequency/tests/test_tfr.py | 8 +++++++- mne/time_frequency/tfr.py | 13 ++++++++----- 3 files changed, 16 insertions(+), 6 deletions(-) create mode 100644 doc/changes/devel/12537.bugfix.rst diff --git a/doc/changes/devel/12537.bugfix.rst b/doc/changes/devel/12537.bugfix.rst new file mode 100644 index 00000000000..911bdce444e --- /dev/null +++ b/doc/changes/devel/12537.bugfix.rst @@ -0,0 +1 @@ +Fix bug in loading of complex/phase TFRs. By `Daniel McCloy`_. diff --git a/mne/time_frequency/tests/test_tfr.py b/mne/time_frequency/tests/test_tfr.py index 33cfdb96b7b..cedc13a479b 100644 --- a/mne/time_frequency/tests/test_tfr.py +++ b/mne/time_frequency/tests/test_tfr.py @@ -1402,7 +1402,7 @@ def test_to_data_frame_time_format(time_format): @parametrize_morlet_multitaper @parametrize_power_phase_complex @pytest.mark.parametrize("picks", ("mag", mag_names, [2, 5, 8])) # all 3 equivalent -def test_raw_compute_tfr(raw, method, output, picks): +def test_raw_compute_tfr(raw, method, output, picks, tmp_path): """Test Raw.compute_tfr() and picks handling.""" full_tfr = raw.compute_tfr(method, output=output, freqs=freqs_linspace) pick_tfr = raw.compute_tfr(method, output=output, freqs=freqs_linspace, picks=picks) @@ -1411,6 +1411,12 @@ def test_raw_compute_tfr(raw, method, output, picks): want = full_tfr.get_data(picks=mag_names) got = pick_tfr.get_data() assert_array_equal(want, got) + # make sure save/load works for phase/complex data + if output in ("phase", "complex"): + pytest.importorskip("h5io") + fname = tmp_path / "temp_tfr.hdf5" + full_tfr.save(fname, overwrite=True) + assert read_tfrs(fname) == full_tfr @parametrize_morlet_multitaper diff --git a/mne/time_frequency/tfr.py b/mne/time_frequency/tfr.py index 00aed68e92f..8f8599f757c 100644 --- a/mne/time_frequency/tfr.py +++ b/mne/time_frequency/tfr.py @@ -1440,7 +1440,7 @@ def __setstate__(self, state): self._set_times(self._raw_times) # Handle instance type. Prior to gh-11282, Raw was not a possibility so if # `inst_type_str` is missing it must be Epochs or Evoked - unknown_class = Epochs if self._data.ndim == 4 else Evoked + unknown_class = Epochs if "epoch" in self._dims else Evoked inst_types = dict(Raw=Raw, Epochs=Epochs, Evoked=Evoked, Unknown=unknown_class) self._inst_type = inst_types[defaults["inst_type_str"]] # sanity check data/freqs/times/info agreement @@ -1499,7 +1499,9 @@ def _check_state(self): """Check data/freqs/times/info agreement during __setstate__.""" msg = "{} axis of data ({}) doesn't match {} attribute ({})" n_chan_info = len(self.info["chs"]) - n_chan, n_freq, n_time = self._data.shape[self._dims.index("channel") :] + n_chan = self._data.shape[self._dims.index("channel")] + n_freq = self._data.shape[self._dims.index("freq")] + n_time = self._data.shape[self._dims.index("time")] if n_chan_info != n_chan: msg = msg.format("Channel", n_chan, "info", n_chan_info) elif n_freq != len(self.freqs): @@ -3372,13 +3374,14 @@ def average(self, method="mean", *, dim="epochs", copy=False): "EpochsTFR.average() got a method that resulted in data of shape " f"{data.shape}, but it should be {expected_shape}." ) + state = self.__getstate__() # restore singleton freqs axis (not necessary for epochs/times: class changes) if dim == "freqs": data = np.expand_dims(data, axis=axis) - state = self.__getstate__() + else: + state["dims"] = (*state["dims"][:axis], *state["dims"][axis + 1 :]) state["data"] = data state["info"] = deepcopy(self.info) - state["dims"] = (*state["dims"][:axis], *state["dims"][axis + 1 :]) state["freqs"] = freqs state["times"] = times if dim == "epochs": @@ -4211,7 +4214,7 @@ def read_tfrs(fname, condition=None, *, verbose=None): hdf5_dict = read_hdf5(fname, title="mnepython", slash="replace") # single TFR from TFR.save() if "inst_type_str" in hdf5_dict: - if hdf5_dict["data"].ndim == 4: + if "epoch" in hdf5_dict["dims"]: Klass = EpochsTFR elif "nave" in hdf5_dict: Klass = AverageTFR From bf74c045d5220682e6e229b95a6e406014c0c73a Mon Sep 17 00:00:00 2001 From: Daniel McCloy Date: Thu, 11 Apr 2024 14:10:59 -0500 Subject: [PATCH 259/405] fix PSD weights handling when bad annotations present (#12538) --- mne/time_frequency/psd.py | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/mne/time_frequency/psd.py b/mne/time_frequency/psd.py index b2123f933fb..b2083c22229 100644 --- a/mne/time_frequency/psd.py +++ b/mne/time_frequency/psd.py @@ -4,6 +4,7 @@ # License: BSD-3-Clause # Copyright the MNE-Python contributors. +import warnings from functools import partial import numpy as np @@ -215,7 +216,7 @@ def psd_array_welch( ) parallel, my_spect_func, n_jobs = parallel_func(_spect_func, n_jobs=n_jobs) - func = partial( + _func = partial( spectrogram, detrend=detrend, noverlap=n_overlap, @@ -232,8 +233,14 @@ def psd_array_welch( assert np.allclose(good_mask, good_mask[[0]], equal_nan=True) t_onsets, t_offsets = _mask_to_onsets_offsets(good_mask[0]) x_splits = [x[..., t_ons:t_off] for t_ons, t_off in zip(t_onsets, t_offsets)] + # weights reflect the number of samples used from each span. For spans longer + # than `n_per_seg`, trailing samples may be discarded. For spans shorter than + # `n_per_seg`, the wrapped function (`scipy.signal.spectrogram`) automatically + # reduces `n_per_seg` to match the span length (with a warning). + step = n_per_seg - n_overlap + span_lengths = [span.shape[-1] for span in x_splits] weights = [ - split.shape[-1] for split in x_splits if split.shape[-1] >= n_per_seg + w if w < n_per_seg else w - ((w - n_overlap) % step) for w in span_lengths ] agg_func = partial(np.average, weights=weights) if n_jobs > 1: @@ -241,9 +248,27 @@ def psd_array_welch( f"Data split into {len(x_splits)} (probably unequal) chunks due to " '"bad_*" annotations. Parallelization may be sub-optimal.' ) + if (np.array(span_lengths) < n_per_seg).any(): + logger.info( + "At least one good data span is shorter than n_per_seg, and will be " + "analyzed with a shorter window than the rest of the file." + ) + + def func(*args, **kwargs): + # swallow SciPy warnings caused by short good data spans + with warnings.catch_warnings(): + warnings.filterwarnings( + action="ignore", + module="scipy", + category=UserWarning, + message=r"nperseg = \d+ is greater than input length", + ) + return _func(*args, **kwargs) + else: x_splits = [arr for arr in np.array_split(x, n_jobs) if arr.size != 0] agg_func = np.concatenate + func = _func f_spect = parallel( my_spect_func(d, func=func, freq_sl=freq_sl, average=average, output=output) for d in x_splits From a6f0331685bf6fc63001ed7ba6b379ee464fd91d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20H=C3=B6chenberger?= Date: Fri, 12 Apr 2024 18:34:18 +0200 Subject: [PATCH 260/405] MRG: Simplify manual installation instructions a little by dropping explicit mention of (lib)mamba (#12362) Co-authored-by: Eric Larson --- doc/install/manual_install.rst | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/doc/install/manual_install.rst b/doc/install/manual_install.rst index c95db0ae2d6..ab7ad074e51 100644 --- a/doc/install/manual_install.rst +++ b/doc/install/manual_install.rst @@ -15,19 +15,20 @@ Installing MNE-Python with all dependencies ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ If you use Anaconda, we suggest installing MNE-Python into its own ``conda`` environment. -The dependency stack is large and may take a long time (several tens of -minutes) to resolve on some systems via the default ``conda`` solver. We -therefore highly recommend using the new `libmamba `__ -solver instead, which is **much** faster. To permanently change to this solver, -you can set ``CONDA_SOLVER=libmamba`` in your environment or run -``conda config --set solver libmamba``. Below we just use ``--solver`` in each command. +First, please ensure you're using a recent version of ``conda``. Run in your terminal: -Run in your terminal: +.. code-block:: console + + $ conda update --name=base conda # update conda + $ conda --version + +The installed ``conda`` version should be ``23.10.0`` or newer. + +Now, you can install MNE-Python: .. code-block:: console - $ conda install --channel=conda-forge --name=base conda-libmamba-solver - $ conda create --solver=libmamba --override-channels --channel=conda-forge --name=mne mne + $ conda create --channel=conda-forge --strict-channel-priority --name=mne mne This will create a new ``conda`` environment called ``mne`` (you can adjust this by passing a different name via ``--name``) and install all @@ -50,7 +51,7 @@ or via :code:`conda`: .. code-block:: console - $ conda create --override-channels --channel=conda-forge --name=mne mne-base + $ conda create --channel=conda-forge --strict-channel-priority --name=mne mne-base This will create a new ``conda`` environment called ``mne`` (you can adjust this by passing a different name via ``--name``). From bf2d368b65ff8adf92bde35f9ae2d6fdd3a03104 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Mon, 15 Apr 2024 08:53:15 -0400 Subject: [PATCH 261/405] MAINT: Bump to latest pydata-sphinx-theme (#12228) Co-authored-by: Daniel McCloy --- doc/_static/style.css | 47 +++++--------------------------- doc/development/contributing.rst | 14 +++++----- doc/development/roadmap.rst | 2 -- pyproject.toml | 2 +- 4 files changed, 15 insertions(+), 50 deletions(-) diff --git a/doc/_static/style.css b/doc/_static/style.css index ccf032c4a7b..11a27b72c92 100644 --- a/doc/_static/style.css +++ b/doc/_static/style.css @@ -7,8 +7,6 @@ --pst-font-family-monospace: 'Source Code Pro', var(--pst-font-family-monospace-system); /* colors that aren't responsive to light/dark mode */ --mne-color-discord: #5865F2; - --mne-color-primary: #007bff; - --mne-color-primary-highlight: #0063cc; /* font weight */ --mne-font-weight-semibold: 600; } @@ -25,18 +23,11 @@ html[data-theme="light"] { --mne-color-card-header: rgba(0, 0, 0, 0.05); /* section headings */ --mne-color-heading: #003e80; - /* pydata-sphinx-theme overrides */ - --pst-color-primary: var(--mne-color-primary); - --pst-color-primary-highlight: var(--mne-color-primary-highlight); - --pst-color-info: var(--pst-color-primary); - --pst-color-border: #ccc; - --pst-color-background: #fff; - --pst-color-link: var(--pst-color-primary-highlight); /* sphinx-gallery overrides */ --sg-download-a-background-color: var(--pst-color-primary); --sg-download-a-background-image: unset; --sg-download-a-border-color: var(--pst-color-border); - --sg-download-a-color: #fff; + --sg-download-a-color: var(--sd-color-primary-text); --sg-download-a-hover-background-color: var(--pst-color-primary-highlight); --sg-download-a-hover-box-shadow-1: none; --sg-download-a-hover-box-shadow-2: none; @@ -52,19 +43,11 @@ html[data-theme="dark"] { --mne-color-card-header: rgba(255, 255, 255, 0.2); /* section headings */ --mne-color-heading: #b8cbe0; - /* pydata-sphinx-theme overrides */ - --pst-color-primary: var(--mne-color-primary); - --pst-color-primary-highlight: var(--mne-color-primary-highlight); - --pst-color-info: var(--pst-color-primary); - --pst-color-border: #333; - --pst-color-background: #000; - --pst-color-link: #66b0ff; - --pst-color-on-background: #1e1e1e; /* sphinx-gallery overrides */ --sg-download-a-background-color: var(--pst-color-primary); --sg-download-a-background-image: unset; --sg-download-a-border-color: var(--pst-color-border); - --sg-download-a-color: #000; + --sg-download-a-color: var(--sd-color-primary-text); --sg-download-a-hover-background-color: var(--pst-color-primary-highlight); --sg-download-a-hover-box-shadow-1: none; --sg-download-a-hover-box-shadow-2: none; @@ -99,11 +82,6 @@ html[data-theme="dark"] img { filter: none; } -/* prev/next links */ -.prev-next-area a p.prev-next-title { - color: var(--pst-color-link); -} - /* make versionadded smaller and inline with param name */ /* don't do for deprecated / versionchanged; they have extra info (too long to fit) */ div.versionadded > p { @@ -148,8 +126,12 @@ p.sphx-glr-signature { border-radius: 0.5rem; /* ↓↓↓↓↓↓↓ these two rules copied from sphinx-design */ box-shadow: 0 .125rem .25rem var(--sd-color-shadow) !important; + color: var(--sg-download-a-color); transition: color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out; } +.sphx-glr-download a.download::before { + color: var(--sg-download-a-color); +} /* Report embedding */ iframe.sg_report { width: 95%; @@ -242,7 +224,6 @@ aside.footnote:last-child { } /* topbar nav active */ .bd-header.navbar-light#navbar-main .navbar-nav > li.active > .nav-link { - color: var(--pst-color-link); font-weight: var(--mne-font-weight-semibold); } /* topbar nav hover */ @@ -250,18 +231,6 @@ aside.footnote:last-child { .bd-header.navbar-light#navbar-main .navbar-nav li a.nav-link:hover { color: var(--pst-color-secondary); } -/* sidebar nav */ -nav.bd-links .active > a, -nav.bd-links .active:hover > a, -.toc-entry a.nav-link.active, -.toc-entry a.nav-link.active:hover { - color: var(--pst-color-link); -} -/* sidebar nav hover */ -nav.bd-links li > a:hover, -.toc-entry a.nav-link:hover { - color: var(--pst-color-secondary); -} /* *********************************************************** homepage logo */ img.logo { @@ -273,10 +242,10 @@ img.logo { ul.quicklinks a { font-weight: var(--mne-font-weight-semibold); color: var(--pst-color-text-base); + text-decoration: none; } ul.quicklinks a:hover { text-decoration: none; - color: var(--pst-color-secondary); } h5.card-header { margin-top: 0px; @@ -287,7 +256,6 @@ h5.card-header::before { height: 0px; margin-top: 0px; } - /* ******************************************************* homepage carousel */ div.frontpage-gallery { overflow: hidden; @@ -342,7 +310,6 @@ div#contributor-avatars div.card img { div#contributor-avatars div.card img { width: 3em; } - .contributor-avatar { clip-path: circle(closest-side); } diff --git a/doc/development/contributing.rst b/doc/development/contributing.rst index 6249251911f..04fa49e924b 100644 --- a/doc/development/contributing.rst +++ b/doc/development/contributing.rst @@ -93,8 +93,8 @@ Setting up your local development environment Configuring git ~~~~~~~~~~~~~~~ -.. note:: Git GUI alternative - :class: sidebar +.. admonition:: Git GUI alternative + :class: sidebar note `GitHub desktop`_ is a GUI alternative to command line git that some users appreciate; it is available for |windows| Windows and |apple| MacOS. @@ -230,8 +230,8 @@ of how that structure is set up is given here: Creating the virtual environment ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. note:: Supported Python environments - :class: sidebar +.. admonition:: Supported Python environments + :class: sidebar note We strongly recommend the `Anaconda`_ or `Miniconda`_ environment managers for Python. Other setups are possible but are not officially supported by @@ -375,7 +375,7 @@ feature, you should first synchronize your local ``main`` branch with the $ git merge upstream/main # synchronize local main branch with remote upstream main branch $ git checkout -b new-feature-x # create local branch "new-feature-x" and check it out -.. note:: Alternative +.. tip:: :class: sidebar You can save some typing by using ``git pull upstream/main`` to replace @@ -865,8 +865,8 @@ to both visualization functions and tutorials/examples. Running the test suite ~~~~~~~~~~~~~~~~~~~~~~ -.. note:: pytest flags - :class: sidebar +.. admonition:: pytest flags + :class: sidebar tip The ``-x`` flag exits the pytest run when any test fails; this can speed up debugging when running all tests in a file or module. diff --git a/doc/development/roadmap.rst b/doc/development/roadmap.rst index ced61c7e4a1..defd4eac5cc 100644 --- a/doc/development/roadmap.rst +++ b/doc/development/roadmap.rst @@ -6,8 +6,6 @@ MNE-Python. These are goals that require substantial effort and/or API design considerations. Some of these may be suitable for Google Summer of Code projects, while others require more extensive work. -.. contents:: Page contents - :local: Open ---- diff --git a/pyproject.toml b/pyproject.toml index eee0a3aa5ea..6697cbc5144 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -144,7 +144,7 @@ test_extra = [ doc = [ "sphinx>=6", "numpydoc", - "pydata_sphinx_theme==0.13.3", + "pydata_sphinx_theme==0.15.2", "sphinx-gallery", "sphinxcontrib-bibtex>=2.5", "sphinxcontrib-towncrier", From e6dedb326910ddcd30acd207aa8f35491e14ca94 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Mon, 15 Apr 2024 10:11:59 -0400 Subject: [PATCH 262/405] ENH: Improve OPM auditory dataset and example (#12539) --- mne/datasets/config.py | 5 +- mne/preprocessing/tests/test_ica.py | 2 +- mne/utils/tests/test_numerics.py | 6 +- tutorials/preprocessing/80_opm_processing.py | 68 +++++++++++++++++--- 4 files changed, 69 insertions(+), 12 deletions(-) diff --git a/mne/datasets/config.py b/mne/datasets/config.py index fb9a04e1e40..22fd45475bc 100644 --- a/mne/datasets/config.py +++ b/mne/datasets/config.py @@ -92,6 +92,7 @@ testing="0.152", misc="0.27", phantom_kit="0.2", + ucl_opm_auditory="0.2", ) TESTING_VERSIONED = f'mne-testing-data-{RELEASES["testing"]}' MISC_VERSIONED = f'mne-misc-data-{RELEASES["misc"]}' @@ -149,8 +150,8 @@ MNE_DATASETS["ucl_opm_auditory"] = dict( archive_name="auditory_OPM_stationary.zip", - hash="md5:9ed0d8d554894542b56f8e7c4c0041fe", - url="https://osf.io/download/mwrt3/?version=1", + hash="md5:b2d69aa2d656b960bd0c18968dc1a14d", + url="https://osf.io/download/tp324/?version=1", # original is mwrt3 folder_name="auditory_OPM_stationary", config_key="MNE_DATASETS_UCL_OPM_AUDITORY_PATH", ) diff --git a/mne/preprocessing/tests/test_ica.py b/mne/preprocessing/tests/test_ica.py index 299b1e961b3..6caac588229 100644 --- a/mne/preprocessing/tests/test_ica.py +++ b/mne/preprocessing/tests/test_ica.py @@ -1481,7 +1481,7 @@ def test_ica_labels(): # derive reference ICA components and append them to raw ica_rf = ICA(n_components=2, max_iter=2, allow_ref_meg=True) - with _record_warnings(), pytest.warns(UserWarning, match="did not converge"): + with _record_warnings(): # high pass and/or no convergence ica_rf.fit(raw.copy().pick("ref_meg")) icacomps = ica_rf.get_sources(raw) # rename components so they are auto-detected by find_bads_ref diff --git a/mne/utils/tests/test_numerics.py b/mne/utils/tests/test_numerics.py index e66082581a4..40560d42cc1 100644 --- a/mne/utils/tests/test_numerics.py +++ b/mne/utils/tests/test_numerics.py @@ -450,7 +450,7 @@ def test_pca(n_components, whiten): assert_array_equal(X, X_orig) X_mne = pca_mne.fit_transform(X) assert_array_equal(X, X_orig) - assert_allclose(X_skl, X_mne) + assert_allclose(X_skl, X_mne * np.sign(np.sum(X_skl * X_mne, axis=0))) assert pca_mne.n_components_ == pca_skl.n_components_ for key in ( "mean_", @@ -459,6 +459,10 @@ def test_pca(n_components, whiten): "explained_variance_ratio_", ): val_skl, val_mne = getattr(pca_skl, key), getattr(pca_mne, key) + if key == "components_": + val_mne = val_mne * np.sign( + np.sum(val_skl * val_mne, axis=1, keepdims=True) + ) assert_allclose(val_skl, val_mne) if isinstance(n_components, float): assert pca_mne.n_components_ == n_dim - 1 diff --git a/tutorials/preprocessing/80_opm_processing.py b/tutorials/preprocessing/80_opm_processing.py index 49a8159d748..8d1642d88b8 100644 --- a/tutorials/preprocessing/80_opm_processing.py +++ b/tutorials/preprocessing/80_opm_processing.py @@ -26,20 +26,20 @@ # %% import matplotlib.pyplot as plt +import nibabel as nib import numpy as np import mne -opm_data_folder = mne.datasets.ucl_opm_auditory.data_path() +subject = "sub-002" +data_path = mne.datasets.ucl_opm_auditory.data_path() opm_file = ( - opm_data_folder - / "sub-002" - / "ses-001" - / "meg" - / "sub-002_ses-001_task-aef_run-001_meg.bin" + data_path / subject / "ses-001" / "meg" / "sub-002_ses-001_task-aef_run-001_meg.bin" ) +subjects_dir = data_path / "derivatives" / "freesurfer" / "subjects" + # For now we are going to assume the device and head coordinate frames are -# identical (even though this is incorrect), so we pass verbose='error' for now +# identical (even though this is incorrect), so we pass verbose='error' raw = mne.io.read_raw_fil(opm_file, verbose="error") raw.crop(120, 210).load_data() # crop for speed @@ -240,7 +240,59 @@ raw, events, tmin=-0.1, tmax=0.4, baseline=(-0.1, 0.0), verbose="error" ) evoked = epochs.average() -evoked.plot() +t_peak = evoked.times[np.argmax(np.std(evoked.copy().pick("meg").data, axis=0))] +fig = evoked.plot() +fig.axes[0].axvline(t_peak, color="red", ls="--", lw=1) + +# %% +# Visualizing coregistration +# -------------------------- +# By design, the sensors in this dataset are already in the scanner RAS coordinate +# frame. We can thus visualize them in the FreeSurfer MRI coordinate frame by computing +# the transformation between the FreeSurfer MRI coordinate frame and scanner RAS: + +mri = nib.load(subjects_dir / "sub-002" / "mri" / "T1.mgz") +trans = mri.header.get_vox2ras_tkr() @ np.linalg.inv(mri.affine) +trans[:3, 3] /= 1000.0 # nibabel uses mm, MNE uses m +trans = mne.transforms.Transform("head", "mri", trans) + +bem = subjects_dir / subject / "bem" / f"{subject}-5120-bem-sol.fif" +src = subjects_dir / subject / "bem" / f"{subject}-oct-6-src.fif" +mne.viz.plot_alignment( + evoked.info, + subjects_dir=subjects_dir, + subject=subject, + trans=trans, + surfaces={"head": 0.1, "inner_skull": 0.2, "white": 1.0}, + meg=["helmet", "sensors"], + verbose="error", + bem=bem, + src=src, +) + +# %% +# Plotting the inverse +# -------------------- +# Now we can compute a forward and inverse: + +fwd = mne.make_forward_solution( + evoked.info, + trans=trans, + bem=bem, + src=src, + verbose=True, +) +noise_cov = mne.compute_covariance(epochs, tmax=0) +inv = mne.minimum_norm.make_inverse_operator(evoked.info, fwd, noise_cov, verbose=True) +stc = mne.minimum_norm.apply_inverse( + evoked, inv, 1.0 / 9.0, method="dSPM", verbose=True +) +brain = stc.plot( + hemi="split", + size=(800, 400), + initial_time=t_peak, + subjects_dir=subjects_dir, +) # %% # References From 8c7daf49667d317b1759264eaaff01cc769ee656 Mon Sep 17 00:00:00 2001 From: teekuningas Date: Mon, 15 Apr 2024 20:20:00 +0300 Subject: [PATCH 263/405] Add Meggie under Related Software documentation (#12540) --- doc/install/mne_tools_suite.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/install/mne_tools_suite.rst b/doc/install/mne_tools_suite.rst index 4b82b0c16fb..64b3933ea0f 100644 --- a/doc/install/mne_tools_suite.rst +++ b/doc/install/mne_tools_suite.rst @@ -66,6 +66,7 @@ MNE-Python, including packages for: and effect localization (`MNE-ARI`_) - real-time analysis (`MNE-Realtime`_) - non-parametric sequential analyses and adaptive sample size determination (`niseq`_) +- a graphical user interface for multi-subject MEG/EEG analysis with plugin support (`Meggie`_) What should I install? ^^^^^^^^^^^^^^^^^^^^^^ @@ -110,5 +111,6 @@ Help with installation is available through the `MNE Forum`_. See the .. _invertmeeg: https://github.com/LukeTheHecker/invert .. _MNE-ARI: https://github.com/john-veillette/mne_ari .. _niseq: https://github.com/john-veillette/niseq +.. _Meggie: https://github.com/cibr-jyu/meggie .. include:: ../links.inc From 5af2dd7f9f25511727716072bc27fcaed251a159 Mon Sep 17 00:00:00 2001 From: ivopascal Date: Mon, 15 Apr 2024 19:46:13 +0200 Subject: [PATCH 264/405] Implement `picks` argument to `Raw.plot()` (#12467) 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 --- doc/changes/devel/12467.newfeature.rst | 1 + doc/changes/names.inc | 2 ++ mne/io/base.py | 2 ++ mne/viz/raw.py | 8 ++++++-- mne/viz/tests/test_raw.py | 20 +++++++++++++++++++- 5 files changed, 30 insertions(+), 3 deletions(-) create mode 100644 doc/changes/devel/12467.newfeature.rst diff --git a/doc/changes/devel/12467.newfeature.rst b/doc/changes/devel/12467.newfeature.rst new file mode 100644 index 00000000000..457a2746d17 --- /dev/null +++ b/doc/changes/devel/12467.newfeature.rst @@ -0,0 +1 @@ +Add ``picks`` parameter to :meth:`mne.io.Raw.plot`, allowing users to select which channels to plot. This makes makes the raw data plotting API consistent with :meth:`mne.Epochs.plot` and :meth:`mne.Evoked.plot`, by :newcontrib:`Ivo de Jong`. \ No newline at end of file diff --git a/doc/changes/names.inc b/doc/changes/names.inc index 7a4ea591144..112418f7e72 100644 --- a/doc/changes/names.inc +++ b/doc/changes/names.inc @@ -226,6 +226,8 @@ .. _Ivana Kojcic: https://github.com/ikojcic +.. _Ivo de Jong: https://github.com/ivopascal + .. _Jaakko Leppakangas: https://github.com/jaeilepp .. _Jack Zhang: https://github.com/jackz314 diff --git a/mne/io/base.py b/mne/io/base.py index b22b760101e..ae622cfa307 100644 --- a/mne/io/base.py +++ b/mne/io/base.py @@ -1847,6 +1847,7 @@ def plot( precompute=None, use_opengl=None, *, + picks=None, theme=None, overview_mode=None, splash=True, @@ -1885,6 +1886,7 @@ def plot( time_format=time_format, precompute=precompute, use_opengl=use_opengl, + picks=picks, theme=theme, overview_mode=overview_mode, splash=splash, diff --git a/mne/viz/raw.py b/mne/viz/raw.py index 65bfb08604e..b54222c807c 100644 --- a/mne/viz/raw.py +++ b/mne/viz/raw.py @@ -11,7 +11,7 @@ import numpy as np -from .._fiff.pick import pick_channels, pick_types +from .._fiff.pick import _picks_to_idx, pick_channels, pick_types from ..defaults import _handle_default from ..filter import create_filter from ..utils import _check_option, _get_stim_channel, _validate_type, legacy, verbose @@ -63,6 +63,7 @@ def plot_raw( time_format="float", precompute=None, use_opengl=None, + picks=None, *, theme=None, overview_mode=None, @@ -192,6 +193,7 @@ def plot_raw( %(time_format)s %(precompute)s %(use_opengl)s + %(picks_all)s %(theme_pg)s .. versionadded:: 1.0 @@ -310,7 +312,9 @@ def plot_raw( # determine trace order ch_names = np.array(raw.ch_names) ch_types = np.array(raw.get_channel_types()) - order = _get_channel_plotting_order(order, ch_types) + + picks = _picks_to_idx(info, picks, none="all") + order = _get_channel_plotting_order(order, ch_types, picks=picks) n_channels = min(info["nchan"], n_channels, len(order)) # adjust order based on channel selection, if needed selections = None diff --git a/mne/viz/tests/test_raw.py b/mne/viz/tests/test_raw.py index c1920aebf93..441ed79c3f3 100644 --- a/mne/viz/tests/test_raw.py +++ b/mne/viz/tests/test_raw.py @@ -11,7 +11,7 @@ import numpy as np import pytest from matplotlib import backend_bases -from numpy.testing import assert_allclose +from numpy.testing import assert_allclose, assert_array_equal from mne import Annotations, create_info, pick_types from mne._fiff.pick import _DATA_CH_TYPES_ORDER_DEFAULT, _PICK_TYPES_DATA_DICT @@ -624,6 +624,24 @@ def test_plot_raw_traces(raw, events, browser_backend): plot_raw(raw, events=events, event_color={-1: "r", 998: "b"}) +def test_plot_raw_picks(raw, browser_backend): + """Test functionality of picks and order arguments.""" + with raw.info._unlock(): + raw.info["lowpass"] = 10.0 # allow heavy decim during plotting + + fig = raw.plot(picks=["MEG 0112"]) + assert len(fig.mne.traces) == 1 + + fig = raw.plot(picks=["meg"]) + assert len(fig.mne.traces) == len(raw.get_channel_types(picks="meg")) + + fig = raw.plot(order=[4, 3]) + assert_array_equal(fig.mne.ch_order, np.array([4, 3])) + + fig = raw.plot(picks=[4, 3]) + assert_array_equal(fig.mne.ch_order, np.array([3, 4])) + + @pytest.mark.parametrize("group_by", ("position", "selection")) def test_plot_raw_groupby(raw, browser_backend, group_by): """Test group-by plotting of raw data.""" From 0aa4bec2228f5d14c38d11fc37cea7a1e8c5140c Mon Sep 17 00:00:00 2001 From: Judy D Zhu <38392787+JD-Zhu@users.noreply.github.com> Date: Tue, 16 Apr 2024 04:41:15 +1000 Subject: [PATCH 265/405] ENH: Allow removal of (up to two) bad marker coils in read_raw_kit() (#12394) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Eric Larson --- doc/changes/devel/12394.newfeature.rst | 1 + mne/io/kit/coreg.py | 15 +++++++++++++-- mne/io/kit/kit.py | 22 +++++++++++++++++++--- mne/utils/docs.py | 6 ++++++ 4 files changed, 39 insertions(+), 5 deletions(-) create mode 100644 doc/changes/devel/12394.newfeature.rst diff --git a/doc/changes/devel/12394.newfeature.rst b/doc/changes/devel/12394.newfeature.rst new file mode 100644 index 00000000000..de456e91461 --- /dev/null +++ b/doc/changes/devel/12394.newfeature.rst @@ -0,0 +1 @@ +Add ability to remove bad marker coils in :func:`mne.io.read_raw_kit`, by `Judy D Zhu`_. \ No newline at end of file diff --git a/mne/io/kit/coreg.py b/mne/io/kit/coreg.py index f58f1e29acf..0887a4b4022 100644 --- a/mne/io/kit/coreg.py +++ b/mne/io/kit/coreg.py @@ -114,7 +114,7 @@ def read_sns(fname): return locs -def _set_dig_kit(mrk, elp, hsp, eeg): +def _set_dig_kit(mrk, elp, hsp, eeg, *, bad_coils=()): """Add landmark points and head shape data to the KIT instance. Digitizer data (elp and hsp) are represented in [mm] in the Polhemus @@ -133,6 +133,9 @@ def _set_dig_kit(mrk, elp, hsp, eeg): Digitizer head shape points, or path to head shape file. If more than 10`000 points are in the head shape, they are automatically decimated. + bad_coils : list + Indices of bad marker coils (up to two). Bad coils will be excluded + when computing the device-head transformation. eeg : dict Ordered dict of EEG dig points. @@ -167,10 +170,18 @@ def _set_dig_kit(mrk, elp, hsp, eeg): f"{elp_points.shape}." ) elp = elp_points - elif len(elp) not in (6, 7, 8): + if len(bad_coils) > 0: + elp = np.delete(elp, np.array(bad_coils) + 3, 0) + # check we have at least 3 marker coils (whether read from file or + # passed in directly) + if len(elp) not in (6, 7, 8): raise ValueError(f"ELP should contain 6 ~ 8 points; got shape {elp.shape}.") if isinstance(mrk, (str, Path, PathLike)): mrk = read_mrk(mrk) + if len(bad_coils) > 0: + mrk = np.delete(mrk, bad_coils, 0) + if len(mrk) not in (3, 4, 5): + raise ValueError(f"MRK should contain 3 ~ 5 points; got shape {mrk.shape}.") mrk = apply_trans(als_ras_trans, mrk) diff --git a/mne/io/kit/kit.py b/mne/io/kit/kit.py index 737222b0090..71cc38e6c94 100644 --- a/mne/io/kit/kit.py +++ b/mne/io/kit/kit.py @@ -43,7 +43,7 @@ INT32 = " RawKIT: r"""Reader function for Ricoh/KIT conversion to FIF. @@ -931,6 +945,7 @@ def read_raw_kit( Force reading old data that is not officially supported. Alternatively, read and re-save the data with the KIT MEG Laboratory application. %(standardize_names)s + %(kit_badcoils)s %(verbose)s Returns @@ -965,6 +980,7 @@ def read_raw_kit( stim_code=stim_code, allow_unknown_format=allow_unknown_format, standardize_names=standardize_names, + bad_coils=bad_coils, verbose=verbose, ) diff --git a/mne/utils/docs.py b/mne/utils/docs.py index e8695499742..f29ff9508a5 100644 --- a/mne/utils/docs.py +++ b/mne/utils/docs.py @@ -2215,6 +2215,12 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): anonymized. Use with caution. """ +docdict["kit_badcoils"] = """ +bad_coils : array-like of int | None + Indices of (up to two) bad marker coils to be removed. + These marker coils must be present in the elp and mrk files. +""" + docdict["kit_elp"] = """ elp : path-like | array of shape (8, 3) | None Digitizer points representing the location of the fiducials and the From 637b4343f059d3c03e9e68f7fd65ef1ffc3b33c7 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 15 Apr 2024 18:59:42 -0400 Subject: [PATCH 266/405] [pre-commit.ci] pre-commit autoupdate (#12541) 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 40b85a11eb4..744e28edcf7 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.5 + rev: v0.3.7 hooks: - id: ruff name: ruff lint mne From 73661d10942187a263a61ced85d31ff5eb8e1ce8 Mon Sep 17 00:00:00 2001 From: rcmdnk Date: Tue, 16 Apr 2024 16:36:03 +0900 Subject: [PATCH 267/405] fix prefilter management for EDF/BDF (#12441) Co-authored-by: Eric Larson --- doc/changes/devel/12441.bugfix.rst | 1 + mne/io/edf/edf.py | 109 ++++++++--------- mne/io/edf/tests/test_edf.py | 185 +++++++++++++++++++++-------- mne/io/edf/tests/test_gdf.py | 4 +- mne/io/tests/test_read_raw.py | 8 +- 5 files changed, 190 insertions(+), 117 deletions(-) create mode 100644 doc/changes/devel/12441.bugfix.rst diff --git a/doc/changes/devel/12441.bugfix.rst b/doc/changes/devel/12441.bugfix.rst new file mode 100644 index 00000000000..87a2d10a710 --- /dev/null +++ b/doc/changes/devel/12441.bugfix.rst @@ -0,0 +1 @@ +Fix prefiltering information management for EDF/BDF, by `Michiru Kaneda`_ diff --git a/mne/io/edf/edf.py b/mne/io/edf/edf.py index ec62eee168a..8a982f43e86 100644 --- a/mne/io/edf/edf.py +++ b/mne/io/edf/edf.py @@ -706,46 +706,10 @@ def _get_info( info["subject_info"]["weight"] = float(edf_info["subject_info"]["weight"]) # Filter settings - highpass = edf_info["highpass"] - lowpass = edf_info["lowpass"] - if highpass.size == 0: - pass - elif all(highpass): - if highpass[0] == "NaN": - # Placeholder for future use. Highpass set in _empty_info. - pass - elif highpass[0] == "DC": - info["highpass"] = 0.0 - else: - hp = highpass[0] - try: - hp = float(hp) - except Exception: - hp = 0.0 - info["highpass"] = hp - else: - info["highpass"] = float(np.max(highpass)) - warn( - "Channels contain different highpass filters. Highest filter " - "setting will be stored." - ) - if np.isnan(info["highpass"]): - info["highpass"] = 0.0 - if lowpass.size == 0: - # Placeholder for future use. Lowpass set in _empty_info. - pass - elif all(lowpass): - if lowpass[0] in ("NaN", "0", "0.0"): - # Placeholder for future use. Lowpass set in _empty_info. - pass - else: - info["lowpass"] = float(lowpass[0]) - else: - info["lowpass"] = float(np.min(lowpass)) - warn( - "Channels contain different lowpass filters. Lowest filter " - "setting will be stored." - ) + if filt_ch_idxs := [x for x in sel if x not in stim_channel_idxs]: + _set_prefilter(info, edf_info, filt_ch_idxs, "highpass") + _set_prefilter(info, edf_info, filt_ch_idxs, "lowpass") + if np.isnan(info["lowpass"]): info["lowpass"] = info["sfreq"] / 2.0 @@ -785,25 +749,47 @@ def _get_info( def _parse_prefilter_string(prefiltering): """Parse prefilter string from EDF+ and BDF headers.""" - highpass = np.array( - [ - v - for hp in [ - re.findall(r"HP:\s*([0-9]+[.]*[0-9]*)", filt) for filt in prefiltering - ] - for v in hp - ] - ) - lowpass = np.array( - [ - v - for hp in [ - re.findall(r"LP:\s*([0-9]+[.]*[0-9]*)", filt) for filt in prefiltering - ] - for v in hp - ] - ) - return highpass, lowpass + filter_types = ["HP", "LP"] + filter_strings = {t: [] for t in filter_types} + for filt in prefiltering: + for t in filter_types: + matches = re.findall(rf"{t}:\s*([a-zA-Z0-9,.]+)(Hz)?", filt) + value = "" + for match in matches: + if match[0]: + value = match[0].replace("Hz", "").replace(",", ".") + filter_strings[t].append(value) + return np.array(filter_strings["HP"]), np.array(filter_strings["LP"]) + + +def _prefilter_float(filt): + if isinstance(filt, (int, float, np.number)): + return filt + if filt == "DC": + return 0.0 + if filt.replace(".", "", 1).isdigit(): + return float(filt) + return np.nan + + +def _set_prefilter(info, edf_info, ch_idxs, key): + value = 0 + if len(values := edf_info.get(key, [])): + values = [x for i, x in enumerate(values) if i in ch_idxs] + if len(np.unique(values)) > 1: + warn( + f"Channels contain different {key} filters. " + f"{'Highest' if key == 'highpass' else 'Lowest'} filter " + "setting will be stored." + ) + if key == "highpass": + value = np.nanmax([_prefilter_float(x) for x in values]) + else: + value = np.nanmin([_prefilter_float(x) for x in values]) + else: + value = _prefilter_float(values[0]) + if not np.isnan(value) and value != 0: + info[key] = value def _edf_str(x): @@ -947,6 +933,7 @@ def _read_edf_header( exclude = _find_exclude_idx(ch_names, exclude, include) exclude = np.concatenate([exclude, tal_idx]) sel = np.setdiff1d(np.arange(len(ch_names)), exclude) + for ch in channels: fid.read(80) # transducer units = [fid.read(8).strip().decode("latin-1") for ch in channels] @@ -983,7 +970,7 @@ def _read_edf_header( digital_max = np.array([float(_edf_str_num(fid.read(8))) for ch in channels])[ sel ] - prefiltering = [_edf_str(fid.read(80)).strip() for ch in channels][:-1] + prefiltering = np.array([_edf_str(fid.read(80)).strip() for ch in channels]) highpass, lowpass = _parse_prefilter_string(prefiltering) # number of samples per record @@ -1161,7 +1148,7 @@ def _read_gdf_header(fname, exclude, include=None): physical_max = np.fromfile(fid, FLOAT64, len(channels)) digital_min = np.fromfile(fid, INT64, len(channels)) digital_max = np.fromfile(fid, INT64, len(channels)) - prefiltering = [_edf_str(fid.read(80)) for ch in channels][:-1] + prefiltering = [_edf_str(fid.read(80)) for ch in channels] highpass, lowpass = _parse_prefilter_string(prefiltering) # n samples per record diff --git a/mne/io/edf/tests/test_edf.py b/mne/io/edf/tests/test_edf.py index 8ae55fdcc11..7517693b6ea 100644 --- a/mne/io/edf/tests/test_edf.py +++ b/mne/io/edf/tests/test_edf.py @@ -31,10 +31,12 @@ from mne.io.edf.edf import ( _edf_str, _parse_prefilter_string, + _prefilter_float, _read_annotations_edf, _read_ch, _read_edf_header, _read_header, + _set_prefilter, ) from mne.io.tests.test_raw import _test_raw_reader from mne.tests.test_annotations import _assert_annotations_equal @@ -173,24 +175,26 @@ def test_bdf_data(): # XXX BDF data for these is around 0.01 when it should be in the uV range, # probably some bug test_scaling = False - raw_py = _test_raw_reader( - read_raw_bdf, - input_fname=bdf_path, - eog=eog, - misc=misc, - exclude=["M2", "IEOG"], - test_scaling=test_scaling, - ) + with pytest.warns(RuntimeWarning, match="Channels contain different"): + raw_py = _test_raw_reader( + read_raw_bdf, + input_fname=bdf_path, + eog=eog, + misc=misc, + exclude=["M2", "IEOG"], + test_scaling=test_scaling, + ) assert len(raw_py.ch_names) == 71 - raw_py = _test_raw_reader( - read_raw_bdf, - input_fname=bdf_path, - montage="biosemi64", - eog=eog, - misc=misc, - exclude=["M2", "IEOG"], - test_scaling=test_scaling, - ) + with pytest.warns(RuntimeWarning, match="Channels contain different"): + raw_py = _test_raw_reader( + read_raw_bdf, + input_fname=bdf_path, + montage="biosemi64", + eog=eog, + misc=misc, + exclude=["M2", "IEOG"], + test_scaling=test_scaling, + ) assert len(raw_py.ch_names) == 71 assert "RawEDF" in repr(raw_py) picks = pick_types(raw_py.info, meg=False, eeg=True, exclude="bads") @@ -631,27 +635,101 @@ def test_read_latin1_annotations(tmp_path): _read_annotations_edf(str(annot_file)) # default encoding="utf8" fails -def test_edf_prefilter_parse(): +@pytest.mark.parametrize( + "prefiltering, hp, lp", + [ + pytest.param(["HP: 1Hz LP: 30Hz"], ["1"], ["30"], id="basic edf"), + pytest.param(["LP: 30Hz HP: 1Hz"], ["1"], ["30"], id="reversed order"), + pytest.param(["HP: 1 LP: 30"], ["1"], ["30"], id="w/o Hz"), + pytest.param(["HP: 0,1 LP: 30,5"], ["0.1"], ["30.5"], id="using comma"), + pytest.param( + ["HP:0.1Hz LP:75Hz N:50Hz"], ["0.1"], ["75"], id="with notch filter" + ), + pytest.param([""], [""], [""], id="empty string"), + pytest.param(["HP: DC; LP: 410"], ["DC"], ["410"], id="bdf_dc"), + pytest.param( + ["", "HP:0.1Hz LP:75Hz N:50Hz", ""], + ["", "0.1", ""], + ["", "75", ""], + id="multi-ch", + ), + ], +) +def test_edf_parse_prefilter_string(prefiltering, hp, lp): """Test prefilter strings from header are parsed correctly.""" - prefilter_basic = ["HP: 0Hz LP: 0Hz"] - highpass, lowpass = _parse_prefilter_string(prefilter_basic) - assert_array_equal(highpass, ["0"]) - assert_array_equal(lowpass, ["0"]) + highpass, lowpass = _parse_prefilter_string(prefiltering) + assert_array_equal(highpass, hp) + assert_array_equal(lowpass, lp) - prefilter_normal_multi_ch = ["HP: 1Hz LP: 30Hz"] * 10 - highpass, lowpass = _parse_prefilter_string(prefilter_normal_multi_ch) - assert_array_equal(highpass, ["1"] * 10) - assert_array_equal(lowpass, ["30"] * 10) - prefilter_unfiltered_ch = prefilter_normal_multi_ch + [""] - highpass, lowpass = _parse_prefilter_string(prefilter_unfiltered_ch) - assert_array_equal(highpass, ["1"] * 10) - assert_array_equal(lowpass, ["30"] * 10) +@pytest.mark.parametrize( + "prefilter_string, expected", + [ + ("0", 0), + ("1.1", 1.1), + ("DC", 0), + ("", np.nan), + ("1.1.1", np.nan), + (1.1, 1.1), + (1, 1), + (np.float32(1.1), np.float32(1.1)), + (np.nan, np.nan), + ], +) +def test_edf_prefilter_float(prefilter_string, expected): + """Test to make float from prefilter string.""" + assert_equal(_prefilter_float(prefilter_string), expected) - prefilter_edf_specs_doc = ["HP:0.1Hz LP:75Hz N:50Hz"] - highpass, lowpass = _parse_prefilter_string(prefilter_edf_specs_doc) - assert_array_equal(highpass, ["0.1"]) - assert_array_equal(lowpass, ["75"]) + +@pytest.mark.parametrize( + "edf_info, hp, lp, hp_warn, lp_warn", + [ + ({"highpass": ["0"], "lowpass": ["1.1"]}, -1, 1.1, False, False), + ({"highpass": [""], "lowpass": [""]}, -1, -1, False, False), + ({"highpass": ["DC"], "lowpass": [""]}, -1, -1, False, False), + ({"highpass": [1], "lowpass": [2]}, 1, 2, False, False), + ({"highpass": [np.nan], "lowpass": [np.nan]}, -1, -1, False, False), + ({"highpass": ["1", "2"], "lowpass": ["3", "4"]}, 2, 3, True, True), + ({"highpass": [np.nan, 1], "lowpass": ["", 3]}, 1, 3, True, True), + ({"highpass": [np.nan, np.nan], "lowpass": [1, 2]}, -1, 1, False, True), + ({}, -1, -1, False, False), + ], +) +def test_edf_set_prefilter(edf_info, hp, lp, hp_warn, lp_warn): + """Test _set_prefilter function.""" + info = {"lowpass": -1, "highpass": -1} + + if hp_warn: + ctx = pytest.warns( + RuntimeWarning, + match=( + "Channels contain different highpass filters. " + "Highest filter setting will be stored." + ), + ) + else: + ctx = nullcontext() + with ctx: + _set_prefilter( + info, edf_info, list(range(len(edf_info.get("highpass", [])))), "highpass" + ) + + if lp_warn: + ctx = pytest.warns( + RuntimeWarning, + match=( + "Channels contain different lowpass filters. " + "Lowest filter setting will be stored." + ), + ) + else: + ctx = nullcontext() + with ctx: + _set_prefilter( + info, edf_info, list(range(len(edf_info.get("lowpass", [])))), "lowpass" + ) + assert info["highpass"] == hp + assert info["lowpass"] == lp @testing.requires_testing_data @@ -832,37 +910,40 @@ def test_empty_chars(): def _hp_lp_rev(*args, **kwargs): out, orig_units = _read_edf_header(*args, **kwargs) out["lowpass"], out["highpass"] = out["highpass"], out["lowpass"] - # this will happen for test_edf_stim_resamp.edf - if ( - len(out["lowpass"]) - and out["lowpass"][0] == "0.000" - and len(out["highpass"]) - and out["highpass"][0] == "0.0" - ): - out["highpass"][0] = "10.0" + return out, orig_units + + +def _hp_lp_mod(*args, **kwargs): + out, orig_units = _read_edf_header(*args, **kwargs) + out["lowpass"][:] = "1" + out["highpass"][:] = "10" return out, orig_units @pytest.mark.filterwarnings("ignore:.*too long.*:RuntimeWarning") @pytest.mark.parametrize( - "fname, lo, hi, warns", + "fname, lo, hi, warns, patch_func", [ - (edf_path, 256, 0, False), - (edf_uneven_path, 50, 0, False), - (edf_stim_channel_path, 64, 0, False), - pytest.param(edf_overlap_annot_path, 64, 0, False, marks=td_mark), - pytest.param(edf_reduced, 256, 0, False, marks=td_mark), - pytest.param(test_generator_edf, 100, 0, False, marks=td_mark), - pytest.param(edf_stim_resamp_path, 256, 0, True, marks=td_mark), + (edf_path, 256, 0, False, "rev"), + (edf_uneven_path, 50, 0, False, "rev"), + (edf_stim_channel_path, 64, 0, False, "rev"), + pytest.param(edf_overlap_annot_path, 64, 0, False, "rev", marks=td_mark), + pytest.param(edf_reduced, 256, 0, False, "rev", marks=td_mark), + pytest.param(test_generator_edf, 100, 0, False, "rev", marks=td_mark), + pytest.param(edf_stim_resamp_path, 256, 0, False, "rev", marks=td_mark), + pytest.param(edf_stim_resamp_path, 256, 0, True, "mod", marks=td_mark), ], ) -def test_hp_lp_reversed(fname, lo, hi, warns, monkeypatch): +def test_hp_lp_reversed(fname, lo, hi, warns, patch_func, monkeypatch): """Test HP/LP reversed (gh-8584).""" fname = str(fname) raw = read_raw_edf(fname) assert raw.info["lowpass"] == lo assert raw.info["highpass"] == hi - monkeypatch.setattr(edf.edf, "_read_edf_header", _hp_lp_rev) + if patch_func == "rev": + monkeypatch.setattr(edf.edf, "_read_edf_header", _hp_lp_rev) + elif patch_func == "mod": + monkeypatch.setattr(edf.edf, "_read_edf_header", _hp_lp_mod) if warns: ctx = pytest.warns(RuntimeWarning, match="greater than lowpass") new_lo, new_hi = raw.info["sfreq"] / 2.0, 0.0 diff --git a/mne/io/edf/tests/test_gdf.py b/mne/io/edf/tests/test_gdf.py index 9ae33ee2feb..8942d13f8a6 100644 --- a/mne/io/edf/tests/test_gdf.py +++ b/mne/io/edf/tests/test_gdf.py @@ -8,7 +8,6 @@ from datetime import datetime, timedelta, timezone import numpy as np -import pytest import scipy.io as sio from numpy.testing import assert_array_almost_equal, assert_array_equal, assert_equal @@ -153,8 +152,7 @@ def test_gdf2_data(): @testing.requires_testing_data def test_one_channel_gdf(): """Test a one-channel GDF file.""" - with pytest.warns(RuntimeWarning, match="contain different"): - ecg = read_raw_gdf(gdf_1ch_path, preload=True) + ecg = read_raw_gdf(gdf_1ch_path, preload=True) assert ecg["ECG"][0].shape == (1, 4500) assert 150.0 == ecg.info["sfreq"] diff --git a/mne/io/tests/test_read_raw.py b/mne/io/tests/test_read_raw.py index f98d1147539..eccd074d9a0 100644 --- a/mne/io/tests/test_read_raw.py +++ b/mne/io/tests/test_read_raw.py @@ -50,7 +50,13 @@ def test_read_raw_suggested(fname): base / "tests/data/test_raw.fif", base / "tests/data/test_raw.fif.gz", base / "edf/tests/data/test.edf", - base / "edf/tests/data/test.bdf", + pytest.param( + base / "edf/tests/data/test.bdf", + marks=( + _testing_mark, + pytest.mark.filterwarnings("ignore:Channels contain different"), + ), + ), base / "brainvision/tests/data/test.vhdr", base / "kit/tests/data/test.sqd", pytest.param(test_base / "KIT" / "data_berlin.con", marks=_testing_mark), From e23e9e1bdd3ecf63af9386cd2a19129d26900864 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Tue, 16 Apr 2024 12:52:09 -0400 Subject: [PATCH 268/405] BUG: Fix bug with CSP rank (#12476) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- doc/changes/devel/12476.bugfix.rst | 1 + examples/decoding/decoding_csp_eeg.py | 1 + mne/cov.py | 92 +++++++++++++++----- mne/decoding/csp.py | 79 ++++++++++++++--- mne/decoding/tests/test_csp.py | 117 +++++++++++++++++++------- mne/rank.py | 89 +++++++++++++++----- 6 files changed, 297 insertions(+), 82 deletions(-) create mode 100644 doc/changes/devel/12476.bugfix.rst diff --git a/doc/changes/devel/12476.bugfix.rst b/doc/changes/devel/12476.bugfix.rst new file mode 100644 index 00000000000..464ef11307c --- /dev/null +++ b/doc/changes/devel/12476.bugfix.rst @@ -0,0 +1 @@ +Fixed bugs with handling of rank in :class:`mne.decoding.CSP`, by `Eric Larson`_. \ No newline at end of file diff --git a/examples/decoding/decoding_csp_eeg.py b/examples/decoding/decoding_csp_eeg.py index 6120bd5e5dd..2ffd18d34b4 100644 --- a/examples/decoding/decoding_csp_eeg.py +++ b/examples/decoding/decoding_csp_eeg.py @@ -49,6 +49,7 @@ montage = make_standard_montage("standard_1005") raw.set_montage(montage) raw.annotations.rename(dict(T1="hands", T2="feet")) +raw.set_eeg_reference(projection=True) # Apply band-pass filter raw.filter(7.0, 30.0, fir_design="firwin", skip_by_annotation="edge") diff --git a/mne/cov.py b/mne/cov.py index 5c0e455a52c..7772a0a8324 100644 --- a/mne/cov.py +++ b/mne/cov.py @@ -59,7 +59,7 @@ empirical_covariance, log_likelihood, ) -from .rank import compute_rank +from .rank import _compute_rank from .utils import ( _array_repr, _check_fname, @@ -1226,6 +1226,21 @@ def _eigvec_subspace(eig, eigvec, mask): return eig, eigvec +@verbose +def _compute_rank_raw_array( + data, info, rank, scalings, *, log_ch_type=None, verbose=None +): + from .io import RawArray + + return _compute_rank( + RawArray(data, info, copy=None, verbose=_verbose_safe_false()), + rank, + scalings, + info, + log_ch_type=log_ch_type, + ) + + def _compute_covariance_auto( data, method, @@ -1237,22 +1252,31 @@ def _compute_covariance_auto( stop_early, picks_list, rank, + *, + cov_kind="", + log_ch_type=None, + log_rank=True, ): """Compute covariance auto mode.""" - from .io import RawArray - # rescale to improve numerical stability orig_rank = rank - rank = compute_rank( - RawArray(data.T, info, copy=None, verbose=_verbose_safe_false()), - rank, - scalings, + rank = _compute_rank_raw_array( + data.T, info, + rank=rank, + scalings=scalings, + verbose=_verbose_safe_false(), ) with _scaled_array(data.T, picks_list, scalings): C = np.dot(data.T, data) _, eigvec, mask = _smart_eigh( - C, info, rank, proj_subspace=True, do_compute_rank=False + C, + info, + rank, + proj_subspace=True, + do_compute_rank=False, + log_ch_type=log_ch_type, + verbose=None if log_rank else _verbose_safe_false(), ) eigvec = eigvec[mask] data = np.dot(data, eigvec.T) @@ -1261,21 +1285,24 @@ def _compute_covariance_auto( (key, np.searchsorted(used, picks)) for key, picks in picks_list ] sub_info = pick_info(info, used) if len(used) != len(mask) else info - logger.info(f"Reducing data rank from {len(mask)} -> {eigvec.shape[0]}") + if log_rank: + logger.info(f"Reducing data rank from {len(mask)} -> {eigvec.shape[0]}") estimator_cov_info = list() - msg = "Estimating covariance using {}" ok_sklearn = check_version("sklearn") if not ok_sklearn and (len(method) != 1 or method[0] != "empirical"): raise ValueError( - "scikit-learn is not installed, `method` must be `empirical`, got " - f"{method}" + 'scikit-learn is not installed, `method` must be "empirical", got ' + f"{repr(method)}" ) for method_ in method: data_ = data.copy() name = method_.__name__ if callable(method_) else method_ - logger.info(msg.format(name.upper())) + logger.info( + f'Estimating {cov_kind + (" " if cov_kind else "")}' + f"covariance using {name.upper()}" + ) mp = method_params[method_] _info = {} @@ -1691,9 +1718,8 @@ def _get_ch_whitener(A, pca, ch_type, rank): mask[:-rank] = False logger.info( - " Setting small {} eigenvalues to zero ({})".format( - ch_type, "using PCA" if pca else "without PCA" - ) + f" Setting small {ch_type} eigenvalues to zero " + f'({"using" if pca else "without"} PCA)' ) if pca: # No PCA case. # This line will reduce the actual number of variables in data @@ -1791,6 +1817,8 @@ def _smart_eigh( proj_subspace=False, do_compute_rank=True, on_rank_mismatch="ignore", + *, + log_ch_type=None, verbose=None, ): """Compute eigh of C taking into account rank and ch_type scalings.""" @@ -1813,8 +1841,13 @@ def _smart_eigh( noise_cov = Covariance(C, ch_names, [], projs, 0) if do_compute_rank: # if necessary - rank = compute_rank( - noise_cov, rank, scalings, info, on_rank_mismatch=on_rank_mismatch + rank = _compute_rank( + noise_cov, + rank, + scalings, + info, + on_rank_mismatch=on_rank_mismatch, + log_ch_type=log_ch_type, ) assert C.ndim == 2 and C.shape[0] == C.shape[1] @@ -1838,7 +1871,11 @@ def _smart_eigh( else: this_rank = rank[ch_type] - e, ev, m = _get_ch_whitener(this_C, False, ch_type.upper(), this_rank) + if log_ch_type is not None: + ch_type_ = log_ch_type + else: + ch_type_ = ch_type.upper() + e, ev, m = _get_ch_whitener(this_C, False, ch_type_, this_rank) if proj_subspace: # Choose the subspace the same way we do for projections e, ev = _eigvec_subspace(e, ev, m) @@ -1995,7 +2032,7 @@ def regularize( else: regs.update(mag=mag, grad=grad) if rank != "full": - rank = compute_rank(cov, rank, scalings, info) + rank = _compute_rank(cov, rank, scalings, info) info_ch_names = info["ch_names"] ch_names_by_type = dict() @@ -2071,7 +2108,17 @@ def regularize( return cov -def _regularized_covariance(data, reg=None, method_params=None, info=None, rank=None): +def _regularized_covariance( + data, + reg=None, + method_params=None, + info=None, + rank=None, + *, + log_ch_type=None, + log_rank=None, + cov_kind="", +): """Compute a regularized covariance from data using sklearn. This is a convenience wrapper for mne.decoding functions, which @@ -2114,6 +2161,9 @@ def _regularized_covariance(data, reg=None, method_params=None, info=None, rank= picks_list=picks_list, scalings=scalings, rank=rank, + cov_kind=cov_kind, + log_ch_type=log_ch_type, + log_rank=log_rank, )[reg]["data"] return cov diff --git a/mne/decoding/csp.py b/mne/decoding/csp.py index ac3983e4617..ba76acd2d7c 100644 --- a/mne/decoding/csp.py +++ b/mne/decoding/csp.py @@ -12,11 +12,18 @@ import numpy as np from scipy.linalg import eigh -from ..cov import _regularized_covariance +from .._fiff.meas_info import create_info +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, copy_doc, fill_doc +from ..utils import ( + _check_option, + _validate_type, + _verbose_safe_false, + copy_doc, + fill_doc, +) from .base import BaseEstimator from .mixin import TransformerMixin @@ -185,6 +192,9 @@ def fit(self, X, y): f"{n_classes} classes; use component_order='mutual_info' instead." ) + # Convert rank to one that will run + _validate_type(self.rank, (dict, None), "rank") + covs, sample_weights = self._compute_covariance_matrices(X, y) eigen_vectors, eigen_values = self._decompose_covs(covs, sample_weights) ix = self._order_components( @@ -519,10 +529,28 @@ def _compute_covariance_matrices(self, X, y): elif self.cov_est == "epoch": cov_estimator = self._epoch_cov + # Someday we could allow the user to pass this, then we wouldn't need to convert + # but in the meantime they can use a pipeline with a scaler + self._info = create_info(n_channels, 1000.0, "mag") + if self.rank is None: + self._rank = _compute_rank_raw_array( + X.transpose(1, 0, 2).reshape(X.shape[1], -1), + self._info, + rank=None, + scalings=None, + log_ch_type="data", + ) + else: + self._rank = {"mag": sum(self.rank.values())} + covs = [] sample_weights = [] - for this_class in self._classes: - cov, weight = cov_estimator(X[y == this_class]) + for ci, this_class in enumerate(self._classes): + cov, weight = cov_estimator( + X[y == this_class], + cov_kind=f"class={this_class}", + log_rank=ci == 0, + ) if self.norm_trace: cov /= np.trace(cov) @@ -532,29 +560,39 @@ def _compute_covariance_matrices(self, X, y): return np.stack(covs), np.array(sample_weights) - def _concat_cov(self, x_class): + def _concat_cov(self, x_class, *, cov_kind, log_rank): """Concatenate epochs before computing the covariance.""" _, n_channels, _ = x_class.shape - x_class = np.transpose(x_class, [1, 0, 2]) - x_class = x_class.reshape(n_channels, -1) + x_class = x_class.transpose(1, 0, 2).reshape(n_channels, -1) cov = _regularized_covariance( - x_class, reg=self.reg, method_params=self.cov_method_params, rank=self.rank + x_class, + reg=self.reg, + method_params=self.cov_method_params, + rank=self._rank, + info=self._info, + cov_kind=cov_kind, + log_rank=log_rank, + log_ch_type="data", ) weight = x_class.shape[0] return cov, weight - def _epoch_cov(self, x_class): + def _epoch_cov(self, x_class, *, cov_kind, log_rank): """Mean of per-epoch covariances.""" cov = sum( _regularized_covariance( this_X, reg=self.reg, method_params=self.cov_method_params, - rank=self.rank, + rank=self._rank, + info=self._info, + cov_kind=cov_kind, + log_rank=log_rank and ii == 0, + log_ch_type="data", ) - for this_X in x_class + for ii, this_X in enumerate(x_class) ) cov /= len(x_class) weight = len(x_class) @@ -563,6 +601,20 @@ def _epoch_cov(self, x_class): def _decompose_covs(self, covs, sample_weights): n_classes = len(covs) + n_channels = covs[0].shape[0] + assert self._rank is not None # should happen in _compute_covariance_matrices + _, sub_vec, mask = _smart_eigh( + covs.mean(0), + self._info, + self._rank, + proj_subspace=True, + do_compute_rank=False, + log_ch_type="data", + verbose=_verbose_safe_false(), + ) + sub_vec = sub_vec[mask] + covs = np.array([sub_vec @ cov @ sub_vec.T for cov in covs], float) + assert covs[0].shape == (mask.sum(),) * 2 if n_classes == 2: eigen_values, eigen_vectors = eigh(covs[0], covs.sum(0)) else: @@ -573,6 +625,9 @@ def _decompose_covs(self, covs, sample_weights): eigen_vectors.T, covs, sample_weights ) eigen_values = None + # project back + eigen_vectors = sub_vec.T @ eigen_vectors + assert eigen_vectors.shape == (n_channels, mask.sum()) return eigen_vectors, eigen_values def _compute_mutual_info(self, covs, sample_weights, eigen_vectors): @@ -824,6 +879,8 @@ def fit(self, X, y): reg=self.reg, method_params=self.cov_method_params, rank=self.rank, + log_ch_type="data", + log_rank=ii == 0, ) C = covs.mean(0) diff --git a/mne/decoding/tests/test_csp.py b/mne/decoding/tests/test_csp.py index e632a02e2a7..1e8d138f83b 100644 --- a/mne/decoding/tests/test_csp.py +++ b/mne/decoding/tests/test_csp.py @@ -13,12 +13,14 @@ from numpy.testing import assert_array_almost_equal, assert_array_equal, assert_equal from mne import Epochs, io, pick_types, read_events -from mne.decoding.csp import CSP, SPoC, _ajd_pham +from mne.decoding import CSP, Scaler, SPoC +from mne.decoding.csp import _ajd_pham +from mne.utils import catch_logging data_dir = Path(__file__).parents[2] / "io" / "tests" / "data" raw_fname = data_dir / "test_raw.fif" event_name = data_dir / "test-eve.fif" -tmin, tmax = -0.2, 0.5 +tmin, tmax = -0.1, 0.2 event_id = dict(aud_l=1, vis_l=3) # if stop is too small pca may fail in some cases, but we're okay on this file start, stop = 0, 8 @@ -245,40 +247,95 @@ def test_csp(): assert np.abs(corr) > 0.95 -def test_regularized_csp(): +# Even the "reg is None and rank is None" case should pass now thanks to the +# do_compute_rank +@pytest.mark.parametrize("ch_type", ("mag", "eeg", ("mag", "eeg"))) +@pytest.mark.parametrize("rank", (None, "correct")) +@pytest.mark.parametrize("reg", [None, 0.001, "oas"]) +def test_regularized_csp(ch_type, rank, reg): """Test Common Spatial Patterns algorithm using regularized covariance.""" pytest.importorskip("sklearn") - raw = io.read_raw_fif(raw_fname) + from sklearn.linear_model import LogisticRegression + from sklearn.model_selection import StratifiedKFold, cross_val_score + from sklearn.pipeline import make_pipeline + + raw = io.read_raw_fif(raw_fname).pick(ch_type, exclude="bads").load_data() + n_orig = len(raw.ch_names) + ch_decim = 2 + raw.pick_channels(raw.ch_names[::ch_decim]) + if "eeg" in ch_type: + raw.set_eeg_reference(projection=True) + n_eig = len(raw.ch_names) - len(raw.info["projs"]) + n_ch = n_orig // ch_decim + if ch_type == "eeg": + assert n_eig == n_ch - 1 + elif ch_type == "mag": + assert n_eig == n_ch - 3 + else: + assert n_eig == n_ch - 4 + if rank == "correct": + if isinstance(ch_type, str): + rank = {ch_type: n_eig} + else: + assert ch_type == ("mag", "eeg") + rank = dict( + mag=102 // ch_decim - 3, + eeg=60 // ch_decim - 1, + ) + else: + assert rank is None, rank + raw.info.normalize_proj() + raw.filter(2, 40) events = read_events(event_name) - picks = pick_types( - raw.info, meg=True, stim=False, ecg=False, eog=False, exclude="bads" - ) - picks = picks[1:13:3] - epochs = Epochs( - raw, events, event_id, tmin, tmax, picks=picks, baseline=(None, 0), preload=True - ) + # map make left and right events the same + events[events[:, 2] == 2, 2] = 1 + events[events[:, 2] == 4, 2] = 3 + epochs = Epochs(raw, events, event_id, tmin, tmax, decim=5, preload=True) + epochs.equalize_event_counts() + assert 25 < len(epochs) < 30 epochs_data = epochs.get_data(copy=False) n_channels = epochs_data.shape[1] - + assert n_channels == n_ch n_components = 3 - reg_cov = [None, 0.05, "ledoit_wolf", "oas"] - for reg in reg_cov: - csp = CSP(n_components=n_components, reg=reg, norm_trace=False, rank=None) - csp.fit(epochs_data, epochs.events[:, -1]) - y = epochs.events[:, -1] - X = csp.fit_transform(epochs_data, y) - assert csp.filters_.shape == (n_channels, n_channels) - assert csp.patterns_.shape == (n_channels, n_channels) - assert_array_almost_equal(csp.fit(epochs_data, y).transform(epochs_data), X) - - # test init exception - pytest.raises(ValueError, csp.fit, epochs_data, np.zeros_like(epochs.events)) - pytest.raises(ValueError, csp.fit, epochs, y) - pytest.raises(ValueError, csp.transform, epochs) - - csp.n_components = n_components - sources = csp.transform(epochs_data) - assert sources.shape[1] == n_components + + sc = Scaler(epochs.info) + 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: + X = csp.fit_transform(epochs_data, epochs.events[:, -1]) + log = log.getvalue() + assert "Setting small MAG" not in log + assert "Setting small data eigen" in log + if rank is None: + assert "Computing rank from data" in log + assert " mag: rank" not in log.lower() + assert " data: rank" in log + assert "rank (mag)" not in log.lower() + assert "rank (data)" in log + else: # if rank is passed no computation is done + assert "Computing rank" not in log + assert ": rank" not in log + assert "rank (" not in log + assert "reducing mag" not in log.lower() + assert f"Reducing data rank from {n_channels} " in log + y = epochs.events[:, -1] + assert csp.filters_.shape == (n_eig, n_channels) + assert csp.patterns_.shape == (n_eig, n_channels) + assert_array_almost_equal(csp.fit(epochs_data, y).transform(epochs_data), X) + + # test init exception + pytest.raises(ValueError, csp.fit, epochs_data, np.zeros_like(epochs.events)) + pytest.raises(ValueError, csp.fit, epochs, y) + pytest.raises(ValueError, csp.transform, epochs) + + csp.n_components = n_components + sources = csp.transform(epochs_data) + 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() + assert 0.75 <= score <= 1.0 def test_csp_pipeline(): diff --git a/mne/rank.py b/mne/rank.py index ae5b6057e56..a176a1f5431 100644 --- a/mne/rank.py +++ b/mne/rank.py @@ -139,7 +139,13 @@ def _estimate_rank_raw( @fill_doc def _estimate_rank_meeg_signals( - data, info, scalings, tol="auto", return_singular=False, tol_kind="absolute" + data, + info, + scalings, + tol="auto", + return_singular=False, + tol_kind="absolute", + log_ch_type=None, ): """Estimate rank for M/EEG data. @@ -187,14 +193,24 @@ def _estimate_rank_meeg_signals( tol_kind=tol_kind, ) rank = out[0] if isinstance(out, tuple) else out - ch_type = " + ".join(list(zip(*picks_list))[0]) + if log_ch_type is None: + ch_type = " + ".join(list(zip(*picks_list))[0]) + else: + ch_type = log_ch_type logger.info(" Estimated rank (%s): %d" % (ch_type, rank)) return out @verbose def _estimate_rank_meeg_cov( - data, info, scalings, tol="auto", return_singular=False, verbose=None + data, + info, + scalings, + tol="auto", + return_singular=False, + *, + log_ch_type=None, + verbose=None, ): """Estimate rank of M/EEG covariance data, given the covariance. @@ -235,8 +251,11 @@ def _estimate_rank_meeg_cov( ) out = estimate_rank(data, tol=tol, norm=False, return_singular=return_singular) rank = out[0] if isinstance(out, tuple) else out - ch_type = " + ".join(list(zip(*picks_list))[0]) - logger.info(" Estimated rank (%s): %d" % (ch_type, rank)) + if log_ch_type is None: + ch_type_ = " + ".join(list(zip(*picks_list))[0]) + else: + ch_type_ = log_ch_type + logger.info(f" Estimated rank ({ch_type_}): {rank}") _undo_scaling_cov(data, picks_list, scalings) return out @@ -352,6 +371,32 @@ def compute_rank( ----- .. versionadded:: 0.18 """ + return _compute_rank( + inst=inst, + rank=rank, + scalings=scalings, + info=info, + tol=tol, + proj=proj, + tol_kind=tol_kind, + on_rank_mismatch=on_rank_mismatch, + ) + + +@verbose +def _compute_rank( + inst, + rank=None, + scalings=None, + info=None, + *, + tol="auto", + proj=True, + tol_kind="absolute", + on_rank_mismatch="ignore", + log_ch_type=None, + verbose=None, +): from .cov import Covariance from .epochs import BaseEpochs from .io import BaseRaw @@ -417,25 +462,22 @@ def compute_rank( proj_op, n_proj, _ = make_projector(info["projs"], ch_names) else: proj_op, n_proj = None, 0 + if log_ch_type is None: + ch_type_ = ch_type.upper() + else: + ch_type_ = log_ch_type if rank_type == "info": # use info this_rank = _info_rank(info, ch_type, picks, info_type) if info_type != "full": this_rank -= n_proj logger.info( - " %s: rank %d after %d projector%s applied to " - "%d channel%s" - % ( - ch_type.upper(), - this_rank, - n_proj, - _pl(n_proj), - n_chan, - _pl(n_chan), - ) + f" {ch_type_}: rank {this_rank} after " + f"{n_proj} projector{_pl(n_proj)} applied to " + "{n_chan} channel{_pl(n_chan)}" ) else: - logger.info(" %s: rank %d from info" % (ch_type.upper(), this_rank)) + logger.info(f" {ch_type_}: rank {this_rank} from info") else: # Use empirical estimation assert rank_type == "estimated" @@ -447,7 +489,13 @@ def compute_rank( if proj: data = np.dot(proj_op, data) this_rank = _estimate_rank_meeg_signals( - data, pick_info(simple_info, picks), scalings, tol, False, tol_kind + data, + pick_info(simple_info, picks), + scalings, + tol, + False, + tol_kind, + log_ch_type=log_ch_type, ) else: assert isinstance(inst, Covariance) @@ -464,6 +512,7 @@ def compute_rank( scalings, tol, return_singular=True, + log_ch_type=log_ch_type, verbose=est_verbose, ) if ch_type in rank: @@ -483,9 +532,9 @@ def compute_rank( continue this_info_rank = _info_rank(info, ch_type, picks, "info") logger.info( - " %s: rank %d computed from %d data channel%s " - "with %d projector%s" - % (ch_type.upper(), this_rank, n_chan, _pl(n_chan), n_proj, _pl(n_proj)) + f" {ch_type_}: rank {this_rank} computed from " + f"{n_chan} data channel{_pl(n_chan)} with " + f"{n_proj} projector{_pl(n_proj)}" ) if this_rank > this_info_rank: warn( From 6368a0b90224181139a845db4dc74beb648e9960 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Tue, 16 Apr 2024 16:32:18 -0400 Subject: [PATCH 269/405] CI: Build docs, too [circle full] From b95fc4a9151f633d40074e7e43cb6f6487275a33 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Wed, 17 Apr 2024 02:19:30 -0400 Subject: [PATCH 270/405] BUG: Fix bug with volume (#12544) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- doc/changes/devel/12544.bugfix.rst | 1 + mne/source_space/_source_space.py | 5 +++-- mne/source_space/tests/test_source_space.py | 21 +++++++++++++++++++++ 3 files changed, 25 insertions(+), 2 deletions(-) create mode 100644 doc/changes/devel/12544.bugfix.rst diff --git a/doc/changes/devel/12544.bugfix.rst b/doc/changes/devel/12544.bugfix.rst new file mode 100644 index 00000000000..d6e3210ec45 --- /dev/null +++ b/doc/changes/devel/12544.bugfix.rst @@ -0,0 +1 @@ +Fix bug with :func:`mne.SourceSpaces.export_volume` where the ``img.affine`` was not set properly, by `Eric Larson`_. \ No newline at end of file diff --git a/mne/source_space/_source_space.py b/mne/source_space/_source_space.py index 471c4182afa..7f2910cbaad 100644 --- a/mne/source_space/_source_space.py +++ b/mne/source_space/_source_space.py @@ -668,10 +668,11 @@ def export_volume( # Figure out how to get from our input source space to output voxels fro_dst_t = invert_transform(transform) - dest = transform["to"] if coords == "head": head_mri_t = _get_trans(trans, "head", "mri")[0] - fro_dst_t = combine_transforms(head_mri_t, fro_dst_t, "head", dest) + fro_dst_t = combine_transforms( + head_mri_t, fro_dst_t, "head", transform["to"] + ) else: fro_dst_t = fro_dst_t diff --git a/mne/source_space/tests/test_source_space.py b/mne/source_space/tests/test_source_space.py index 14e5242ffe2..628428fd84e 100644 --- a/mne/source_space/tests/test_source_space.py +++ b/mne/source_space/tests/test_source_space.py @@ -874,6 +874,27 @@ def test_combine_source_spaces(tmp_path): with pytest.warns(RuntimeWarning, match="2 surf vertices lay outside"): src.export_volume(image_fname, mri_resolution="sparse", overwrite=True) + # gh-12495 + image_fname = tmp_path / "temp-image.nii" + lh_cereb = mne.setup_volume_source_space( + "sample", + mri=aseg_fname, + volume_label="Left-Cerebellum-Cortex", + add_interpolator=False, + subjects_dir=subjects_dir, + ) + lh_cereb.export_volume(image_fname, mri_resolution=True) + aseg = nib.load(str(aseg_fname)) + out = nib.load(str(image_fname)) + assert_allclose(out.affine, aseg.affine) + src_data = _get_img_fdata(out).astype(bool) + aseg_data = _get_img_fdata(aseg) == 8 + n_src = src_data.sum() + n_aseg = aseg_data.sum() + assert n_aseg == n_src + n_overlap = (src_data & aseg_data).sum() + assert n_src == n_overlap + @testing.requires_testing_data def test_morph_source_spaces(): From 4767ff5188d3b5c471ad2b420366989ceb4b6159 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Wed, 17 Apr 2024 08:53:34 -0400 Subject: [PATCH 271/405] MAINT: Fix example [circle full] [skip azp] [skip actions] --- tutorials/clinical/60_sleep.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/tutorials/clinical/60_sleep.py b/tutorials/clinical/60_sleep.py index 17e7a69ddbf..b25776d7435 100644 --- a/tutorials/clinical/60_sleep.py +++ b/tutorials/clinical/60_sleep.py @@ -75,7 +75,11 @@ [alice_files, bob_files] = fetch_data(subjects=[ALICE, BOB], recording=[1]) raw_train = mne.io.read_raw_edf( - alice_files[0], stim_channel="Event marker", infer_types=True, preload=True + alice_files[0], + stim_channel="Event marker", + infer_types=True, + preload=True, + verbose="error", # ignore issues with stored filter settings ) annot_train = mne.read_annotations(alice_files[1]) @@ -172,7 +176,11 @@ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ raw_test = mne.io.read_raw_edf( - bob_files[0], stim_channel="Event marker", infer_types=True, preload=True + bob_files[0], + stim_channel="Event marker", + infer_types=True, + preload=True, + verbose="error", ) annot_test = mne.read_annotations(bob_files[1]) annot_test.crop(annot_test[1]["onset"] - 30 * 60, annot_test[-2]["onset"] + 30 * 60) From 6b5a59d2cb07177a0735b3b9a2701345cfc1d98b Mon Sep 17 00:00:00 2001 From: Clemens Brunner Date: Wed, 17 Apr 2024 20:49:39 +0200 Subject: [PATCH 272/405] Show bad channels in gray (#12548) Co-authored-by: Eric Larson --- doc/conf.py | 6 +++++- mne/viz/raw.py | 2 +- mne/viz/tests/test_raw.py | 7 +++++++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index b4d0ca36f16..ae7ab9677fd 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -774,6 +774,7 @@ def append_attr_meth_examples(app, what, name, obj, options, lines): # -- Nitpicky ---------------------------------------------------------------- nitpicky = True +show_warning_types = True nitpick_ignore = [ ("py:class", "None. Remove all items from D."), ("py:class", "a set-like object providing a view on D's items"), @@ -803,7 +804,10 @@ def append_attr_meth_examples(app, what, name, obj, options, lines): "(filename|metadata|proj|times|tmax|tmin|annotations|ch_names|compensation_grade|filenames|first_samp|first_time|last_samp|n_times|proj|times|tmax|tmin)", ), # noqa: E501 ] -suppress_warnings = ["image.nonlocal_uri"] # we intentionally link outside +suppress_warnings = [ + "image.nonlocal_uri", # we intentionally link outside + "config.cache", # our rebuild is okay +] # -- Sphinx hacks / overrides ------------------------------------------------ diff --git a/mne/viz/raw.py b/mne/viz/raw.py index b54222c807c..dd90352d0cc 100644 --- a/mne/viz/raw.py +++ b/mne/viz/raw.py @@ -313,7 +313,7 @@ def plot_raw( ch_names = np.array(raw.ch_names) ch_types = np.array(raw.get_channel_types()) - picks = _picks_to_idx(info, picks, none="all") + picks = _picks_to_idx(info, picks, none="all", exclude=()) order = _get_channel_plotting_order(order, ch_types, picks=picks) n_channels = min(info["nchan"], n_channels, len(order)) # adjust order based on channel selection, if needed diff --git a/mne/viz/tests/test_raw.py b/mne/viz/tests/test_raw.py index 441ed79c3f3..031f3d34392 100644 --- a/mne/viz/tests/test_raw.py +++ b/mne/viz/tests/test_raw.py @@ -541,6 +541,7 @@ def test_plot_raw_traces(raw, events, browser_backend): ismpl = browser_backend.name == "matplotlib" with raw.info._unlock(): raw.info["lowpass"] = 10.0 # allow heavy decim during plotting + assert raw.info["bads"] == [] fig = raw.plot( events=events, order=[1, 7, 5, 2, 3], n_channels=3, group_by="original" ) @@ -623,6 +624,12 @@ def test_plot_raw_traces(raw, events, browser_backend): raw.plot(event_color={"foo": "r"}) plot_raw(raw, events=events, event_color={-1: "r", 998: "b"}) + # gh-12547 + raw.info["bads"] = raw.ch_names[1:2] + picks = [1, 7, 5, 2, 3] + fig = raw.plot(events=events, order=picks, group_by="original") + assert_array_equal(fig.mne.picks, picks) + def test_plot_raw_picks(raw, browser_backend): """Test functionality of picks and order arguments.""" From 321825b5e13f771e5ce31d0383609468d0a1d5e8 Mon Sep 17 00:00:00 2001 From: Mathieu Scheltienne Date: Thu, 18 Apr 2024 09:24:50 +0200 Subject: [PATCH 273/405] fix command in example comment (#12545) --- examples/forward/left_cerebellum_volume_source.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/forward/left_cerebellum_volume_source.py b/examples/forward/left_cerebellum_volume_source.py index 22e46073d88..ff810493e99 100644 --- a/examples/forward/left_cerebellum_volume_source.py +++ b/examples/forward/left_cerebellum_volume_source.py @@ -73,8 +73,8 @@ # And display source positions in freeview:: # # >>> from mne.utils import run_subprocess -# >>> mri_fname = subjects_dir + '/sample/mri/brain.mgz' -# >>> run_subprocess(['freeview', '-v', mri_fname, '-v', -# '%s:colormap=lut:opacity=0.5' % aseg_fname, '-v', -# '%s:colormap=jet:colorscale=0,2' % nii_fname, -# '-slice', '157 75 105']) +# >>> mri_fname = subjects_dir / "sample" / "mri" / "brain.mgz" +# >>> run_subprocess(["freeview", "-v", str(mri_fname), "-v", +# f"{aseg_fname}:colormap=lut:opacity=0.5", +# "-v", f"{nii_fname}:colormap=jet:colorscale=0,2", +# "--slice", "157", "75", "105"]) From 27c07a8e2c8e2add8c8921c144986a9530455eb1 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Thu, 18 Apr 2024 13:42:17 -0400 Subject: [PATCH 274/405] MAINT: Pin Sphinx (#12552) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 6697cbc5144..5a2dbce91a6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -142,7 +142,7 @@ test_extra = [ # Dependencies for building the documentation doc = [ - "sphinx>=6", + "sphinx>=6,<7.3", "numpydoc", "pydata_sphinx_theme==0.15.2", "sphinx-gallery", From 39a4ddb81cb6e7218f770d34f0ae93e5987258bb Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Fri, 19 Apr 2024 13:01:34 -0400 Subject: [PATCH 275/405] DOC: Changes for 1.7.0 release --- .mailmap | 2 + CITATION.cff | 99 +++--- SECURITY.md | 6 +- codemeta.json | 290 +++++++++++------- doc/_static/versions.json | 9 +- doc/changes/devel.rst | 5 - doc/changes/devel/11234.newfeature.rst | 1 - doc/changes/devel/11282.apichange.rst | 1 - doc/changes/devel/11282.bugfix.rst | 1 - doc/changes/devel/11282.newfeature.rst | 1 - doc/changes/devel/12190.bugfix.rst | 1 - doc/changes/devel/12195.newfeature.rst | 1 - doc/changes/devel/12206.bugfix.rst | 1 - doc/changes/devel/12206.newfeature.rst | 3 - doc/changes/devel/12207.newfeature.rst | 1 - doc/changes/devel/12218.newfeature.rst | 1 - doc/changes/devel/12236.bugfix.rst | 1 - doc/changes/devel/12237.newfeature.rst | 2 - doc/changes/devel/12238.newfeature.rst | 1 - doc/changes/devel/12248.bugfix.rst | 1 - doc/changes/devel/12250.newfeature.rst | 1 - doc/changes/devel/12250.notable.rst | 11 - doc/changes/devel/12264.dependency.rst | 1 - doc/changes/devel/12268.newfeature.rst | 1 - doc/changes/devel/12269.newfeature.rst | 1 - doc/changes/devel/12279.bugfix.rst | 1 - doc/changes/devel/12282.bugfix.rst | 1 - doc/changes/devel/12289.newfeature.rst | 1 - doc/changes/devel/12299.other.rst | 1 - doc/changes/devel/12308.apichange.rst | 1 - doc/changes/devel/12309.newfeature.rst | 1 - doc/changes/devel/12311.newfeature.rst | 1 - doc/changes/devel/12318.other.rst | 1 - doc/changes/devel/12319.bugfix.rst | 1 - doc/changes/devel/12323.newfeature.rst | 1 - doc/changes/devel/12324.bugfix.rst | 1 - doc/changes/devel/12326.other.rst | 1 - doc/changes/devel/12332.newfeature.rst | 1 - doc/changes/devel/12336.bugfix.rst | 1 - doc/changes/devel/12343.newfeature.rst | 1 - doc/changes/devel/12345.bugfix.rst | 1 - doc/changes/devel/12348.bugfix.rst | 1 - doc/changes/devel/12354.bugfix.rst | 1 - doc/changes/devel/12357.bugfix.rst | 1 - doc/changes/devel/12358.other.rst | 1 - doc/changes/devel/12371.newfeature.rst | 1 - doc/changes/devel/12376.dependency.rst | 1 - doc/changes/devel/12380.bugfix.rst | 1 - doc/changes/devel/12382.apichange.rst | 1 - doc/changes/devel/12382.bugfix.rst | 1 - doc/changes/devel/12383.newfeature.rst | 1 - doc/changes/devel/12389.bugfix.rst | 1 - doc/changes/devel/12393.bugfix.rst | 1 - doc/changes/devel/12394.newfeature.rst | 1 - doc/changes/devel/12399.bugfix.rst | 1 - doc/changes/devel/12410.bugfix.rst | 1 - doc/changes/devel/12420.other.rst | 1 - doc/changes/devel/12430.bugfix.rst | 1 - doc/changes/devel/12436.bugfix.rst | 1 - doc/changes/devel/12441.bugfix.rst | 1 - doc/changes/devel/12443.newfeature.rst | 1 - doc/changes/devel/12444.bugfix.rst | 1 - doc/changes/devel/12445.newfeature.rst | 1 - doc/changes/devel/12446.newfeature.rst | 1 - doc/changes/devel/12450.other.rst | 1 - doc/changes/devel/12451.bugfix.rst | 1 - doc/changes/devel/12451.dependency.rst | 1 - doc/changes/devel/12454.newfeature.rst | 1 - doc/changes/devel/12456.bugfix.rst | 1 - doc/changes/devel/12461.other.rst | 1 - doc/changes/devel/12462.newfeature.rst | 1 - doc/changes/devel/12463.newfeature.rst | 1 - doc/changes/devel/12464.other.rst | 2 - doc/changes/devel/12467.newfeature.rst | 1 - doc/changes/devel/12470.bugfix.rst | 1 - doc/changes/devel/12474.bugfix.rst | 1 - doc/changes/devel/12476.bugfix.rst | 1 - doc/changes/devel/12481.bugfix.rst | 1 - doc/changes/devel/12483.bugfix.rst | 1 - doc/changes/devel/12484.bugfix.rst | 1 - doc/changes/devel/12489.bugfix.rst | 1 - doc/changes/devel/12491.dependency.rst | 1 - doc/changes/devel/12498.bugfix.rst | 2 - doc/changes/devel/12507.bugfix.rst | 5 - doc/changes/devel/12509.other.rst | 2 - doc/changes/devel/12510.newfeature.rst | 1 - doc/changes/devel/12513.newfeature.rst | 2 - doc/changes/devel/12518.newfeature.rst | 1 - doc/changes/devel/12523.bugfix.rst | 1 - doc/changes/devel/12526.bugfix.rst | 1 - doc/changes/devel/12535.bugfix.rst | 1 - doc/changes/devel/12536.bugfix.rst | 1 - doc/changes/devel/12537.bugfix.rst | 1 - doc/changes/devel/12544.bugfix.rst | 1 - doc/changes/v0.24.rst | 2 +- doc/changes/v1.7.rst | 180 +++++++++++ doc/development/whats_new.rst | 2 +- doc/documentation/cited.rst | 6 +- examples/decoding/decoding_spoc_CMC.py | 2 +- mne/commands/mne_browse_raw.py | 2 +- .../sleep_physionet/tests/test_physionet.py | 4 +- mne/fixes.py | 2 +- mne/io/fiff/tests/test_raw_fiff.py | 2 +- mne/io/kit/coreg.py | 2 +- mne/preprocessing/_peak_finder.py | 2 +- tools/generate_codemeta.py | 1 + 106 files changed, 443 insertions(+), 284 deletions(-) delete mode 100644 doc/changes/devel.rst delete mode 100644 doc/changes/devel/11234.newfeature.rst delete mode 100644 doc/changes/devel/11282.apichange.rst delete mode 100644 doc/changes/devel/11282.bugfix.rst delete mode 100644 doc/changes/devel/11282.newfeature.rst delete mode 100644 doc/changes/devel/12190.bugfix.rst delete mode 100644 doc/changes/devel/12195.newfeature.rst delete mode 100644 doc/changes/devel/12206.bugfix.rst delete mode 100644 doc/changes/devel/12206.newfeature.rst delete mode 100644 doc/changes/devel/12207.newfeature.rst delete mode 100644 doc/changes/devel/12218.newfeature.rst delete mode 100644 doc/changes/devel/12236.bugfix.rst delete mode 100644 doc/changes/devel/12237.newfeature.rst delete mode 100644 doc/changes/devel/12238.newfeature.rst delete mode 100644 doc/changes/devel/12248.bugfix.rst delete mode 100644 doc/changes/devel/12250.newfeature.rst delete mode 100644 doc/changes/devel/12250.notable.rst delete mode 100644 doc/changes/devel/12264.dependency.rst delete mode 100644 doc/changes/devel/12268.newfeature.rst delete mode 100644 doc/changes/devel/12269.newfeature.rst delete mode 100644 doc/changes/devel/12279.bugfix.rst delete mode 100644 doc/changes/devel/12282.bugfix.rst delete mode 100644 doc/changes/devel/12289.newfeature.rst delete mode 100644 doc/changes/devel/12299.other.rst delete mode 100644 doc/changes/devel/12308.apichange.rst delete mode 100644 doc/changes/devel/12309.newfeature.rst delete mode 100644 doc/changes/devel/12311.newfeature.rst delete mode 100644 doc/changes/devel/12318.other.rst delete mode 100644 doc/changes/devel/12319.bugfix.rst delete mode 100644 doc/changes/devel/12323.newfeature.rst delete mode 100644 doc/changes/devel/12324.bugfix.rst delete mode 100644 doc/changes/devel/12326.other.rst delete mode 100644 doc/changes/devel/12332.newfeature.rst delete mode 100644 doc/changes/devel/12336.bugfix.rst delete mode 100644 doc/changes/devel/12343.newfeature.rst delete mode 100644 doc/changes/devel/12345.bugfix.rst delete mode 100644 doc/changes/devel/12348.bugfix.rst delete mode 100644 doc/changes/devel/12354.bugfix.rst delete mode 100644 doc/changes/devel/12357.bugfix.rst delete mode 100644 doc/changes/devel/12358.other.rst delete mode 100644 doc/changes/devel/12371.newfeature.rst delete mode 100644 doc/changes/devel/12376.dependency.rst delete mode 100644 doc/changes/devel/12380.bugfix.rst delete mode 100644 doc/changes/devel/12382.apichange.rst delete mode 100644 doc/changes/devel/12382.bugfix.rst delete mode 100644 doc/changes/devel/12383.newfeature.rst delete mode 100644 doc/changes/devel/12389.bugfix.rst delete mode 100644 doc/changes/devel/12393.bugfix.rst delete mode 100644 doc/changes/devel/12394.newfeature.rst delete mode 100644 doc/changes/devel/12399.bugfix.rst delete mode 100644 doc/changes/devel/12410.bugfix.rst delete mode 100644 doc/changes/devel/12420.other.rst delete mode 100644 doc/changes/devel/12430.bugfix.rst delete mode 100644 doc/changes/devel/12436.bugfix.rst delete mode 100644 doc/changes/devel/12441.bugfix.rst delete mode 100644 doc/changes/devel/12443.newfeature.rst delete mode 100644 doc/changes/devel/12444.bugfix.rst delete mode 100644 doc/changes/devel/12445.newfeature.rst delete mode 100644 doc/changes/devel/12446.newfeature.rst delete mode 100644 doc/changes/devel/12450.other.rst delete mode 100644 doc/changes/devel/12451.bugfix.rst delete mode 100644 doc/changes/devel/12451.dependency.rst delete mode 100644 doc/changes/devel/12454.newfeature.rst delete mode 100644 doc/changes/devel/12456.bugfix.rst delete mode 100644 doc/changes/devel/12461.other.rst delete mode 100644 doc/changes/devel/12462.newfeature.rst delete mode 100644 doc/changes/devel/12463.newfeature.rst delete mode 100644 doc/changes/devel/12464.other.rst delete mode 100644 doc/changes/devel/12467.newfeature.rst delete mode 100644 doc/changes/devel/12470.bugfix.rst delete mode 100644 doc/changes/devel/12474.bugfix.rst delete mode 100644 doc/changes/devel/12476.bugfix.rst delete mode 100644 doc/changes/devel/12481.bugfix.rst delete mode 100644 doc/changes/devel/12483.bugfix.rst delete mode 100644 doc/changes/devel/12484.bugfix.rst delete mode 100644 doc/changes/devel/12489.bugfix.rst delete mode 100644 doc/changes/devel/12491.dependency.rst delete mode 100644 doc/changes/devel/12498.bugfix.rst delete mode 100644 doc/changes/devel/12507.bugfix.rst delete mode 100644 doc/changes/devel/12509.other.rst delete mode 100644 doc/changes/devel/12510.newfeature.rst delete mode 100644 doc/changes/devel/12513.newfeature.rst delete mode 100644 doc/changes/devel/12518.newfeature.rst delete mode 100644 doc/changes/devel/12523.bugfix.rst delete mode 100644 doc/changes/devel/12526.bugfix.rst delete mode 100644 doc/changes/devel/12535.bugfix.rst delete mode 100644 doc/changes/devel/12536.bugfix.rst delete mode 100644 doc/changes/devel/12537.bugfix.rst delete mode 100644 doc/changes/devel/12544.bugfix.rst create mode 100644 doc/changes/v1.7.rst diff --git a/.mailmap b/.mailmap index 10afa14ea85..d71df509cc2 100644 --- a/.mailmap +++ b/.mailmap @@ -114,11 +114,13 @@ Giorgio Marinato neurogima <76406896+neurogima@users Guillaume Dumas deep-introspection Guillaume Dumas Guillaume Dumas Hamid Maymandi <46011104+HamidMandi@users.noreply.github.com> Hamid <46011104+HamidMandi@users.noreply.github.com> +Hasrat Ali Arzoo <56307533+hasrat17@users.noreply.github.com> hasrat17 <56307533+hasrat17@users.noreply.github.com> Hongjiang Ye YE Hongjiang Hubert Banville hubertjb Hüseyin Orkun Elmas Hüseyin Hyonyoung Shin <55095699+mcvain@users.noreply.github.com> mcvain <55095699+mcvain@users.noreply.github.com> Ingoo Lee dlsrnsi +Ivo de Jong ivopascal Jaakko Leppakangas Jaakko Leppakangas Jaakko Leppakangas jaeilepp Jaakko Leppakangas jaeilepp diff --git a/CITATION.cff b/CITATION.cff index c1850a2f55b..936f3f90677 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -1,9 +1,9 @@ cff-version: 1.2.0 title: "MNE-Python" message: "If you use this software, please cite both the software itself, and the paper listed in the preferred-citation field." -version: 1.6.0 -date-released: "2023-11-20" -commit: 498cf789685ede0b29e712a1e7220c69443e8744 +version: 1.7.0 +date-released: "2024-04-19" +commit: a3743420a8eef774dafd2908f0de89c4d37fcd01 doi: 10.5281/zenodo.592483 keywords: - MEG @@ -35,10 +35,10 @@ authors: given-names: Teon - family-names: Sassenhagen given-names: Jona - - family-names: Luessi - given-names: Martin - family-names: McCloy given-names: Daniel + - family-names: Luessi + given-names: Martin - family-names: King given-names: Jean-Remi - family-names: Höchenberger @@ -53,10 +53,10 @@ authors: given-names: Marijn - family-names: Wronkiewicz given-names: Mark - - family-names: Holdgraf - given-names: Chris - family-names: Rockhill given-names: Alex + - family-names: Holdgraf + given-names: Chris - family-names: Massich given-names: Joan - family-names: Bekhti @@ -117,12 +117,12 @@ authors: given-names: Martin - family-names: Foti given-names: Nick + - family-names: Huberty + given-names: Scott - family-names: Nangini given-names: Cathy - family-names: García Alanis given-names: José C - - family-names: Huberty - given-names: Scott - family-names: Hauk given-names: Olaf - family-names: Maddox @@ -165,6 +165,10 @@ authors: given-names: Christopher - family-names: Raimundo given-names: Félix + - family-names: Woessner + given-names: Jacob + - family-names: Kaneda + given-names: Michiru - family-names: Alday given-names: Phillip - family-names: Pari @@ -189,6 +193,10 @@ authors: given-names: Alexandre - family-names: Gütlin given-names: Dirk + - family-names: Heinila + given-names: Erkka + - family-names: Armeni + given-names: Kristijan - name: kjs - family-names: Weinstein given-names: Alejandro @@ -202,14 +210,10 @@ authors: given-names: Dmitrii - family-names: Peterson given-names: Erica - - family-names: Heinila - given-names: Erkka - family-names: Hanna given-names: Jevri - family-names: Houck given-names: Jon - - family-names: Kaneda - given-names: Michiru - family-names: Klein given-names: Natalie - family-names: Roujansky @@ -220,16 +224,18 @@ authors: given-names: Antti - family-names: Maess given-names: Burkhard + - family-names: Forster + given-names: Carina - family-names: O'Reilly given-names: Christian + - family-names: Welke + given-names: Dominik - family-names: Kolkhorst given-names: Henrich - family-names: Banville given-names: Hubert - family-names: Zhang given-names: Jack - - family-names: Woessner - given-names: Jacob - family-names: Maksymenko given-names: Kostiantyn - family-names: Clarke @@ -242,8 +248,6 @@ authors: given-names: Pierre-Antoine - family-names: Choudhary given-names: Saket - - family-names: Forster - given-names: Carina - family-names: Kim given-names: Cora - family-names: Klotzsche @@ -268,6 +272,8 @@ authors: given-names: Nick - family-names: Ruuskanen given-names: Santeri + - family-names: Herbst + given-names: Sophie - family-names: Radanovic given-names: Ana - family-names: Quinn @@ -278,8 +284,6 @@ authors: given-names: Basile - family-names: Welke given-names: Dominik - - family-names: Welke - given-names: Dominik - family-names: Stephen given-names: Emily - family-names: Hornberger @@ -294,22 +298,30 @@ authors: given-names: Giorgio - family-names: Anevar given-names: Hafeza + - family-names: Abdelhedi + given-names: Hamza - family-names: Sosulski given-names: Jan - family-names: Stout given-names: Jeff - family-names: Calder-Travis given-names: Joshua + - family-names: Zhu + given-names: Judy D - family-names: Eisenman given-names: Larry - family-names: Esch given-names: Lorenz - family-names: Dovgialo given-names: Marian + - family-names: Alibou + given-names: Nabil - family-names: Barascud given-names: Nicolas - family-names: Legrand given-names: Nicolas + - family-names: Kapralov + given-names: Nikolai - family-names: Falach given-names: Rotem - family-names: Deslauriers-Gauthier @@ -320,6 +332,10 @@ authors: given-names: Steve - family-names: Bierer given-names: Steven + - family-names: Binns + given-names: Thomas Samuel + - family-names: Stenner + given-names: Tristan - family-names: Férat given-names: Victor - family-names: Peterson @@ -350,8 +366,6 @@ authors: given-names: Gennadiy - family-names: O'Neill given-names: George - - family-names: Abdelhedi - given-names: Hamza - family-names: Schiratti given-names: Jean-Baptiste - family-names: Evans @@ -362,16 +376,14 @@ authors: given-names: Jordan - family-names: Teves given-names: Joshua - - family-names: Zhu - given-names: Judy D - - family-names: Armeni - given-names: Kristijan - family-names: Mathewson given-names: Kyle - family-names: Gwilliams given-names: Laura - family-names: Varghese given-names: Lenny + - family-names: Hamilton + given-names: Liberty - family-names: Gemein given-names: Lukas - family-names: Hecker @@ -393,6 +405,8 @@ authors: given-names: Niklas - family-names: Kozynets given-names: Oleh + - family-names: Molfese + given-names: Peter J - family-names: Ablin given-names: Pierre - family-names: Bertrand @@ -407,24 +421,20 @@ authors: given-names: Sena - family-names: Khan given-names: Sheraz - - family-names: Herbst - given-names: Sophie - family-names: Datta given-names: Sumalyo - family-names: Papadopoulo given-names: Theodore + - family-names: Donoghue + given-names: Thomas - family-names: Jochmann given-names: Thomas - - family-names: Binns - given-names: Thomas Samuel - family-names: Merk given-names: Timon - family-names: Flak given-names: Tod - family-names: Dupré la Tour given-names: Tom - - family-names: Stenner - given-names: Tristan - family-names: NessAiver given-names: Tziona - name: akshay0724 @@ -441,6 +451,8 @@ authors: given-names: Adina - family-names: Ciok given-names: Alex + - family-names: Kiefer + given-names: Alexander - family-names: Gilbert given-names: Andy - family-names: Pradhan @@ -515,6 +527,8 @@ authors: given-names: Evgeny - family-names: Zamberlan given-names: Federico + - family-names: Hofer + given-names: Florian - family-names: Pop given-names: Florin - family-names: Weber @@ -530,6 +544,8 @@ authors: given-names: Gonzalo - family-names: Maymandi given-names: Hamid + - family-names: Arzoo + given-names: Hasrat Ali - family-names: Sonntag given-names: Hermann - family-names: Ye @@ -540,10 +556,10 @@ authors: given-names: Hüseyin Orkun - family-names: Machairas given-names: Ilias - - family-names: Skelin - given-names: Ivan - family-names: Zubarev given-names: Ivan + - family-names: de Jong + given-names: Ivo - family-names: Kaczmarzyk given-names: Jakub - family-names: Zerfowski @@ -576,8 +592,6 @@ authors: given-names: Lau Møller - family-names: Barbosa given-names: Leonardo S - - family-names: Hamilton - given-names: Liberty - family-names: Alfine given-names: Lorenzo - family-names: Hejtmánek @@ -596,6 +610,8 @@ authors: given-names: Marcin - family-names: Henney given-names: Mark Alexander + - family-names: Oberg + given-names: Martin - family-names: Schulz given-names: Martin - family-names: van Harmelen @@ -639,8 +655,6 @@ authors: given-names: Padma - family-names: Silva given-names: Pedro - - family-names: Molfese - given-names: Peter J - family-names: Das given-names: Proloy - family-names: Chu @@ -661,6 +675,8 @@ authors: given-names: Reza - family-names: Koehler given-names: Richard + - family-names: Scholz + given-names: Richard - family-names: Stargardsky given-names: Riessarius - family-names: Oostenveld @@ -691,6 +707,8 @@ authors: given-names: Senwen - family-names: Antopolskiy given-names: Sergey + - family-names: Shirazi + given-names: Seyed (Yahya) - family-names: Wong given-names: Simeon - family-names: Wong @@ -711,8 +729,6 @@ authors: given-names: Svea Marie - family-names: Wang given-names: T - - family-names: Donoghue - given-names: Thomas - family-names: Moreau given-names: Thomas - family-names: Radman @@ -727,12 +743,17 @@ authors: given-names: Tommy - family-names: Anijärv given-names: Toomas Erik + - family-names: Kumaravel + given-names: Velu Prabhakar + - family-names: Turner + given-names: Will - family-names: Xia given-names: Xiaokai - family-names: Zuo given-names: Yiping - family-names: Zhang given-names: Zhi + - name: btkcodedev - name: buildqa - name: luzpaz preferred-citation: diff --git a/SECURITY.md b/SECURITY.md index e627242d244..82d4c9e45de 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -10,9 +10,9 @@ without a proper 6-month deprecation cycle. | Version | Supported | | ------- | ------------------------ | -| 1.7.x | :heavy_check_mark: (dev) | -| 1.6.x | :heavy_check_mark: | -| < 1.6 | :x: | +| 1.8.x | :heavy_check_mark: (dev) | +| 1.7.x | :heavy_check_mark: | +| < 1.7 | :x: | ## Reporting a Vulnerability diff --git a/codemeta.json b/codemeta.json index b2922b2194d..ebfe798c648 100644 --- a/codemeta.json +++ b/codemeta.json @@ -5,11 +5,11 @@ "codeRepository": "git+https://github.com/mne-tools/mne-python.git", "dateCreated": "2010-12-26", "datePublished": "2014-08-04", - "dateModified": "2023-11-20", - "downloadUrl": "https://github.com/mne-tools/mne-python/archive/v1.6.0.zip", + "dateModified": "2024-04-19", + "downloadUrl": "https://github.com/mne-tools/mne-python/archive/v1.7.0.zip", "issueTracker": "https://github.com/mne-tools/mne-python/issues", "name": "MNE-Python", - "version": "1.6.0", + "version": "1.7.0", "description": "MNE-Python is an open-source Python package for exploring, visualizing, and analyzing human neurophysiological data. It provides methods for data input/output, preprocessing, visualization, source estimation, time-frequency analysis, connectivity analysis, machine learning, and statistics.", "applicationCategory": "Neuroscience", "developmentStatus": "active", @@ -37,7 +37,7 @@ "macOS" ], "softwareRequirements": [ - "python>=3.8", + "python>=3.9", "numpy>=1.21.2", "scipy>=1.7.1", "matplotlib>=3.5.0", @@ -46,9 +46,7 @@ "decorator", "packaging", "jinja2", - "importlib_resources>=5.10.2; python_version<'3.9'", - "lazy_loader>=0.3", - "defusedxml" + "lazy_loader>=0.3" ], "author": [ { @@ -99,18 +97,18 @@ "givenName":"Jona", "familyName": "Sassenhagen" }, - { - "@type":"Person", - "email":"mluessi@nmr.mgh.harvard.edu", - "givenName":"Martin", - "familyName": "Luessi" - }, { "@type":"Person", "email":"dan@mccloy.info", "givenName":"Daniel", "familyName": "McCloy" }, + { + "@type":"Person", + "email":"mluessi@nmr.mgh.harvard.edu", + "givenName":"Martin", + "familyName": "Luessi" + }, { "@type":"Person", "email":"jeanremi.king+github@gmail.com", @@ -153,18 +151,18 @@ "givenName":"Mark", "familyName": "Wronkiewicz" }, - { - "@type":"Person", - "email":"choldgraf@gmail.com", - "givenName":"Chris", - "familyName": "Holdgraf" - }, { "@type":"Person", "email":"aprockhill206@gmail.com", "givenName":"Alex", "familyName": "Rockhill" }, + { + "@type":"Person", + "email":"choldgraf@gmail.com", + "givenName":"Chris", + "familyName": "Holdgraf" + }, { "@type":"Person", "email":"mailsik@gmail.com", @@ -345,6 +343,12 @@ "givenName":"Nick", "familyName": "Foti" }, + { + "@type":"Person", + "email":"", + "givenName":"Scott", + "familyName": "Huberty" + }, { "@type":"Person", "email":"cnangini@gmail.com", @@ -357,12 +361,6 @@ "givenName":"José C", "familyName": "García Alanis" }, - { - "@type":"Person", - "email":"", - "givenName":"Scott", - "familyName": "Huberty" - }, { "@type":"Person", "email":"olaf.hauk@mrc-cbu.cam.ac.uk", @@ -489,6 +487,18 @@ "givenName":"Félix", "familyName": "Raimundo" }, + { + "@type":"Person", + "email":"Woessner.jacob@gmail.com", + "givenName":"Jacob", + "familyName": "Woessner" + }, + { + "@type":"Person", + "email":"rcmdnk@gmail.com", + "givenName":"Michiru", + "familyName": "Kaneda" + }, { "@type":"Person", "email":"phillip.alday@mpi.nl", @@ -561,6 +571,18 @@ "givenName":"Dirk", "familyName": "Gütlin" }, + { + "@type":"Person", + "email":"erkkahe@gmail.com", + "givenName":"Erkka", + "familyName": "Heinila" + }, + { + "@type":"Person", + "email":"kristijan.armeni@gmail.com", + "givenName":"Kristijan", + "familyName": "Armeni" + }, { "@type":"Person", "email":"kjs@llama", @@ -603,12 +625,6 @@ "givenName":"Erica", "familyName": "Peterson" }, - { - "@type":"Person", - "email":"erkkahe@gmail.com", - "givenName":"Erkka", - "familyName": "Heinila" - }, { "@type":"Person", "email":"jevri.hanna@gmail.com", @@ -621,12 +637,6 @@ "givenName":"Jon", "familyName": "Houck" }, - { - "@type":"Person", - "email":"rcmdnk@gmail.com", - "givenName":"Michiru", - "familyName": "Kaneda" - }, { "@type":"Person", "email":"neklein@andrew.cmu.edu", @@ -657,12 +667,24 @@ "givenName":"Burkhard", "familyName": "Maess" }, + { + "@type":"Person", + "email":"carinaforster0611@gmail.com", + "givenName":"Carina", + "familyName": "Forster" + }, { "@type":"Person", "email":"christian.oreilly@gmail.com", "givenName":"Christian", "familyName": "O'Reilly" }, + { + "@type":"Person", + "email":"dominik.welke@ae.mpg.de", + "givenName":"Dominik", + "familyName": "Welke" + }, { "@type":"Person", "email":"", @@ -681,12 +703,6 @@ "givenName":"Jack", "familyName": "Zhang" }, - { - "@type":"Person", - "email":"Woessner.jacob@gmail.com", - "givenName":"Jacob", - "familyName": "Woessner" - }, { "@type":"Person", "email":"makkostya@ukr.net", @@ -723,12 +739,6 @@ "givenName":"Saket", "familyName": "Choudhary" }, - { - "@type":"Person", - "email":"carinaforster0611@gmail.com", - "givenName":"Carina", - "familyName": "Forster" - }, { "@type":"Person", "email":"", @@ -801,6 +811,12 @@ "givenName":"Santeri", "familyName": "Ruuskanen" }, + { + "@type":"Person", + "email":"ksherbst@gmail.com", + "givenName":"Sophie", + "familyName": "Herbst" + }, { "@type":"Person", "email":"", @@ -825,12 +841,6 @@ "givenName":"Basile", "familyName": "Pinsard" }, - { - "@type":"Person", - "email":"dominik.welke@ae.mpg.de", - "givenName":"Dominik", - "familyName": "Welke" - }, { "@type":"Person", "email":"dominik.welke@web.de", @@ -879,6 +889,12 @@ "givenName":"Hafeza", "familyName": "Anevar" }, + { + "@type":"Person", + "email":"hamza.abdelhedii@gmail.com", + "givenName":"Hamza", + "familyName": "Abdelhedi" + }, { "@type":"Person", "email":"mail@jan-sosulski.de", @@ -897,6 +913,12 @@ "givenName":"Joshua", "familyName": "Calder-Travis" }, + { + "@type":"Person", + "email":"", + "givenName":"Judy D", + "familyName": "Zhu" + }, { "@type":"Person", "email":"leisenman@wustl.edu", @@ -915,6 +937,12 @@ "givenName":"Marian", "familyName": "Dovgialo" }, + { + "@type":"Person", + "email":"", + "givenName":"Nabil", + "familyName": "Alibou" + }, { "@type":"Person", "email":"", @@ -927,6 +955,12 @@ "givenName":"Nicolas", "familyName": "Legrand" }, + { + "@type":"Person", + "email":"4dvlup@gmail.com", + "givenName":"Nikolai", + "familyName": "Kapralov" + }, { "@type":"Person", "email":"falachrotem@gmail.com", @@ -957,6 +991,18 @@ "givenName":"Steven", "familyName": "Bierer" }, + { + "@type":"Person", + "email":"t.s.binns@outlook.com", + "givenName":"Thomas Samuel", + "familyName": "Binns" + }, + { + "@type":"Person", + "email":"ttstenner@gmail.com", + "givenName":"Tristan", + "familyName": "Stenner" + }, { "@type":"Person", "email":"victor.ferat@live.Fr", @@ -1047,12 +1093,6 @@ "givenName":"George", "familyName": "O'Neill" }, - { - "@type":"Person", - "email":"hamza.abdelhedii@gmail.com", - "givenName":"Hamza", - "familyName": "Abdelhedi" - }, { "@type":"Person", "email":"jean.baptiste.schiratti@gmail.com", @@ -1083,18 +1123,6 @@ "givenName":"Joshua", "familyName": "Teves" }, - { - "@type":"Person", - "email":"", - "givenName":"Judy D", - "familyName": "Zhu" - }, - { - "@type":"Person", - "email":"kristijan.armeni@gmail.com", - "givenName":"Kristijan", - "familyName": "Armeni" - }, { "@type":"Person", "email":"kylemath@gmail.com", @@ -1113,6 +1141,12 @@ "givenName":"Lenny", "familyName": "Varghese" }, + { + "@type":"Person", + "email":"", + "givenName":"Liberty", + "familyName": "Hamilton" + }, { "@type":"Person", "email":"", @@ -1179,6 +1213,12 @@ "givenName":"Oleh", "familyName": "Kozynets" }, + { + "@type":"Person", + "email":"pmolfese@gmail.com", + "givenName":"Peter J", + "familyName": "Molfese" + }, { "@type":"Person", "email":"pierreablin@gmail.com", @@ -1221,12 +1261,6 @@ "givenName":"Sheraz", "familyName": "Khan" }, - { - "@type":"Person", - "email":"ksherbst@gmail.com", - "givenName":"Sophie", - "familyName": "Herbst" - }, { "@type":"Person", "email":"", @@ -1241,15 +1275,15 @@ }, { "@type":"Person", - "email":"", + "email":"tdonoghue.research@gmail.com", "givenName":"Thomas", - "familyName": "Jochmann" + "familyName": "Donoghue" }, { "@type":"Person", - "email":"t.s.binns@outlook.com", - "givenName":"Thomas Samuel", - "familyName": "Binns" + "email":"", + "givenName":"Thomas", + "familyName": "Jochmann" }, { "@type":"Person", @@ -1269,12 +1303,6 @@ "givenName":"Tom", "familyName": "Dupré la Tour" }, - { - "@type":"Person", - "email":"ttstenner@gmail.com", - "givenName":"Tristan", - "familyName": "Stenner" - }, { "@type":"Person", "email":"tzionan@mail.tau.ac.il", @@ -1329,6 +1357,12 @@ "givenName":"Alex", "familyName": "Ciok" }, + { + "@type":"Person", + "email":"", + "givenName":"Alexander", + "familyName": "Kiefer" + }, { "@type":"Person", "email":"7andy121@gmail.com", @@ -1551,6 +1585,12 @@ "givenName":"Federico", "familyName": "Zamberlan" }, + { + "@type":"Person", + "email":"hofaflo@gmail.com", + "givenName":"Florian", + "familyName": "Hofer" + }, { "@type":"Person", "email":"florinpop@me.com", @@ -1599,6 +1639,12 @@ "givenName":"Hamid", "familyName": "Maymandi" }, + { + "@type":"Person", + "email":"", + "givenName":"Hasrat Ali", + "familyName": "Arzoo" + }, { "@type":"Person", "email":"hermann.sonntag@gmail.com", @@ -1631,15 +1677,15 @@ }, { "@type":"Person", - "email":"", + "email":"ivan.zubarev@aalto.fi", "givenName":"Ivan", - "familyName": "Skelin" + "familyName": "Zubarev" }, { "@type":"Person", - "email":"ivan.zubarev@aalto.fi", - "givenName":"Ivan", - "familyName": "Zubarev" + "email":"ivopascal@gmail.com", + "givenName":"Ivo", + "familyName": "de Jong" }, { "@type":"Person", @@ -1737,12 +1783,6 @@ "givenName":"Leonardo S", "familyName": "Barbosa" }, - { - "@type":"Person", - "email":"", - "givenName":"Liberty", - "familyName": "Hamilton" - }, { "@type":"Person", "email":"lorenzo.alfine@gmail.com", @@ -1797,6 +1837,12 @@ "givenName":"Mark Alexander", "familyName": "Henney" }, + { + "@type":"Person", + "email":"", + "givenName":"Martin", + "familyName": "Oberg" + }, { "@type":"Person", "email":"dev@mgschulz.de", @@ -1929,12 +1975,6 @@ "givenName":"Pedro", "familyName": "Silva" }, - { - "@type":"Person", - "email":"pmolfese@gmail.com", - "givenName":"Peter J", - "familyName": "Molfese" - }, { "@type":"Person", "email":"proloy@umd.edu", @@ -1995,6 +2035,12 @@ "givenName":"Richard", "familyName": "Koehler" }, + { + "@type":"Person", + "email":"", + "givenName":"Richard", + "familyName": "Scholz" + }, { "@type":"Person", "email":"rie.acad@gmail.com", @@ -2085,6 +2131,12 @@ "givenName":"Sergey", "familyName": "Antopolskiy" }, + { + "@type":"Person", + "email":"shirazi@ieee.org", + "givenName":"Seyed (Yahya)", + "familyName": "Shirazi" + }, { "@type":"Person", "email":"", @@ -2145,12 +2197,6 @@ "givenName":"T", "familyName": "Wang" }, - { - "@type":"Person", - "email":"tdonoghue.research@gmail.com", - "givenName":"Thomas", - "familyName": "Donoghue" - }, { "@type":"Person", "email":"thomas.moreau.2010@gmail.com", @@ -2193,6 +2239,18 @@ "givenName":"Toomas Erik", "familyName": "Anijärv" }, + { + "@type":"Person", + "email":"", + "givenName":"Velu Prabhakar", + "familyName": "Kumaravel" + }, + { + "@type":"Person", + "email":"williamfrancisturner@gmail.com", + "givenName":"Will", + "familyName": "Turner" + }, { "@type":"Person", "email":"xia@xiaokai.me", @@ -2211,6 +2269,12 @@ "givenName":"Zhi", "familyName": "Zhang" }, + { + "@type":"Person", + "email":"btk.codedev@gmail.com", + "givenName":"", + "familyName": "btkcodedev" + }, { "@type":"Person", "email":"", diff --git a/doc/_static/versions.json b/doc/_static/versions.json index 8141440bd16..48e4006f494 100644 --- a/doc/_static/versions.json +++ b/doc/_static/versions.json @@ -1,14 +1,19 @@ [ { - "name": "1.7 (devel)", + "name": "1.8 (devel)", "version": "dev", "url": "https://mne.tools/dev/" }, { - "name": "1.6 (stable)", + "name": "1.7 (stable)", "version": "stable", "url": "https://mne.tools/stable/" }, + { + "name": "1.6", + "version": "1.6", + "url": "https://mne.tools/1.6/" + }, { "name": "1.5", "version": "1.5", diff --git a/doc/changes/devel.rst b/doc/changes/devel.rst deleted file mode 100644 index 0e80d522b51..00000000000 --- a/doc/changes/devel.rst +++ /dev/null @@ -1,5 +0,0 @@ -.. 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/11234.newfeature.rst b/doc/changes/devel/11234.newfeature.rst deleted file mode 100644 index 46cc408a3d9..00000000000 --- a/doc/changes/devel/11234.newfeature.rst +++ /dev/null @@ -1 +0,0 @@ -Detecting Bad EEG/MEG channels using the local outlier factor (LOF) algorithm in :func:`mne.preprocessing.find_bad_channels_lof`, by :newcontrib:`Velu Prabhakar Kumaravel`. \ No newline at end of file diff --git a/doc/changes/devel/11282.apichange.rst b/doc/changes/devel/11282.apichange.rst deleted file mode 100644 index 9112db897cf..00000000000 --- a/doc/changes/devel/11282.apichange.rst +++ /dev/null @@ -1 +0,0 @@ -The default value of the ``zero_mean`` parameter of :func:`mne.time_frequency.tfr_array_morlet` will change from ``False`` to ``True`` in version 1.8, for consistency with related functions. By `Daniel McCloy`_. diff --git a/doc/changes/devel/11282.bugfix.rst b/doc/changes/devel/11282.bugfix.rst deleted file mode 100644 index 72e6e73a42a..00000000000 --- a/doc/changes/devel/11282.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fixes to interactivity in time-frequency objects: the rectangle selector now works on TFR image plots of gradiometer data; and in ``TFR.plot_joint()`` plots, the colormap limits of interactively-generated topomaps match the colormap limits of the main plot. By `Daniel McCloy`_. \ No newline at end of file diff --git a/doc/changes/devel/11282.newfeature.rst b/doc/changes/devel/11282.newfeature.rst deleted file mode 100644 index 5c19d68f351..00000000000 --- a/doc/changes/devel/11282.newfeature.rst +++ /dev/null @@ -1 +0,0 @@ -New class :class:`mne.time_frequency.RawTFR` and new methods :meth:`mne.io.Raw.compute_tfr`, :meth:`mne.Epochs.compute_tfr`, and :meth:`mne.Evoked.compute_tfr`. These new methods supersede functions :func:`mne.time_frequency.tfr_morlet`, and :func:`mne.time_frequency.tfr_multitaper`, and :func:`mne.time_frequency.tfr_stockwell`, which are now considered "legacy" functions. By `Daniel McCloy`_. \ No newline at end of file diff --git a/doc/changes/devel/12190.bugfix.rst b/doc/changes/devel/12190.bugfix.rst deleted file mode 100644 index d7ef2e07444..00000000000 --- a/doc/changes/devel/12190.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Allow :func:`mne.viz.plot_compare_evokeds` to plot eyetracking channels, and improve error handling, y `Scott Huberty`_. \ No newline at end of file diff --git a/doc/changes/devel/12195.newfeature.rst b/doc/changes/devel/12195.newfeature.rst deleted file mode 100644 index 0c7e044abce..00000000000 --- a/doc/changes/devel/12195.newfeature.rst +++ /dev/null @@ -1 +0,0 @@ -Add ability reject :class:`mne.Epochs` using callables, by `Jacob Woessner`_. \ No newline at end of file diff --git a/doc/changes/devel/12206.bugfix.rst b/doc/changes/devel/12206.bugfix.rst deleted file mode 100644 index 6cf72e266b9..00000000000 --- a/doc/changes/devel/12206.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix bug in :meth:`mne.Epochs.apply_function` where data was handed down incorrectly in parallel processing, by `Dominik Welke`_. \ No newline at end of file diff --git a/doc/changes/devel/12206.newfeature.rst b/doc/changes/devel/12206.newfeature.rst deleted file mode 100644 index 9ef966ed208..00000000000 --- a/doc/changes/devel/12206.newfeature.rst +++ /dev/null @@ -1,3 +0,0 @@ -Custom functions applied via :meth:`mne.io.Raw.apply_function`, :meth:`mne.Epochs.apply_function` or :meth:`mne.Evoked.apply_function` can now use ``ch_idx`` or ``ch_name`` to get access to the currently processed channel during channel wise processing. - -:meth:`mne.Evoked.apply_function` can now also work on full data array instead of just channel wise, analogous to :meth:`mne.io.Raw.apply_function` and :meth:`mne.Epochs.apply_function`, by `Dominik Welke`_. \ No newline at end of file diff --git a/doc/changes/devel/12207.newfeature.rst b/doc/changes/devel/12207.newfeature.rst deleted file mode 100644 index 7d741a06bf5..00000000000 --- a/doc/changes/devel/12207.newfeature.rst +++ /dev/null @@ -1 +0,0 @@ -Allow :class:`mne.time_frequency.EpochsTFR` as input to :func:`mne.epochs.equalize_epoch_counts`, by `Carina Forster`_. \ No newline at end of file diff --git a/doc/changes/devel/12218.newfeature.rst b/doc/changes/devel/12218.newfeature.rst deleted file mode 100644 index 4ea286f0a22..00000000000 --- a/doc/changes/devel/12218.newfeature.rst +++ /dev/null @@ -1 +0,0 @@ -Speed up export to .edf in :func:`mne.export.export_raw` by using ``edfio`` instead of ``EDFlib-Python``. diff --git a/doc/changes/devel/12236.bugfix.rst b/doc/changes/devel/12236.bugfix.rst deleted file mode 100644 index ad807ea3487..00000000000 --- a/doc/changes/devel/12236.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Remove incorrect type hints in :func:`mne.io.read_raw_neuralynx`, by `Richard Höchenberger`_. diff --git a/doc/changes/devel/12237.newfeature.rst b/doc/changes/devel/12237.newfeature.rst deleted file mode 100644 index e89822f27ed..00000000000 --- a/doc/changes/devel/12237.newfeature.rst +++ /dev/null @@ -1,2 +0,0 @@ -Added a helper function :func:`mne.preprocessing.eyetracking.convert_units` to convert eyegaze data from pixel-on-screen values to radians of visual angle. Also added a helper function :func:`mne.preprocessing.eyetracking.get_screen_visual_angle` to get the visual angle that the participant screen subtends, by `Scott Huberty`_. - diff --git a/doc/changes/devel/12238.newfeature.rst b/doc/changes/devel/12238.newfeature.rst deleted file mode 100644 index 631722bc07a..00000000000 --- a/doc/changes/devel/12238.newfeature.rst +++ /dev/null @@ -1 +0,0 @@ -Inform the user about channel discrepancy between provided info, forward operator, and/or covariance matrices in :func:`mne.beamformer.make_lcmv`, by :newcontrib:`Nikolai Kapralov`. \ No newline at end of file diff --git a/doc/changes/devel/12248.bugfix.rst b/doc/changes/devel/12248.bugfix.rst deleted file mode 100644 index bc4124a2267..00000000000 --- a/doc/changes/devel/12248.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix bug with accessing the last data sample using ``raw[:, -1]`` where an empty array was returned, by `Eric Larson`_. diff --git a/doc/changes/devel/12250.newfeature.rst b/doc/changes/devel/12250.newfeature.rst deleted file mode 100644 index 20d67dead77..00000000000 --- a/doc/changes/devel/12250.newfeature.rst +++ /dev/null @@ -1 +0,0 @@ -We added type hints for the return values of :func:`mne.read_evokeds` and :func:`mne.io.read_raw`. Development environments like VS Code or PyCharm will now provide more help when using these functions in your code. By `Richard Höchenberger`_ and `Eric Larson`_. (:gh:`12297`) diff --git a/doc/changes/devel/12250.notable.rst b/doc/changes/devel/12250.notable.rst deleted file mode 100644 index 7616894e636..00000000000 --- a/doc/changes/devel/12250.notable.rst +++ /dev/null @@ -1,11 +0,0 @@ -In this version, we started adding type hints (also known as "type annotations") to select parts of the codebase. -This meta information will be used by development environments (IDEs) like VS Code and PyCharm automatically to provide -better assistance such as tab completion or error detection even before running your code. - -So far, we've only added return type hints to :func:`mne.io.read_raw`, :func:`mne.read_epochs`, :func:`mne.read_evokeds` and -all format-specific ``read_raw_*()`` and ``read_epochs_*()`` functions. Now your editors will know: -these functions return evoked and raw data, respectively. We are planning add type hints to more functions after careful -evaluation in the future. - -You don't need to do anything to benefit from these changes – your editor will pick them up automatically and provide the -enhanced experience if it supports it! diff --git a/doc/changes/devel/12264.dependency.rst b/doc/changes/devel/12264.dependency.rst deleted file mode 100644 index c511b3448a8..00000000000 --- a/doc/changes/devel/12264.dependency.rst +++ /dev/null @@ -1 +0,0 @@ -``defusedxml`` is now an optional (rather than required) dependency and needed when reading EGI-MFF data, NEDF data, and BrainVision montages, by `Eric Larson`_. \ No newline at end of file diff --git a/doc/changes/devel/12268.newfeature.rst b/doc/changes/devel/12268.newfeature.rst deleted file mode 100644 index caf46fec03f..00000000000 --- a/doc/changes/devel/12268.newfeature.rst +++ /dev/null @@ -1 +0,0 @@ -Add ``method="polyphase"`` to :meth:`mne.io.Raw.resample` and related functions to allow resampling using :func:`scipy.signal.upfirdn`, by `Eric Larson`_. \ No newline at end of file diff --git a/doc/changes/devel/12269.newfeature.rst b/doc/changes/devel/12269.newfeature.rst deleted file mode 100644 index 321bd02070e..00000000000 --- a/doc/changes/devel/12269.newfeature.rst +++ /dev/null @@ -1 +0,0 @@ -The package build backend was switched from ``setuptools`` to ``hatchling``. This will only affect users who build and install MNE-Python from source. By `Richard Höchenberger`_. (:gh:`12281`) \ No newline at end of file diff --git a/doc/changes/devel/12279.bugfix.rst b/doc/changes/devel/12279.bugfix.rst deleted file mode 100644 index 93aee511fec..00000000000 --- a/doc/changes/devel/12279.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Correctly handle temporal gaps in Neuralynx .ncs files via :func:`mne.io.read_raw_neuralynx`, by `Kristijan Armeni`_ and `Eric Larson`_. \ No newline at end of file diff --git a/doc/changes/devel/12282.bugfix.rst b/doc/changes/devel/12282.bugfix.rst deleted file mode 100644 index e743d0b6071..00000000000 --- a/doc/changes/devel/12282.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix bug where parent directory existence was not checked properly in :meth:`mne.io.Raw.save`, by `Eric Larson`_. diff --git a/doc/changes/devel/12289.newfeature.rst b/doc/changes/devel/12289.newfeature.rst deleted file mode 100644 index 8110e4cf737..00000000000 --- a/doc/changes/devel/12289.newfeature.rst +++ /dev/null @@ -1 +0,0 @@ -:meth:`mne.Annotations.to_data_frame` can now output different formats for the ``onset`` column: seconds, milliseconds, datetime objects, and timedelta objects. By `Daniel McCloy`_. diff --git a/doc/changes/devel/12299.other.rst b/doc/changes/devel/12299.other.rst deleted file mode 100644 index 61c4bf56725..00000000000 --- a/doc/changes/devel/12299.other.rst +++ /dev/null @@ -1 +0,0 @@ -Adopted towncrier_ for changelog entries, by `Eric Larson`_. diff --git a/doc/changes/devel/12308.apichange.rst b/doc/changes/devel/12308.apichange.rst deleted file mode 100644 index 4d1b8e13923..00000000000 --- a/doc/changes/devel/12308.apichange.rst +++ /dev/null @@ -1 +0,0 @@ -The parameter for providing data to :func:`mne.time_frequency.tfr_array_morlet` and :func:`mne.time_frequency.tfr_array_multitaper` has been switched from ``epoch_data`` to ``data``. Only use the ``data`` parameter to avoid a warning. Changes by `Thomas Binns`_. \ No newline at end of file diff --git a/doc/changes/devel/12309.newfeature.rst b/doc/changes/devel/12309.newfeature.rst deleted file mode 100644 index 8e732044a8e..00000000000 --- a/doc/changes/devel/12309.newfeature.rst +++ /dev/null @@ -1 +0,0 @@ -Add method :meth:`mne.SourceEstimate.save_as_surface` to allow saving GIFTI files from surface source estimates, by `Peter Molfese`_. diff --git a/doc/changes/devel/12311.newfeature.rst b/doc/changes/devel/12311.newfeature.rst deleted file mode 100644 index c5e074278f9..00000000000 --- a/doc/changes/devel/12311.newfeature.rst +++ /dev/null @@ -1 +0,0 @@ -:class:`mne.Epochs` can now be constructed using :class:`mne.Annotations` stored in the ``raw`` object, by specifying ``events=None``. By `Alex Rockhill`_. \ No newline at end of file diff --git a/doc/changes/devel/12318.other.rst b/doc/changes/devel/12318.other.rst deleted file mode 100644 index 94890e1dfc4..00000000000 --- a/doc/changes/devel/12318.other.rst +++ /dev/null @@ -1 +0,0 @@ -Automate adding of PR number to towncrier stubs, by `Eric Larson`_. diff --git a/doc/changes/devel/12319.bugfix.rst b/doc/changes/devel/12319.bugfix.rst deleted file mode 100644 index 16eb1a3350a..00000000000 --- a/doc/changes/devel/12319.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix bug where section parameter in :meth:`mne.Report.add_html` was not being utilized resulting in improper formatting, by :newcontrib:`Martin Oberg`. diff --git a/doc/changes/devel/12323.newfeature.rst b/doc/changes/devel/12323.newfeature.rst deleted file mode 100644 index f10fdf5cf23..00000000000 --- a/doc/changes/devel/12323.newfeature.rst +++ /dev/null @@ -1 +0,0 @@ -Add :meth:`~mne.SourceEstimate.savgol_filter`, :meth:`~mne.SourceEstimate.filter`, :meth:`~mne.SourceEstimate.apply_hilbert`, and :meth:`~mne.SourceEstimate.apply_function` methods to :class:`mne.SourceEstimate` and related classes, by `Hamza Abdelhedi`_. \ No newline at end of file diff --git a/doc/changes/devel/12324.bugfix.rst b/doc/changes/devel/12324.bugfix.rst deleted file mode 100644 index ec7f2c5849d..00000000000 --- a/doc/changes/devel/12324.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Add ``tol`` parameter to :meth:`mne.events_from_annotations` so that the user can specify the tolerance to ignore rounding errors of event onsets when using ``chunk_duration`` is not None (default is 1e-8), by `Michiru Kaneda`_ diff --git a/doc/changes/devel/12326.other.rst b/doc/changes/devel/12326.other.rst deleted file mode 100644 index f0bd6a377d6..00000000000 --- a/doc/changes/devel/12326.other.rst +++ /dev/null @@ -1 +0,0 @@ -Updated the text in the preprocessing tutorial to use :meth:`mne.io.Raw.pick` instead of the legacy :meth:`mne.io.Raw.pick_types`, by :newcontrib:`btkcodedev`. diff --git a/doc/changes/devel/12332.newfeature.rst b/doc/changes/devel/12332.newfeature.rst deleted file mode 100644 index 0a7a82227ba..00000000000 --- a/doc/changes/devel/12332.newfeature.rst +++ /dev/null @@ -1 +0,0 @@ -Add ability to export STIM channels to EDF in :meth:`mne.io.Raw.export`, by `Clemens Brunner`_. \ No newline at end of file diff --git a/doc/changes/devel/12336.bugfix.rst b/doc/changes/devel/12336.bugfix.rst deleted file mode 100644 index c7ce44b8dab..00000000000 --- a/doc/changes/devel/12336.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Allow :meth:`mne.io.Raw.interpolate_bads` and :meth:`mne.Epochs.interpolate_bads` to work on ``ecog`` and ``seeg`` data; for ``seeg`` data a spline is fit to neighboring electrode contacts on the same shaft, by `Alex Rockhill`_ \ No newline at end of file diff --git a/doc/changes/devel/12343.newfeature.rst b/doc/changes/devel/12343.newfeature.rst deleted file mode 100644 index 9825f924e48..00000000000 --- a/doc/changes/devel/12343.newfeature.rst +++ /dev/null @@ -1 +0,0 @@ -Speed up raw FIF reading when using small buffer sizes by `Eric Larson`_. \ No newline at end of file diff --git a/doc/changes/devel/12345.bugfix.rst b/doc/changes/devel/12345.bugfix.rst deleted file mode 100644 index fa592c6926c..00000000000 --- a/doc/changes/devel/12345.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix clicking on an axis of :func:`mne.viz.plot_evoked_topo` when multiple vertical lines ``vlines`` are used, by `Mathieu Scheltienne`_. diff --git a/doc/changes/devel/12348.bugfix.rst b/doc/changes/devel/12348.bugfix.rst deleted file mode 100644 index aad91ed9dec..00000000000 --- a/doc/changes/devel/12348.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix bug in :func:`mne.preprocessing.maxwell_filter` where calibration was incorrectly applied during virtual sensor reconstruction, by `Eric Larson`_ and :newcontrib:`Motofumi Fushimi`. diff --git a/doc/changes/devel/12354.bugfix.rst b/doc/changes/devel/12354.bugfix.rst deleted file mode 100644 index f3c944c9373..00000000000 --- a/doc/changes/devel/12354.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix bug in :meth:`mne.viz.EvokedField.set_vmax` that prevented setting the color limits of the MEG magnetic field density, by `Marijn van Vliet`_ diff --git a/doc/changes/devel/12357.bugfix.rst b/doc/changes/devel/12357.bugfix.rst deleted file mode 100644 index d38ce54d5f5..00000000000 --- a/doc/changes/devel/12357.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix faulty indexing in :func:`mne.io.read_raw_neuralynx` when picking a single channel, by `Kristijan Armeni`_. \ No newline at end of file diff --git a/doc/changes/devel/12358.other.rst b/doc/changes/devel/12358.other.rst deleted file mode 100644 index 788db1d1a41..00000000000 --- a/doc/changes/devel/12358.other.rst +++ /dev/null @@ -1 +0,0 @@ -Refresh code base to use Python 3.9 syntax using Ruff UP rules (pyupgrade), by `Clemens Brunner`_. \ No newline at end of file diff --git a/doc/changes/devel/12371.newfeature.rst b/doc/changes/devel/12371.newfeature.rst deleted file mode 100644 index 4d28ff1f5ce..00000000000 --- a/doc/changes/devel/12371.newfeature.rst +++ /dev/null @@ -1 +0,0 @@ -Speed up :func:`mne.io.read_raw_neuralynx` on large datasets with many gaps, by `Kristijan Armeni`_. \ No newline at end of file diff --git a/doc/changes/devel/12376.dependency.rst b/doc/changes/devel/12376.dependency.rst deleted file mode 100644 index 148ce8ac9ec..00000000000 --- a/doc/changes/devel/12376.dependency.rst +++ /dev/null @@ -1 +0,0 @@ -For developers, ``pytest>=8.0`` is now required for running unit tests, by `Eric Larson`_. diff --git a/doc/changes/devel/12380.bugfix.rst b/doc/changes/devel/12380.bugfix.rst deleted file mode 100644 index 8c5ee5a6fca..00000000000 --- a/doc/changes/devel/12380.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix bug where :func:`mne.preprocessing.compute_proj_ecg` and :func:`mne.preprocessing.compute_proj_eog` could modify the default ``reject`` and ``flat`` arguments on multiple calls based on channel types present, by `Eric Larson`_. diff --git a/doc/changes/devel/12382.apichange.rst b/doc/changes/devel/12382.apichange.rst deleted file mode 100644 index aa38b436cf0..00000000000 --- a/doc/changes/devel/12382.apichange.rst +++ /dev/null @@ -1 +0,0 @@ -Change :func:`mne.stc_near_sensors` ``surface`` default from the ``'pial'`` surface to the surface in ``src`` if ``src`` is not ``None`` in version 1.8, by `Alex Rockhill`_. diff --git a/doc/changes/devel/12382.bugfix.rst b/doc/changes/devel/12382.bugfix.rst deleted file mode 100644 index 8409f016206..00000000000 --- a/doc/changes/devel/12382.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix bad channels not handled properly in :func:`mne.stc_near_sensors` by `Alex Rockhill`_. diff --git a/doc/changes/devel/12383.newfeature.rst b/doc/changes/devel/12383.newfeature.rst deleted file mode 100644 index f896572eb93..00000000000 --- a/doc/changes/devel/12383.newfeature.rst +++ /dev/null @@ -1 +0,0 @@ -Add ability to detect minima peaks found in :class:`mne.Evoked` if data is all positive and maxima if data is all negative. \ No newline at end of file diff --git a/doc/changes/devel/12389.bugfix.rst b/doc/changes/devel/12389.bugfix.rst deleted file mode 100644 index 85892df97a8..00000000000 --- a/doc/changes/devel/12389.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix bug where :func:`mne.preprocessing.regress_artifact` projection check was not specific to the channels being processed, by `Eric Larson`_. diff --git a/doc/changes/devel/12393.bugfix.rst b/doc/changes/devel/12393.bugfix.rst deleted file mode 100644 index 017f81b398b..00000000000 --- a/doc/changes/devel/12393.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Change how samples are read when using ``data_format='auto'`` in :func:`mne.io.read_raw_cnt`, by `Jacob Woessner`_. \ No newline at end of file diff --git a/doc/changes/devel/12394.newfeature.rst b/doc/changes/devel/12394.newfeature.rst deleted file mode 100644 index de456e91461..00000000000 --- a/doc/changes/devel/12394.newfeature.rst +++ /dev/null @@ -1 +0,0 @@ -Add ability to remove bad marker coils in :func:`mne.io.read_raw_kit`, by `Judy D Zhu`_. \ No newline at end of file diff --git a/doc/changes/devel/12399.bugfix.rst b/doc/changes/devel/12399.bugfix.rst deleted file mode 100644 index cf53e91b5c8..00000000000 --- a/doc/changes/devel/12399.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix bugs with :class:`mne.Report` CSS where TOC items could disappear at the bottom of the page, by `Eric Larson`_. \ No newline at end of file diff --git a/doc/changes/devel/12410.bugfix.rst b/doc/changes/devel/12410.bugfix.rst deleted file mode 100644 index c5d939845b0..00000000000 --- a/doc/changes/devel/12410.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -In :func:`~mne.viz.plot_compare_evokeds`, actually plot GFP (not RMS amplitude) for EEG channels when global field power is requested by `Daniel McCloy`_. \ No newline at end of file diff --git a/doc/changes/devel/12420.other.rst b/doc/changes/devel/12420.other.rst deleted file mode 100644 index 8b949d25dc7..00000000000 --- a/doc/changes/devel/12420.other.rst +++ /dev/null @@ -1 +0,0 @@ -Clarify in the :ref:`EEG referencing tutorial ` that an average reference projector ready is required for inverse modeling, by :newcontrib:`Nabil Alibou` diff --git a/doc/changes/devel/12430.bugfix.rst b/doc/changes/devel/12430.bugfix.rst deleted file mode 100644 index 688e7066fa8..00000000000 --- a/doc/changes/devel/12430.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Reformats channel and detector lookup in :func:`mne.io.read_raw_snirf` from array based to dictionary based. Removes incorrect assertions that every detector and source must have data associated with every registered optode position, by :newcontrib:`Alex Kiefer`. \ No newline at end of file diff --git a/doc/changes/devel/12436.bugfix.rst b/doc/changes/devel/12436.bugfix.rst deleted file mode 100644 index 7ddbd9f5d21..00000000000 --- a/doc/changes/devel/12436.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix :ref:`tut-working-with-seeg` use of :func:`mne.stc_near_sensors` to use the :class:`mne.VolSourceEstimate` positions and not the pial surface, by `Alex Rockhill`_ diff --git a/doc/changes/devel/12441.bugfix.rst b/doc/changes/devel/12441.bugfix.rst deleted file mode 100644 index 87a2d10a710..00000000000 --- a/doc/changes/devel/12441.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix prefiltering information management for EDF/BDF, by `Michiru Kaneda`_ diff --git a/doc/changes/devel/12443.newfeature.rst b/doc/changes/devel/12443.newfeature.rst deleted file mode 100644 index f704e45b4a5..00000000000 --- a/doc/changes/devel/12443.newfeature.rst +++ /dev/null @@ -1 +0,0 @@ -Add option to pass ``image_kwargs`` to :class:`mne.Report.add_epochs` to allow adjusting e.g. ``vmin`` and ``vmax`` of the epochs image in the report, by `Sophie Herbst`_. \ No newline at end of file diff --git a/doc/changes/devel/12444.bugfix.rst b/doc/changes/devel/12444.bugfix.rst deleted file mode 100644 index c27fb5e8425..00000000000 --- a/doc/changes/devel/12444.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix validation of ``ch_type`` in :func:`mne.preprocessing.annotate_muscle_zscore`, by `Mathieu Scheltienne`_. diff --git a/doc/changes/devel/12445.newfeature.rst b/doc/changes/devel/12445.newfeature.rst deleted file mode 100644 index ccaef2c2c07..00000000000 --- a/doc/changes/devel/12445.newfeature.rst +++ /dev/null @@ -1 +0,0 @@ -Add support for multiple raw instances in :func:`mne.preprocessing.compute_average_dev_head_t` by `Eric Larson`_. diff --git a/doc/changes/devel/12446.newfeature.rst b/doc/changes/devel/12446.newfeature.rst deleted file mode 100644 index 734721ce628..00000000000 --- a/doc/changes/devel/12446.newfeature.rst +++ /dev/null @@ -1 +0,0 @@ -Support partial pathlength factors for each wavelength in :func:`mne.preprocessing.nirs.beer_lambert_law`, by :newcontrib:`Richard Scholz`. diff --git a/doc/changes/devel/12450.other.rst b/doc/changes/devel/12450.other.rst deleted file mode 100644 index 48265f87416..00000000000 --- a/doc/changes/devel/12450.other.rst +++ /dev/null @@ -1 +0,0 @@ -Move private data preparation functions for BrainVision export from ``pybv`` to ``mne``, by `Clemens Brunner`_. \ No newline at end of file diff --git a/doc/changes/devel/12451.bugfix.rst b/doc/changes/devel/12451.bugfix.rst deleted file mode 100644 index 2aca44529f1..00000000000 --- a/doc/changes/devel/12451.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix errant redundant use of ``BIDSPath.split`` when writing split raw and epochs data, by `Eric Larson`_. diff --git a/doc/changes/devel/12451.dependency.rst b/doc/changes/devel/12451.dependency.rst deleted file mode 100644 index 8227dd779ad..00000000000 --- a/doc/changes/devel/12451.dependency.rst +++ /dev/null @@ -1 +0,0 @@ -``pytest-harvest`` is no longer used as a test dependency, by `Eric Larson`_. diff --git a/doc/changes/devel/12454.newfeature.rst b/doc/changes/devel/12454.newfeature.rst deleted file mode 100644 index 5a4a9cc9cdb..00000000000 --- a/doc/changes/devel/12454.newfeature.rst +++ /dev/null @@ -1 +0,0 @@ -Completing PR 12453. Add option to pass ``image_kwargs`` per channel type to :class:`mne.Report.add_epochs`. \ No newline at end of file diff --git a/doc/changes/devel/12456.bugfix.rst b/doc/changes/devel/12456.bugfix.rst deleted file mode 100644 index 01e15b3c22e..00000000000 --- a/doc/changes/devel/12456.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Disable config parser interpolation when reading BrainVision files, which allows using the percent sign as a regular character in channel units, by `Clemens Brunner`_. \ No newline at end of file diff --git a/doc/changes/devel/12461.other.rst b/doc/changes/devel/12461.other.rst deleted file mode 100644 index b6fcea48fc7..00000000000 --- a/doc/changes/devel/12461.other.rst +++ /dev/null @@ -1 +0,0 @@ -Fix dead links in ``README.rst`` documentation by :newcontrib:`Will Turner`. \ No newline at end of file diff --git a/doc/changes/devel/12462.newfeature.rst b/doc/changes/devel/12462.newfeature.rst deleted file mode 100644 index 4624579ba26..00000000000 --- a/doc/changes/devel/12462.newfeature.rst +++ /dev/null @@ -1 +0,0 @@ -:func:`mne.epochs.make_metadata` now accepts strings as ``tmin`` and ``tmax`` parameter values, simplifying metadata creation based on time-varying events such as responses to a stimulus, by `Richard Höchenberger`_. diff --git a/doc/changes/devel/12463.newfeature.rst b/doc/changes/devel/12463.newfeature.rst deleted file mode 100644 index d041b0c912f..00000000000 --- a/doc/changes/devel/12463.newfeature.rst +++ /dev/null @@ -1 +0,0 @@ -Include date of acquisition and filter parameters in ``raw.info`` for :func:`mne.io.read_raw_neuralynx` by `Kristijan Armeni`_. \ No newline at end of file diff --git a/doc/changes/devel/12464.other.rst b/doc/changes/devel/12464.other.rst deleted file mode 100644 index 6839c4ebe61..00000000000 --- a/doc/changes/devel/12464.other.rst +++ /dev/null @@ -1,2 +0,0 @@ -Replacing percent format with f-strings format specifiers , by :newcontrib:`Hasrat Ali Arzoo`. - diff --git a/doc/changes/devel/12467.newfeature.rst b/doc/changes/devel/12467.newfeature.rst deleted file mode 100644 index 457a2746d17..00000000000 --- a/doc/changes/devel/12467.newfeature.rst +++ /dev/null @@ -1 +0,0 @@ -Add ``picks`` parameter to :meth:`mne.io.Raw.plot`, allowing users to select which channels to plot. This makes makes the raw data plotting API consistent with :meth:`mne.Epochs.plot` and :meth:`mne.Evoked.plot`, by :newcontrib:`Ivo de Jong`. \ No newline at end of file diff --git a/doc/changes/devel/12470.bugfix.rst b/doc/changes/devel/12470.bugfix.rst deleted file mode 100644 index d8d72843304..00000000000 --- a/doc/changes/devel/12470.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -- Fix the default color of :meth:`mne.viz.Brain.add_text` to properly contrast with the figure background color, by `Marijn van Vliet`_. diff --git a/doc/changes/devel/12474.bugfix.rst b/doc/changes/devel/12474.bugfix.rst deleted file mode 100644 index 875d7574f7b..00000000000 --- a/doc/changes/devel/12474.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -- Changed default ECoG and sEEG electrode sizes in brain plots to better reflect real world sizes, by `Liberty Hamilton`_ diff --git a/doc/changes/devel/12476.bugfix.rst b/doc/changes/devel/12476.bugfix.rst deleted file mode 100644 index 464ef11307c..00000000000 --- a/doc/changes/devel/12476.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed bugs with handling of rank in :class:`mne.decoding.CSP`, by `Eric Larson`_. \ No newline at end of file diff --git a/doc/changes/devel/12481.bugfix.rst b/doc/changes/devel/12481.bugfix.rst deleted file mode 100644 index a9108fe4040..00000000000 --- a/doc/changes/devel/12481.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -- Fix reading segmented recordings with :func:`mne.io.read_raw_eyelink` by `Dominik Welke`_. \ No newline at end of file diff --git a/doc/changes/devel/12483.bugfix.rst b/doc/changes/devel/12483.bugfix.rst deleted file mode 100644 index 601bf94838c..00000000000 --- a/doc/changes/devel/12483.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Improve compatibility with other Qt-based GUIs by handling theme icons better, by `Eric Larson`_. diff --git a/doc/changes/devel/12484.bugfix.rst b/doc/changes/devel/12484.bugfix.rst deleted file mode 100644 index 2430f534661..00000000000 --- a/doc/changes/devel/12484.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -- Fix problem caused by onsets with NaN values using :func:`mne.io.read_raw_eeglab` by `Jacob Woessner`_ \ No newline at end of file diff --git a/doc/changes/devel/12489.bugfix.rst b/doc/changes/devel/12489.bugfix.rst deleted file mode 100644 index 9172ec64f7e..00000000000 --- a/doc/changes/devel/12489.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix cleaning of channel names for non vectorview or CTF dataset including whitespaces or dash in their channel names, by `Mathieu Scheltienne`_. diff --git a/doc/changes/devel/12491.dependency.rst b/doc/changes/devel/12491.dependency.rst deleted file mode 100644 index 423082320ca..00000000000 --- a/doc/changes/devel/12491.dependency.rst +++ /dev/null @@ -1 +0,0 @@ -The minimum supported version of Qt bindings is 5.15, by `Eric Larson`_. diff --git a/doc/changes/devel/12498.bugfix.rst b/doc/changes/devel/12498.bugfix.rst deleted file mode 100644 index 2655cf692d1..00000000000 --- a/doc/changes/devel/12498.bugfix.rst +++ /dev/null @@ -1,2 +0,0 @@ -Fix bug with :meth:`mne.preprocessing.ICA.plot_sources` for ``evoked`` data where the -legend contained too many entries, by `Eric Larson`_. diff --git a/doc/changes/devel/12507.bugfix.rst b/doc/changes/devel/12507.bugfix.rst deleted file mode 100644 index c172701bb93..00000000000 --- a/doc/changes/devel/12507.bugfix.rst +++ /dev/null @@ -1,5 +0,0 @@ -Fix bug where using ``phase="minimum"`` in filtering functions like -:meth:`mne.io.Raw.filter` constructed a filter half the desired length with -compromised attenuation. Now ``phase="minimum"`` has the same length and comparable -suppression as ``phase="zero"``, and the old (incorrect) behavior can be achieved -with ``phase="minimum-half"``, by `Eric Larson`_. diff --git a/doc/changes/devel/12509.other.rst b/doc/changes/devel/12509.other.rst deleted file mode 100644 index e3709653025..00000000000 --- a/doc/changes/devel/12509.other.rst +++ /dev/null @@ -1,2 +0,0 @@ -Update the list of sensor types in docstrings, tutorials and the glossary by `Nabil Alibou`_. - diff --git a/doc/changes/devel/12510.newfeature.rst b/doc/changes/devel/12510.newfeature.rst deleted file mode 100644 index 3194e47e6a9..00000000000 --- a/doc/changes/devel/12510.newfeature.rst +++ /dev/null @@ -1 +0,0 @@ -Add ``physical_range="channelwise"`` to :meth:`mne.io.Raw.export` for exporting to EDF, which can improve amplitude resolution if individual channels vary greatly in their offsets, by `Clemens Brunner`_. \ No newline at end of file diff --git a/doc/changes/devel/12513.newfeature.rst b/doc/changes/devel/12513.newfeature.rst deleted file mode 100644 index 7189adaf3c0..00000000000 --- a/doc/changes/devel/12513.newfeature.rst +++ /dev/null @@ -1,2 +0,0 @@ -Added the ability to reorder report contents via :meth:`mne.Report.reorder` (with -helper to get contents with :meth:`mne.Report.get_contents`), by `Eric Larson`_. diff --git a/doc/changes/devel/12518.newfeature.rst b/doc/changes/devel/12518.newfeature.rst deleted file mode 100644 index 306254ee6be..00000000000 --- a/doc/changes/devel/12518.newfeature.rst +++ /dev/null @@ -1 +0,0 @@ -Add ``exclude_after_unique`` option to :meth:`mne.io.read_raw_edf` and :meth:`mne.io.read_raw_edf` to search for exclude channels after making channels names unique, by `Michiru Kaneda`_ diff --git a/doc/changes/devel/12523.bugfix.rst b/doc/changes/devel/12523.bugfix.rst deleted file mode 100644 index 3ce8cea9d11..00000000000 --- a/doc/changes/devel/12523.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Remove FDT file format check for strings in EEGLAB's EEG.data in :func:`mne.io.read_raw_eeglab` and related functions by :newcontrib:`Seyed Yahya Shirazi` diff --git a/doc/changes/devel/12526.bugfix.rst b/doc/changes/devel/12526.bugfix.rst deleted file mode 100644 index b853cdc751a..00000000000 --- a/doc/changes/devel/12526.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Correct reading of ``info["subject_info"]["his_id"]`` in :func:`mne.io.read_raw_snirf`, by `Eric Larson`_. diff --git a/doc/changes/devel/12535.bugfix.rst b/doc/changes/devel/12535.bugfix.rst deleted file mode 100644 index eeeda0bffac..00000000000 --- a/doc/changes/devel/12535.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Calling :meth:`~mne.io.Raw.compute_psd` with ``method="multitaper"`` is now expressly disallowed when ``reject_by_annotation=True`` and ``bad_*`` annotations are present (previously this was nominally allowed but resulted in ``nan`` values in the PSD). By `Daniel McCloy`_. diff --git a/doc/changes/devel/12536.bugfix.rst b/doc/changes/devel/12536.bugfix.rst deleted file mode 100644 index 2b4a709186f..00000000000 --- a/doc/changes/devel/12536.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -:meth:`~mne.io.Raw.compute_psd` and :func:`~mne.time_frequency.psd_array_welch` will now use FFT windows aligned to the onsets of good data spans when ``bad_*`` annotations are present. By `Daniel McCloy`_. diff --git a/doc/changes/devel/12537.bugfix.rst b/doc/changes/devel/12537.bugfix.rst deleted file mode 100644 index 911bdce444e..00000000000 --- a/doc/changes/devel/12537.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix bug in loading of complex/phase TFRs. By `Daniel McCloy`_. diff --git a/doc/changes/devel/12544.bugfix.rst b/doc/changes/devel/12544.bugfix.rst deleted file mode 100644 index d6e3210ec45..00000000000 --- a/doc/changes/devel/12544.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix bug with :func:`mne.SourceSpaces.export_volume` where the ``img.affine`` was not set properly, by `Eric Larson`_. \ No newline at end of file diff --git a/doc/changes/v0.24.rst b/doc/changes/v0.24.rst index 1eb4abd2193..5f92e3dbdf6 100644 --- a/doc/changes/v0.24.rst +++ b/doc/changes/v0.24.rst @@ -89,7 +89,7 @@ Enhancements - :func:`mne.concatenate_raws`, :func:`mne.concatenate_epochs`, and :func:`mne.write_evokeds` gained a new parameter ``on_mismatch``, which controls behavior in case not all of the supplied instances share the same device-to-head transformation (:gh:`9438` by `Richard Höchenberger`_) -- Add support for multiple datablocks (acquistions with pauses) in :func:`mne.io.read_raw_nihon` (:gh:`9437` by `Federico Raimondo`_) +- Add support for multiple datablocks (acquisitions with pauses) in :func:`mne.io.read_raw_nihon` (:gh:`9437` by `Federico Raimondo`_) - Add new function :func:`mne.preprocessing.annotate_break` to automatically detect and mark "break" periods without any marked experimental events in the continuous data (:gh:`9445` by `Richard Höchenberger`_) diff --git a/doc/changes/v1.7.rst b/doc/changes/v1.7.rst new file mode 100644 index 00000000000..e8f8e2e8e7b --- /dev/null +++ b/doc/changes/v1.7.rst @@ -0,0 +1,180 @@ +.. _changes_1_7_0: + +1.7.0 (2024-04-19) +================== + +Notable changes +--------------- + +- In this version, we started adding type hints (also known as "type annotations") to select parts of the codebase. + This meta information will be used by development environments (IDEs) like VS Code and PyCharm automatically to provide + better assistance such as tab completion or error detection even before running your code. + + So far, we've only added return type hints to :func:`mne.io.read_raw`, :func:`mne.read_epochs`, :func:`mne.read_evokeds` and + all format-specific ``read_raw_*()`` and ``read_epochs_*()`` functions. Now your editors will know: + these functions return evoked and raw data, respectively. We are planning add type hints to more functions after careful + evaluation in the future. + + You don't need to do anything to benefit from these changes – your editor will pick them up automatically and provide the + enhanced experience if it supports it! (`#12250 `__) + + +Dependencies +------------ + +- ``defusedxml`` is now an optional (rather than required) dependency and needed when reading EGI-MFF data, NEDF data, and BrainVision montages, by `Eric Larson`_. (`#12264 `__) +- For developers, ``pytest>=8.0`` is now required for running unit tests, by `Eric Larson`_. (`#12376 `__) +- ``pytest-harvest`` is no longer used as a test dependency, by `Eric Larson`_. (`#12451 `__) +- The minimum supported version of Qt bindings is 5.15, by `Eric Larson`_. (`#12491 `__) + + +Bugfixes +-------- + +- Fix bug where section parameter in :meth:`mne.Report.add_html` was not being utilized resulting in improper formatting, by :newcontrib:`Martin Oberg`. (`#12319 `__) +- Fix bug in :func:`mne.preprocessing.maxwell_filter` where calibration was incorrectly applied during virtual sensor reconstruction, by `Eric Larson`_ and :newcontrib:`Motofumi Fushimi`. (`#12348 `__) +- Reformats channel and detector lookup in :func:`mne.io.read_raw_snirf` from array based to dictionary based. Removes incorrect assertions that every detector and source must have data associated with every registered optode position, by :newcontrib:`Alex Kiefer`. (`#12430 `__) +- Remove FDT file format check for strings in EEGLAB's EEG.data in :func:`mne.io.read_raw_eeglab` and related functions by :newcontrib:`Seyed Yahya Shirazi` (`#12523 `__) +- Fixes to interactivity in time-frequency objects: the rectangle selector now works on TFR image plots of gradiometer data; and in ``TFR.plot_joint()`` plots, the colormap limits of interactively-generated topomaps match the colormap limits of the main plot. By `Daniel McCloy`_. (`#11282 `__) +- Allow :func:`mne.viz.plot_compare_evokeds` to plot eyetracking channels, and improve error handling, y `Scott Huberty`_. (`#12190 `__) +- Fix bug in :meth:`mne.Epochs.apply_function` where data was handed down incorrectly in parallel processing, by `Dominik Welke`_. (`#12206 `__) +- Remove incorrect type hints in :func:`mne.io.read_raw_neuralynx`, by `Richard Höchenberger`_. (`#12236 `__) +- Fix bug with accessing the last data sample using ``raw[:, -1]`` where an empty array was returned, by `Eric Larson`_. (`#12248 `__) +- Correctly handle temporal gaps in Neuralynx .ncs files via :func:`mne.io.read_raw_neuralynx`, by `Kristijan Armeni`_ and `Eric Larson`_. (`#12279 `__) +- Fix bug where parent directory existence was not checked properly in :meth:`mne.io.Raw.save`, by `Eric Larson`_. (`#12282 `__) +- Add ``tol`` parameter to :meth:`mne.events_from_annotations` so that the user can specify the tolerance to ignore rounding errors of event onsets when using ``chunk_duration`` is not None (default is 1e-8), by `Michiru Kaneda`_ (`#12324 `__) +- Allow :meth:`mne.io.Raw.interpolate_bads` and :meth:`mne.Epochs.interpolate_bads` to work on ``ecog`` and ``seeg`` data; for ``seeg`` data a spline is fit to neighboring electrode contacts on the same shaft, by `Alex Rockhill`_ (`#12336 `__) +- Fix clicking on an axis of :func:`mne.viz.plot_evoked_topo` when multiple vertical lines ``vlines`` are used, by `Mathieu Scheltienne`_. (`#12345 `__) +- Fix bug in :meth:`mne.viz.EvokedField.set_vmax` that prevented setting the color limits of the MEG magnetic field density, by `Marijn van Vliet`_ (`#12354 `__) +- Fix faulty indexing in :func:`mne.io.read_raw_neuralynx` when picking a single channel, by `Kristijan Armeni`_. (`#12357 `__) +- Fix bug where :func:`mne.preprocessing.compute_proj_ecg` and :func:`mne.preprocessing.compute_proj_eog` could modify the default ``reject`` and ``flat`` arguments on multiple calls based on channel types present, by `Eric Larson`_. (`#12380 `__) +- Fix bad channels not handled properly in :func:`mne.stc_near_sensors` by `Alex Rockhill`_. (`#12382 `__) +- Fix bug where :func:`mne.preprocessing.regress_artifact` projection check was not specific to the channels being processed, by `Eric Larson`_. (`#12389 `__) +- Change how samples are read when using ``data_format='auto'`` in :func:`mne.io.read_raw_cnt`, by `Jacob Woessner`_. (`#12393 `__) +- Fix bugs with :class:`mne.Report` CSS where TOC items could disappear at the bottom of the page, by `Eric Larson`_. (`#12399 `__) +- In :func:`~mne.viz.plot_compare_evokeds`, actually plot GFP (not RMS amplitude) for EEG channels when global field power is requested by `Daniel McCloy`_. (`#12410 `__) +- Fix :ref:`tut-working-with-seeg` use of :func:`mne.stc_near_sensors` to use the :class:`mne.VolSourceEstimate` positions and not the pial surface, by `Alex Rockhill`_ (`#12436 `__) +- Fix prefiltering information management for EDF/BDF, by `Michiru Kaneda`_ (`#12441 `__) +- Fix validation of ``ch_type`` in :func:`mne.preprocessing.annotate_muscle_zscore`, by `Mathieu Scheltienne`_. (`#12444 `__) +- Fix errant redundant use of ``BIDSPath.split`` when writing split raw and epochs data, by `Eric Larson`_. (`#12451 `__) +- Disable config parser interpolation when reading BrainVision files, which allows using the percent sign as a regular character in channel units, by `Clemens Brunner`_. (`#12456 `__) +- - Fix the default color of :meth:`mne.viz.Brain.add_text` to properly contrast with the figure background color, by `Marijn van Vliet`_. (`#12470 `__) +- - Changed default ECoG and sEEG electrode sizes in brain plots to better reflect real world sizes, by `Liberty Hamilton`_ (`#12474 `__) +- Fixed bugs with handling of rank in :class:`mne.decoding.CSP`, by `Eric Larson`_. (`#12476 `__) +- - Fix reading segmented recordings with :func:`mne.io.read_raw_eyelink` by `Dominik Welke`_. (`#12481 `__) +- Improve compatibility with other Qt-based GUIs by handling theme icons better, by `Eric Larson`_. (`#12483 `__) +- - Fix problem caused by onsets with NaN values using :func:`mne.io.read_raw_eeglab` by `Jacob Woessner`_ (`#12484 `__) +- Fix cleaning of channel names for non vectorview or CTF dataset including whitespaces or dash in their channel names, by `Mathieu Scheltienne`_. (`#12489 `__) +- Fix bug with :meth:`mne.preprocessing.ICA.plot_sources` for ``evoked`` data where the + legend contained too many entries, by `Eric Larson`_. (`#12498 `__) +- Fix bug where using ``phase="minimum"`` in filtering functions like + :meth:`mne.io.Raw.filter` constructed a filter half the desired length with + compromised attenuation. Now ``phase="minimum"`` has the same length and comparable + suppression as ``phase="zero"``, and the old (incorrect) behavior can be achieved + with ``phase="minimum-half"``, by `Eric Larson`_. (`#12507 `__) +- Correct reading of ``info["subject_info"]["his_id"]`` in :func:`mne.io.read_raw_snirf`, by `Eric Larson`_. (`#12526 `__) +- Calling :meth:`~mne.io.Raw.compute_psd` with ``method="multitaper"`` is now expressly disallowed when ``reject_by_annotation=True`` and ``bad_*`` annotations are present (previously this was nominally allowed but resulted in ``nan`` values in the PSD). By `Daniel McCloy`_. (`#12535 `__) +- :meth:`~mne.io.Raw.compute_psd` and :func:`~mne.time_frequency.psd_array_welch` will now use FFT windows aligned to the onsets of good data spans when ``bad_*`` annotations are present. By `Daniel McCloy`_. (`#12536 `__) +- Fix bug in loading of complex/phase TFRs. By `Daniel McCloy`_. (`#12537 `__) +- Fix bug with :func:`mne.SourceSpaces.export_volume` where the ``img.affine`` was not set properly, by `Eric Larson`_. (`#12544 `__) + + +API changes by deprecation +-------------------------- + +- The default value of the ``zero_mean`` parameter of :func:`mne.time_frequency.tfr_array_morlet` will change from ``False`` to ``True`` in version 1.8, for consistency with related functions. By `Daniel McCloy`_. (`#11282 `__) +- The parameter for providing data to :func:`mne.time_frequency.tfr_array_morlet` and :func:`mne.time_frequency.tfr_array_multitaper` has been switched from ``epoch_data`` to ``data``. Only use the ``data`` parameter to avoid a warning. Changes by `Thomas Binns`_. (`#12308 `__) +- Change :func:`mne.stc_near_sensors` ``surface`` default from the ``'pial'`` surface to the surface in ``src`` if ``src`` is not ``None`` in version 1.8, by `Alex Rockhill`_. (`#12382 `__) + + +New features +------------ + +- Detecting Bad EEG/MEG channels using the local outlier factor (LOF) algorithm in :func:`mne.preprocessing.find_bad_channels_lof`, by :newcontrib:`Velu Prabhakar Kumaravel`. (`#11234 `__) +- Inform the user about channel discrepancy between provided info, forward operator, and/or covariance matrices in :func:`mne.beamformer.make_lcmv`, by :newcontrib:`Nikolai Kapralov`. (`#12238 `__) +- Support partial pathlength factors for each wavelength in :func:`mne.preprocessing.nirs.beer_lambert_law`, by :newcontrib:`Richard Scholz`. (`#12446 `__) +- Add ``picks`` parameter to :meth:`mne.io.Raw.plot`, allowing users to select which channels to plot. This makes makes the raw data plotting API consistent with :meth:`mne.Epochs.plot` and :meth:`mne.Evoked.plot`, by :newcontrib:`Ivo de Jong`. (`#12467 `__) +- New class :class:`mne.time_frequency.RawTFR` and new methods :meth:`mne.io.Raw.compute_tfr`, :meth:`mne.Epochs.compute_tfr`, and :meth:`mne.Evoked.compute_tfr`. These new methods supersede functions :func:`mne.time_frequency.tfr_morlet`, and :func:`mne.time_frequency.tfr_multitaper`, and :func:`mne.time_frequency.tfr_stockwell`, which are now considered "legacy" functions. By `Daniel McCloy`_. (`#11282 `__) +- Add ability reject :class:`mne.Epochs` using callables, by `Jacob Woessner`_. (`#12195 `__) +- Custom functions applied via :meth:`mne.io.Raw.apply_function`, :meth:`mne.Epochs.apply_function` or :meth:`mne.Evoked.apply_function` can now use ``ch_idx`` or ``ch_name`` to get access to the currently processed channel during channel wise processing. +- :meth:`mne.Evoked.apply_function` can now also work on full data array instead of just channel wise, analogous to :meth:`mne.io.Raw.apply_function` and :meth:`mne.Epochs.apply_function`, by `Dominik Welke`_. (`#12206 `__) +- Allow :class:`mne.time_frequency.EpochsTFR` as input to :func:`mne.epochs.equalize_epoch_counts`, by `Carina Forster`_. (`#12207 `__) +- Speed up export to .edf in :func:`mne.export.export_raw` by using ``edfio`` instead of ``EDFlib-Python``. (`#12218 `__) +- Added a helper function :func:`mne.preprocessing.eyetracking.convert_units` to convert eyegaze data from pixel-on-screen values to radians of visual angle. Also added a helper function :func:`mne.preprocessing.eyetracking.get_screen_visual_angle` to get the visual angle that the participant screen subtends, by `Scott Huberty`_. (`#12237 `__) +- We added type hints for the return values of :func:`mne.read_evokeds` and :func:`mne.io.read_raw`. Development environments like VS Code or PyCharm will now provide more help when using these functions in your code. By `Richard Höchenberger`_ and `Eric Larson`_. (:gh:`12297`) (`#12250 `__) +- Add ``method="polyphase"`` to :meth:`mne.io.Raw.resample` and related functions to allow resampling using :func:`scipy.signal.upfirdn`, by `Eric Larson`_. (`#12268 `__) +- The package build backend was switched from ``setuptools`` to ``hatchling``. This will only affect users who build and install MNE-Python from source. By `Richard Höchenberger`_. (:gh:`12281`) (`#12269 `__) +- :meth:`mne.Annotations.to_data_frame` can now output different formats for the ``onset`` column: seconds, milliseconds, datetime objects, and timedelta objects. By `Daniel McCloy`_. (`#12289 `__) +- Add method :meth:`mne.SourceEstimate.save_as_surface` to allow saving GIFTI files from surface source estimates, by `Peter Molfese`_. (`#12309 `__) +- :class:`mne.Epochs` can now be constructed using :class:`mne.Annotations` stored in the ``raw`` object, by specifying ``events=None``. By `Alex Rockhill`_. (`#12311 `__) +- Add :meth:`~mne.SourceEstimate.savgol_filter`, :meth:`~mne.SourceEstimate.filter`, :meth:`~mne.SourceEstimate.apply_hilbert`, and :meth:`~mne.SourceEstimate.apply_function` methods to :class:`mne.SourceEstimate` and related classes, by `Hamza Abdelhedi`_. (`#12323 `__) +- Add ability to export STIM channels to EDF in :meth:`mne.io.Raw.export`, by `Clemens Brunner`_. (`#12332 `__) +- Speed up raw FIF reading when using small buffer sizes by `Eric Larson`_. (`#12343 `__) +- Speed up :func:`mne.io.read_raw_neuralynx` on large datasets with many gaps, by `Kristijan Armeni`_. (`#12371 `__) +- Add ability to detect minima peaks found in :class:`mne.Evoked` if data is all positive and maxima if data is all negative. (`#12383 `__) +- Add ability to remove bad marker coils in :func:`mne.io.read_raw_kit`, by `Judy D Zhu`_. (`#12394 `__) +- Add option to pass ``image_kwargs`` to :class:`mne.Report.add_epochs` to allow adjusting e.g. ``vmin`` and ``vmax`` of the epochs image in the report, by `Sophie Herbst`_. (`#12443 `__) +- Add support for multiple raw instances in :func:`mne.preprocessing.compute_average_dev_head_t` by `Eric Larson`_. (`#12445 `__) +- Completing PR 12453. Add option to pass ``image_kwargs`` per channel type to :class:`mne.Report.add_epochs`. (`#12454 `__) +- :func:`mne.epochs.make_metadata` now accepts strings as ``tmin`` and ``tmax`` parameter values, simplifying metadata creation based on time-varying events such as responses to a stimulus, by `Richard Höchenberger`_. (`#12462 `__) +- Include date of acquisition and filter parameters in ``raw.info`` for :func:`mne.io.read_raw_neuralynx` by `Kristijan Armeni`_. (`#12463 `__) +- Add ``physical_range="channelwise"`` to :meth:`mne.io.Raw.export` for exporting to EDF, which can improve amplitude resolution if individual channels vary greatly in their offsets, by `Clemens Brunner`_. (`#12510 `__) +- Added the ability to reorder report contents via :meth:`mne.Report.reorder` (with + helper to get contents with :meth:`mne.Report.get_contents`), by `Eric Larson`_. (`#12513 `__) +- Add ``exclude_after_unique`` option to :meth:`mne.io.read_raw_edf` and :meth:`mne.io.read_raw_edf` to search for exclude channels after making channels names unique, by `Michiru Kaneda`_ (`#12518 `__) + + +Other changes +------------- + +- Updated the text in the preprocessing tutorial to use :meth:`mne.io.Raw.pick` instead of the legacy :meth:`mne.io.Raw.pick_types`, by :newcontrib:`btkcodedev`. (`#12326 `__) +- Clarify in the :ref:`EEG referencing tutorial ` that an average reference projector ready is required for inverse modeling, by :newcontrib:`Nabil Alibou` (`#12420 `__) +- Fix dead links in ``README.rst`` documentation by :newcontrib:`Will Turner`. (`#12461 `__) +- Replacing percent format with f-strings format specifiers , by :newcontrib:`Hasrat Ali Arzoo`. (`#12464 `__) +- Adopted towncrier_ for changelog entries, by `Eric Larson`_. (`#12299 `__) +- Automate adding of PR number to towncrier stubs, by `Eric Larson`_. (`#12318 `__) +- Refresh code base to use Python 3.9 syntax using Ruff UP rules (pyupgrade), by `Clemens Brunner`_. (`#12358 `__) +- Move private data preparation functions for BrainVision export from ``pybv`` to ``mne``, by `Clemens Brunner`_. (`#12450 `__) +- Update the list of sensor types in docstrings, tutorials and the glossary by `Nabil Alibou`_. (`#12509 `__) + + +Authors +------- +* Alex Rockhill +* Alexander Kiefer+ +* Alexandre Gramfort +* Britta Westner +* Carina Forster +* Clemens Brunner +* Daniel McCloy +* Dominik Welke +* Eric Larson +* Erkka Heinila +* Florian Hofer +* Hamza Abdelhedi +* Hasrat Ali Arzoo+ +* Ivo de Jong+ +* Jacob Woessner +* Judy D Zhu +* Kristijan Armeni +* Liberty Hamilton +* Marijn van Vliet +* Martin Oberg+ +* Mathieu Scheltienne +* Michiru Kaneda +* Motofumi Fushimi+ +* Nabil Alibou+ +* Nikolai Kapralov+ +* Peter J. Molfese +* Richard Höchenberger +* Richard Scholz+ +* Scott Huberty +* Seyed (Yahya) Shirazi+ +* Sophie Herbst +* Stefan Appelhoff +* Thomas Donoghue +* Thomas Samuel Binns +* Tristan Stenner +* Velu Prabhakar Kumaravel+ +* Will Turner+ +* btkcodedev+ diff --git a/doc/development/whats_new.rst b/doc/development/whats_new.rst index 61c14a876f9..920194e7fb2 100644 --- a/doc/development/whats_new.rst +++ b/doc/development/whats_new.rst @@ -8,7 +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 ../changes/v1.4.rst diff --git a/doc/documentation/cited.rst b/doc/documentation/cited.rst index 7654cf3fd40..31c19589b16 100644 --- a/doc/documentation/cited.rst +++ b/doc/documentation/cited.rst @@ -3,7 +3,7 @@ Papers citing MNE-Python ======================== -Estimates provided by Google Scholar as of 14 August 2023: +Estimates provided by Google Scholar as of 19 April 2024: -- `MNE (1540) `_ -- `MNE-Python (2040) `_ +- `MNE (1730) `_ +- `MNE-Python (2570) `_ diff --git a/examples/decoding/decoding_spoc_CMC.py b/examples/decoding/decoding_spoc_CMC.py index 4d49fb1e350..0a02a61052c 100644 --- a/examples/decoding/decoding_spoc_CMC.py +++ b/examples/decoding/decoding_spoc_CMC.py @@ -64,7 +64,7 @@ # Define a two fold cross-validation cv = KFold(n_splits=2, shuffle=False) -# Run cross validaton +# Run cross validation y_preds = cross_val_predict(clf, X, y, cv=cv) # Plot the True EMG power and the EMG power predicted from MEG data diff --git a/mne/commands/mne_browse_raw.py b/mne/commands/mne_browse_raw.py index 0c3d81a16e9..2e662e1768b 100644 --- a/mne/commands/mne_browse_raw.py +++ b/mne/commands/mne_browse_raw.py @@ -84,7 +84,7 @@ def run(): "-p", "--preload", dest="preload", - help="Preload raw data (for faster navigaton)", + help="Preload raw data (for faster navigation)", default=False, action="store_true", ) diff --git a/mne/datasets/sleep_physionet/tests/test_physionet.py b/mne/datasets/sleep_physionet/tests/test_physionet.py index 7cf57632057..5147be94ab9 100644 --- a/mne/datasets/sleep_physionet/tests/test_physionet.py +++ b/mne/datasets/sleep_physionet/tests/test_physionet.py @@ -30,8 +30,8 @@ def _keep_basename_only(paths): def _get_expected_url(name): base = "https://physionet.org/physiobank/database/sleep-edfx/" - midle = "sleep-cassette/" if name.startswith("SC") else "sleep-telemetry/" - return base + midle + "/" + name + middle = "sleep-cassette/" if name.startswith("SC") else "sleep-telemetry/" + return base + middle + "/" + name def _get_expected_path(base, name): diff --git a/mne/fixes.py b/mne/fixes.py index 98b1ce805cd..f7534377b5a 100644 --- a/mne/fixes.py +++ b/mne/fixes.py @@ -113,7 +113,7 @@ def _csc_matrix_cast(x): def rng_uniform(rng): - """Get the unform/randint from the rng.""" + """Get the uniform/randint from the rng.""" # prefer Generator.integers, fall back to RandomState.randint return getattr(rng, "integers", getattr(rng, "randint", None)) diff --git a/mne/io/fiff/tests/test_raw_fiff.py b/mne/io/fiff/tests/test_raw_fiff.py index 91125de98be..dc3c732979d 100644 --- a/mne/io/fiff/tests/test_raw_fiff.py +++ b/mne/io/fiff/tests/test_raw_fiff.py @@ -1292,7 +1292,7 @@ def test_crop(): assert raw1[:][0].shape == (1, 2001) # degenerate - with pytest.raises(ValueError, match="No samples.*when include_tmax=Fals"): + with pytest.raises(ValueError, match="No samples.*when include_tmax=False"): raw.crop(0, 0, include_tmax=False) # edge cases cropping to exact duration +/- 1 sample diff --git a/mne/io/kit/coreg.py b/mne/io/kit/coreg.py index 0887a4b4022..3e691249790 100644 --- a/mne/io/kit/coreg.py +++ b/mne/io/kit/coreg.py @@ -72,7 +72,7 @@ def read_mrk(fname): elif fname.suffix == ".pickled": warn( "Reading pickled files is unsafe and not future compatible, save " - "to a standard format (text or FIF) instea, e.g. with:\n" + "to a standard format (text or FIF) instead, e.g. with:\n" r"np.savetxt(fid, pts, delimiter=\"\\t\", newline=\"\\n\")", FutureWarning, ) diff --git a/mne/preprocessing/_peak_finder.py b/mne/preprocessing/_peak_finder.py index 99272ae0fda..078e4aadb23 100644 --- a/mne/preprocessing/_peak_finder.py +++ b/mne/preprocessing/_peak_finder.py @@ -85,7 +85,7 @@ def peak_finder(x0, thresh=None, extrema=1, verbose=None): left_min = min_mag # Deal with first point a little differently since tacked it on - # Calculate the sign of the derivative since we taked the first point + # Calculate the sign of the derivative since we took the first point # on it does not necessarily alternate like the rest. signDx = np.sign(np.diff(x[:3])) if signDx[0] <= 0: # The first point is larger or equal to the second diff --git a/tools/generate_codemeta.py b/tools/generate_codemeta.py index 9e697cecc55..a1c1fac77b4 100644 --- a/tools/generate_codemeta.py +++ b/tools/generate_codemeta.py @@ -44,6 +44,7 @@ "De Santis", "Dupré la Tour", "de la Torre", + "de Jong", "de Montalivet", "van den Bosch", "Van den Bossche", From 156ae18572c8322ae75c964258f3fdc607006587 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Fri, 19 Apr 2024 14:17:08 -0400 Subject: [PATCH 276/405] 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 277/405] 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 278/405] 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 279/405] 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 280/405] 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 281/405] 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 282/405] 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 283/405] [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 284/405] 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 285/405] 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 286/405] 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 287/405] 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 288/405] 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 289/405] 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 290/405] 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 291/405] 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 292/405] 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 293/405] 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 294/405] 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 295/405] 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 296/405] 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 297/405] 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 298/405] 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 299/405] 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 300/405] 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 301/405] [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 302/405] 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 303/405] 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 304/405] 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 305/405] 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 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 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 306/405] 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 307/405] 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 308/405] 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 309/405] 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 310/405] 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 311/405] [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 312/405] 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 313/405] 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 314/405] 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 315/405] 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 316/405] 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 317/405] 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 318/405] 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 319/405] 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 320/405] 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 321/405] 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 322/405] 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 323/405] 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
-

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

- - {{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("