From 219e22bbaeaa69cfa757d785997caf6278e233fd Mon Sep 17 00:00:00 2001 From: AtomicGlance Date: Sun, 6 Sep 2026 11:17:29 +0330 Subject: [PATCH 1/4] BUG: use trial count for automatic CTPS threshold --- doc/changes/dev/14279.bugfix.rst | 1 + doc/changes/names.inc | 1 + mne/preprocessing/ica.py | 65 +++++++++++++++++++++-------- mne/preprocessing/tests/test_ica.py | 33 ++++++++++++++- 4 files changed, 82 insertions(+), 18 deletions(-) create mode 100644 doc/changes/dev/14279.bugfix.rst diff --git a/doc/changes/dev/14279.bugfix.rst b/doc/changes/dev/14279.bugfix.rst new file mode 100644 index 00000000000..1b0e76bcd62 --- /dev/null +++ b/doc/changes/dev/14279.bugfix.rst @@ -0,0 +1 @@ +Use the number of accepted trials, rather than the sampling frequency, when computing the automatic CTPS ECG threshold, by :newcontrib:`Amirali Moradniaei`. diff --git a/doc/changes/names.inc b/doc/changes/names.inc index d28e1b19c9f..9098a450a48 100644 --- a/doc/changes/names.inc +++ b/doc/changes/names.inc @@ -20,6 +20,7 @@ .. _Alexandra Corneyllie: https://github.com/Lx37 .. _Alexandre Barachant: https://alexandre.barachant.org .. _Aman Srivastava: https://github.com/aman-coder03 +.. _Amirali Moradniaei: https://github.com/AtomicGlance .. _Ana Radanovic: https://github.com/anaradanovic .. _Andrea Brovelli: https://brovelli.github.io/ .. _Andreas Højlund: https://github.com/ahoejlund diff --git a/mne/preprocessing/ica.py b/mne/preprocessing/ica.py index c8aebe7dbc5..c18723456a5 100644 --- a/mne/preprocessing/ica.py +++ b/mne/preprocessing/ica.py @@ -118,6 +118,36 @@ def sfunc(x, y, ndim_output=ndim_output): return sfunc +def _compute_ctps_threshold(n_trials, pk_threshold=20): + """Compute the normalized Kuiper-index threshold for CTPS. + + Parameters + ---------- + n_trials : int + Number of trials used to compute the cross-trial phase statistics. + pk_threshold : float + Significance threshold expressed as ``-log10(Pk)``. + + Returns + ------- + threshold : float + Kuiper-index threshold corresponding to ``pk_threshold``. + """ + if not isinstance(n_trials, Integral): + raise TypeError("n_trials must be an integer") + if n_trials < 1: + raise ValueError("n_trials must be at least 1") + + Vs = np.arange(1, 100) / 100 + C = math.sqrt(n_trials) + 0.155 + 0.24 / math.sqrt(n_trials) + # In formula (13), when k gets large, only k=1 matters for the + # summation. k*V*C thus becomes V*C. + Pks = 2 * (4 * (Vs * C) ** 2 - 1) * (np.exp(-2 * (Vs * C) ** 2)) + # NOTE: the threshold of pk is transformed to Pk for comparison: + # pk = -log10(Pk). + return Vs[np.argmin(np.abs(Pks - 10 ** (-pk_threshold)))] + + # Violate our assumption that the output is 1D so can't be used. # Could eventually be added but probably not worth the effort unless someone # requests it. @@ -1617,29 +1647,29 @@ def _find_bads_ch( return labels, scores - def _get_ctps_threshold(self, pk_threshold=20): + def _get_ctps_threshold(self, n_trials, pk_threshold=20): """Automatically decide the threshold of Kuiper index for CTPS method. This function finds the threshold of Kuiper index based on the threshold of pk. Kuiper statistic that minimizes the difference between pk and the pk threshold (defaults to 20 :footcite:`DammersEtAl2008`) - is returned. It is assumed that the data are appropriately filtered and - bad data are rejected at least based on peak-to-peak amplitude - when/before running the ICA decomposition on data. + is returned. ``n_trials`` is the number of trials used by CTPS. It is + assumed that the data are appropriately filtered and bad data are + rejected at least based on peak-to-peak amplitude when/before running + the ICA decomposition on data. + + Parameters + ---------- + n_trials : int + Number of trials used to compute the cross-trial phase statistics. + pk_threshold : float + Significance threshold expressed as ``-log10(Pk)``. References ---------- .. footbibliography:: """ - N = self.info["sfreq"] - Vs = np.arange(1, 100) / 100 - C = math.sqrt(N) + 0.155 + 0.24 / math.sqrt(N) - # in formula (13), when k gets large, only k=1 matters for the - # summation. k*V*C thus becomes V*C - Pks = 2 * (4 * (Vs * C) ** 2 - 1) * (np.exp(-2 * (Vs * C) ** 2)) - # NOTE: the threshold of pk is transformed to Pk for comparison - # pk = -log10(Pk) - return Vs[np.argmin(np.abs(Pks - 10 ** (-pk_threshold)))] + return _compute_ctps_threshold(n_trials, pk_threshold) @verbose def find_bads_ecg( @@ -1716,7 +1746,8 @@ def find_bads_ecg( - If ``method='ctps'``, ``threshold`` refers to the significance value of a Kuiper statistic, and ``threshold='auto'`` will compute the - threshold automatically based on the sampling frequency. + threshold automatically based on the number of trials supplied to + CTPS. - If ``method='correlation'`` and ``measure='correlation'``, ``threshold`` refers to the Pearson correlation value, and ``threshold='auto'`` sets the threshold to 0.9. @@ -1747,9 +1778,6 @@ def find_bads_ecg( ecg = inst.ch_names[idx_ecg] if method == "ctps": - if threshold == "auto": - threshold = self._get_ctps_threshold() - logger.info(f"Using threshold: {threshold:.2f} for CTPS ECG detection") if isinstance(inst, BaseRaw): sources = self.get_sources( create_ecg_epochs( @@ -1771,6 +1799,9 @@ def find_bads_ecg( sources = self.get_sources(inst).get_data(copy=False) else: raise ValueError("With `ctps` only Raw and Epochs input is supported") + if threshold == "auto": + threshold = self._get_ctps_threshold(sources.shape[0]) + logger.info(f"Using threshold: {threshold:.2f} for CTPS ECG detection") _, p_vals, _ = ctps(sources) scores = p_vals.max(-1) ecg_idx = np.where(scores >= threshold)[0] diff --git a/mne/preprocessing/tests/test_ica.py b/mne/preprocessing/tests/test_ica.py index d095154936c..bc03a131b66 100644 --- a/mne/preprocessing/tests/test_ica.py +++ b/mne/preprocessing/tests/test_ica.py @@ -49,6 +49,7 @@ read_ica, ) from mne.preprocessing.ica import ( + _compute_ctps_threshold, _ica_explained_variance, _sort_components, corrmap, @@ -762,6 +763,36 @@ def short_raw_epochs(): return raw, epochs, epochs_eog +def test_ctps_threshold_uses_trial_count(monkeypatch): + """Check CTPS auto threshold uses the number of input epochs.""" + assert_allclose(_compute_ctps_threshold(100), 0.5) + assert _compute_ctps_threshold(100) > _compute_ctps_threshold(400) + with pytest.raises(ValueError, match="at least 1"): + _compute_ctps_threshold(0) + with pytest.raises(TypeError, match="integer"): + _compute_ctps_threshold(1.5) + + info = create_info(["EEG 001", "ECG"], 100.0, ["eeg", "ecg"]) + events = np.column_stack([np.arange(4), np.zeros(4, int), np.ones(4, int)]) + epochs = EpochsArray(np.zeros((4, 2, 20)), info, events) + ica = _ICA(n_components=2, random_state=0) + sources = MagicMock() + sources.get_data.return_value = np.zeros((4, 2, 20)) + monkeypatch.setattr(ica, "get_sources", lambda inst: sources) + monkeypatch.setattr( + "mne.preprocessing.ica.ctps", + lambda data: (np.zeros((2, 20)), np.zeros((2, 20)), None), + ) + seen_trials = [] + monkeypatch.setattr( + ica, + "_get_ctps_threshold", + lambda n_trials: seen_trials.append(n_trials) or 0.5, + ) + ica.find_bads_ecg(epochs, ch_name="ECG", threshold="auto") + assert seen_trials == [4] + + @pytest.mark.slowtest @pytest.mark.parametrize("method", ["picard", "fastica"]) def test_ica_additional(method, tmp_path, short_raw_epochs): @@ -794,7 +825,7 @@ def test_ica_additional(method, tmp_path, short_raw_epochs): _assert_ica_attributes(ica, raw.get_data(np.arange(1, 6))) # check Kuiper index threshold - assert_allclose(ica._get_ctps_threshold(), 0.5) + assert_allclose(ica._get_ctps_threshold(n_trials=100), 0.5) with pytest.raises(TypeError, match="str or numeric"): ica.find_bads_ecg(raw, threshold=None) with pytest.warns(RuntimeWarning, match="is longer than the signal"): From bcd2e2230d2360386ce6f2ca6b70c54f36acc5de Mon Sep 17 00:00:00 2001 From: AtomicGlance Date: Wed, 9 Sep 2026 00:23:14 +0330 Subject: [PATCH 2/4] FIX: compare CTPS thresholds in normalized units --- mne/preprocessing/ica.py | 21 +++++++++++-------- mne/preprocessing/tests/test_ica.py | 32 ++++++++++------------------- 2 files changed, 23 insertions(+), 30 deletions(-) diff --git a/mne/preprocessing/ica.py b/mne/preprocessing/ica.py index c18723456a5..4c3e25cf6f3 100644 --- a/mne/preprocessing/ica.py +++ b/mne/preprocessing/ica.py @@ -87,7 +87,7 @@ warn, ) from .bads import _find_outliers -from .ctps_ import ctps +from .ctps_ import _prob_kuiper, 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 @@ -126,12 +126,13 @@ def _compute_ctps_threshold(n_trials, pk_threshold=20): n_trials : int Number of trials used to compute the cross-trial phase statistics. pk_threshold : float - Significance threshold expressed as ``-log10(Pk)``. + Significance threshold expressed as ``-log10(Pk)`` before normalizing + by the number of trials. Returns ------- threshold : float - Kuiper-index threshold corresponding to ``pk_threshold``. + Normalized Kuiper-index threshold corresponding to ``pk_threshold``. """ if not isinstance(n_trials, Integral): raise TypeError("n_trials must be an integer") @@ -145,7 +146,11 @@ def _compute_ctps_threshold(n_trials, pk_threshold=20): Pks = 2 * (4 * (Vs * C) ** 2 - 1) * (np.exp(-2 * (Vs * C) ** 2)) # NOTE: the threshold of pk is transformed to Pk for comparison: # pk = -log10(Pk). - return Vs[np.argmin(np.abs(Pks - 10 ** (-pk_threshold)))] + # Formula (13) gives a threshold for the raw Kuiper statistic V. Convert + # it to the normalized p_K score returned by ``ctps`` before comparing it + # with the scores in ``ICA.find_bads_ecg``. + v_threshold = Vs[np.argmin(np.abs(Pks - 10 ** (-pk_threshold)))] + return _prob_kuiper(v_threshold, n_trials).item() # Violate our assumption that the output is 1D so can't be used. @@ -1650,11 +1655,9 @@ def _find_bads_ch( def _get_ctps_threshold(self, n_trials, pk_threshold=20): """Automatically decide the threshold of Kuiper index for CTPS method. - This function finds the threshold of Kuiper index based on the - threshold of pk. Kuiper statistic that minimizes the difference between - pk and the pk threshold (defaults to 20 :footcite:`DammersEtAl2008`) - is returned. ``n_trials`` is the number of trials used by CTPS. It is - assumed that the data are appropriately filtered and bad data are + This function finds the normalized Kuiper-index threshold based on the + threshold of pk. ``n_trials`` is the number of trials used by CTPS. It + is assumed that the data are appropriately filtered and bad data are rejected at least based on peak-to-peak amplitude when/before running the ICA decomposition on data. diff --git a/mne/preprocessing/tests/test_ica.py b/mne/preprocessing/tests/test_ica.py index bc03a131b66..f792430a5ee 100644 --- a/mne/preprocessing/tests/test_ica.py +++ b/mne/preprocessing/tests/test_ica.py @@ -763,34 +763,24 @@ def short_raw_epochs(): return raw, epochs, epochs_eog -def test_ctps_threshold_uses_trial_count(monkeypatch): +def test_ctps_threshold_uses_trial_count(short_raw_epochs): """Check CTPS auto threshold uses the number of input epochs.""" - assert_allclose(_compute_ctps_threshold(100), 0.5) + assert_allclose(_compute_ctps_threshold(100), 0.2324, rtol=1e-3) assert _compute_ctps_threshold(100) > _compute_ctps_threshold(400) with pytest.raises(ValueError, match="at least 1"): _compute_ctps_threshold(0) with pytest.raises(TypeError, match="integer"): _compute_ctps_threshold(1.5) - info = create_info(["EEG 001", "ECG"], 100.0, ["eeg", "ecg"]) - events = np.column_stack([np.arange(4), np.zeros(4, int), np.ones(4, int)]) - epochs = EpochsArray(np.zeros((4, 2, 20)), info, events) - ica = _ICA(n_components=2, random_state=0) - sources = MagicMock() - sources.get_data.return_value = np.zeros((4, 2, 20)) - monkeypatch.setattr(ica, "get_sources", lambda inst: sources) - monkeypatch.setattr( - "mne.preprocessing.ica.ctps", - lambda data: (np.zeros((2, 20)), np.zeros((2, 20)), None), - ) - seen_trials = [] - monkeypatch.setattr( - ica, - "_get_ctps_threshold", - lambda n_trials: seen_trials.append(n_trials) or 0.5, + _, epochs, _ = short_raw_epochs + ica = _ICA(n_components=2, max_iter=1, random_state=0) + with _baseline_corrected: + ica.fit(epochs) + with catch_logging(True) as log: + ica.find_bads_ecg(epochs, threshold="auto") + assert ( + f"Using threshold: {_compute_ctps_threshold(len(epochs)):.2f}" in log.getvalue() ) - ica.find_bads_ecg(epochs, ch_name="ECG", threshold="auto") - assert seen_trials == [4] @pytest.mark.slowtest @@ -825,7 +815,7 @@ def test_ica_additional(method, tmp_path, short_raw_epochs): _assert_ica_attributes(ica, raw.get_data(np.arange(1, 6))) # check Kuiper index threshold - assert_allclose(ica._get_ctps_threshold(n_trials=100), 0.5) + assert_allclose(ica._get_ctps_threshold(n_trials=100), 0.2324, rtol=1e-3) with pytest.raises(TypeError, match="str or numeric"): ica.find_bads_ecg(raw, threshold=None) with pytest.warns(RuntimeWarning, match="is longer than the signal"): From a482858dad3053b3180b7efa9a0789799f7b1aef Mon Sep 17 00:00:00 2001 From: AtomicGlance Date: Wed, 9 Sep 2026 09:20:40 +0330 Subject: [PATCH 3/4] FIX: use a fixed normalized CTPS cutoff --- doc/changes/dev/14279.bugfix.rst | 2 +- mne/preprocessing/ica.py | 69 +++-------------------------- mne/preprocessing/tests/test_ica.py | 35 +++++++-------- 3 files changed, 23 insertions(+), 83 deletions(-) diff --git a/doc/changes/dev/14279.bugfix.rst b/doc/changes/dev/14279.bugfix.rst index 1b0e76bcd62..7617d1b15e6 100644 --- a/doc/changes/dev/14279.bugfix.rst +++ b/doc/changes/dev/14279.bugfix.rst @@ -1 +1 @@ -Use the number of accepted trials, rather than the sampling frequency, when computing the automatic CTPS ECG threshold, by :newcontrib:`Amirali Moradniaei`. +Use a fixed normalized Kuiper-index cutoff of 0.3 for automatic CTPS ECG detection in :meth:`mne.preprocessing.ICA.find_bads_ecg`, independent of sampling frequency and trial count, by :newcontrib:`Amirali Moradniaei`. diff --git a/mne/preprocessing/ica.py b/mne/preprocessing/ica.py index 4c3e25cf6f3..f171e874891 100644 --- a/mne/preprocessing/ica.py +++ b/mne/preprocessing/ica.py @@ -4,7 +4,6 @@ # Copyright the MNE-Python contributors. import json -import math import warnings from collections import namedtuple from collections.abc import Sequence @@ -87,7 +86,7 @@ warn, ) from .bads import _find_outliers -from .ctps_ import _prob_kuiper, ctps +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 @@ -118,41 +117,6 @@ def sfunc(x, y, ndim_output=ndim_output): return sfunc -def _compute_ctps_threshold(n_trials, pk_threshold=20): - """Compute the normalized Kuiper-index threshold for CTPS. - - Parameters - ---------- - n_trials : int - Number of trials used to compute the cross-trial phase statistics. - pk_threshold : float - Significance threshold expressed as ``-log10(Pk)`` before normalizing - by the number of trials. - - Returns - ------- - threshold : float - Normalized Kuiper-index threshold corresponding to ``pk_threshold``. - """ - if not isinstance(n_trials, Integral): - raise TypeError("n_trials must be an integer") - if n_trials < 1: - raise ValueError("n_trials must be at least 1") - - Vs = np.arange(1, 100) / 100 - C = math.sqrt(n_trials) + 0.155 + 0.24 / math.sqrt(n_trials) - # In formula (13), when k gets large, only k=1 matters for the - # summation. k*V*C thus becomes V*C. - Pks = 2 * (4 * (Vs * C) ** 2 - 1) * (np.exp(-2 * (Vs * C) ** 2)) - # NOTE: the threshold of pk is transformed to Pk for comparison: - # pk = -log10(Pk). - # Formula (13) gives a threshold for the raw Kuiper statistic V. Convert - # it to the normalized p_K score returned by ``ctps`` before comparing it - # with the scores in ``ICA.find_bads_ecg``. - v_threshold = Vs[np.argmin(np.abs(Pks - 10 ** (-pk_threshold)))] - return _prob_kuiper(v_threshold, n_trials).item() - - # Violate our assumption that the output is 1D so can't be used. # Could eventually be added but probably not worth the effort unless someone # requests it. @@ -1652,28 +1616,6 @@ def _find_bads_ch( return labels, scores - def _get_ctps_threshold(self, n_trials, pk_threshold=20): - """Automatically decide the threshold of Kuiper index for CTPS method. - - This function finds the normalized Kuiper-index threshold based on the - threshold of pk. ``n_trials`` is the number of trials used by CTPS. It - is assumed that the data are appropriately filtered and bad data are - rejected at least based on peak-to-peak amplitude when/before running - the ICA decomposition on data. - - Parameters - ---------- - n_trials : int - Number of trials used to compute the cross-trial phase statistics. - pk_threshold : float - Significance threshold expressed as ``-log10(Pk)``. - - References - ---------- - .. footbibliography:: - """ - return _compute_ctps_threshold(n_trials, pk_threshold) - @verbose def find_bads_ecg( self, @@ -1747,10 +1689,9 @@ def find_bads_ecg( The ``threshold``, ``method``, and ``measure`` parameters interact in the following ways: - - If ``method='ctps'``, ``threshold`` refers to the significance value - of a Kuiper statistic, and ``threshold='auto'`` will compute the - threshold automatically based on the number of trials supplied to - CTPS. + - If ``method='ctps'``, ``threshold`` refers to the maximum normalized + Kuiper index across time, and ``threshold='auto'`` sets the threshold + to 0.3, independent of the sampling frequency and number of trials. - If ``method='correlation'`` and ``measure='correlation'``, ``threshold`` refers to the Pearson correlation value, and ``threshold='auto'`` sets the threshold to 0.9. @@ -1803,7 +1744,7 @@ def find_bads_ecg( else: raise ValueError("With `ctps` only Raw and Epochs input is supported") if threshold == "auto": - threshold = self._get_ctps_threshold(sources.shape[0]) + threshold = 0.3 logger.info(f"Using threshold: {threshold:.2f} for CTPS ECG detection") _, p_vals, _ = ctps(sources) scores = p_vals.max(-1) diff --git a/mne/preprocessing/tests/test_ica.py b/mne/preprocessing/tests/test_ica.py index f792430a5ee..1217de4e053 100644 --- a/mne/preprocessing/tests/test_ica.py +++ b/mne/preprocessing/tests/test_ica.py @@ -49,7 +49,6 @@ read_ica, ) from mne.preprocessing.ica import ( - _compute_ctps_threshold, _ica_explained_variance, _sort_components, corrmap, @@ -763,24 +762,22 @@ def short_raw_epochs(): return raw, epochs, epochs_eog -def test_ctps_threshold_uses_trial_count(short_raw_epochs): - """Check CTPS auto threshold uses the number of input epochs.""" - assert_allclose(_compute_ctps_threshold(100), 0.2324, rtol=1e-3) - assert _compute_ctps_threshold(100) > _compute_ctps_threshold(400) - with pytest.raises(ValueError, match="at least 1"): - _compute_ctps_threshold(0) - with pytest.raises(TypeError, match="integer"): - _compute_ctps_threshold(1.5) - +@pytest.mark.parametrize("n_epochs", [2, 3]) +@pytest.mark.parametrize("sfreq", [50, 100]) +def test_ctps_auto_threshold(short_raw_epochs, n_epochs, sfreq): + """Check auto uses the same normalized cutoff for different inputs.""" _, epochs, _ = short_raw_epochs - ica = _ICA(n_components=2, max_iter=1, random_state=0) + epochs = epochs[:n_epochs].copy().resample(sfreq) + ica = _ICA(n_components=2, rng=0) with _baseline_corrected: ica.fit(epochs) with catch_logging(True) as log: - ica.find_bads_ecg(epochs, threshold="auto") - assert ( - f"Using threshold: {_compute_ctps_threshold(len(epochs)):.2f}" in log.getvalue() - ) + auto_idx, auto_scores = ica.find_bads_ecg(epochs, threshold="auto") + manual_idx, manual_scores = ica.find_bads_ecg(epochs, threshold=0.3) + assert "Using threshold: 0.30" in log.getvalue() + assert_array_equal(auto_scores, manual_scores) + assert_array_equal(auto_idx, manual_idx) + assert_array_equal(sorted(auto_idx), np.flatnonzero(auto_scores >= 0.3)) @pytest.mark.slowtest @@ -814,8 +811,6 @@ def test_ica_additional(method, tmp_path, short_raw_epochs): ica.fit(raw, np.arange(1, 6)) _assert_ica_attributes(ica, raw.get_data(np.arange(1, 6))) - # check Kuiper index threshold - assert_allclose(ica._get_ctps_threshold(n_trials=100), 0.2324, rtol=1e-3) with pytest.raises(TypeError, match="str or numeric"): ica.find_bads_ecg(raw, threshold=None) with pytest.warns(RuntimeWarning, match="is longer than the signal"): @@ -827,7 +822,11 @@ def test_ica_additional(method, tmp_path, short_raw_epochs): ) # check passing a ch_name to find_bads_ecg with pytest.warns(RuntimeWarning, match="longer"): - _, scores_1 = ica.find_bads_ecg(raw, threshold="auto") + auto_idx, scores_1 = ica.find_bads_ecg(raw, threshold="auto") + with pytest.warns(RuntimeWarning, match="longer"): + manual_idx, manual_scores = ica.find_bads_ecg(raw, threshold=0.3) + assert_array_equal(scores_1, manual_scores) + assert_array_equal(auto_idx, manual_idx) with pytest.warns(RuntimeWarning, match="longer"): _, scores_2 = ica.find_bads_ecg(raw, raw.ch_names[1], threshold="auto") assert scores_1[0] != scores_2[0] From 94ceb4fdfbad7f7fa6b1827f4974b228816c8a21 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Wed, 9 Sep 2026 07:45:59 -0400 Subject: [PATCH 4/4] Fix ElementTree --- mne/channels/_dig_montage_utils.py | 4 +++- mne/channels/_standard_montage_utils.py | 4 +++- mne/io/nedf/nedf.py | 4 +++- tutorials/preprocessing/40_artifact_correction_ica.py | 4 ++-- 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/mne/channels/_dig_montage_utils.py b/mne/channels/_dig_montage_utils.py index 31bee83648d..e574f8d27ab 100644 --- a/mne/channels/_dig_montage_utils.py +++ b/mne/channels/_dig_montage_utils.py @@ -19,7 +19,9 @@ 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) - defusedxml = _soft_import("defusedxml", "reading EGI montages") + _soft_import("defusedxml", "reading EGI montages") + import defusedxml.ElementTree + root = defusedxml.ElementTree.parse(fname).getroot() ns = root.tag[root.tag.index("{") : root.tag.index("}") + 1] sensors = root.find(f"{ns}sensorLayout/{ns}sensors") diff --git a/mne/channels/_standard_montage_utils.py b/mne/channels/_standard_montage_utils.py index b0e9aeebfb3..e77b10c78b6 100644 --- a/mne/channels/_standard_montage_utils.py +++ b/mne/channels/_standard_montage_utils.py @@ -423,7 +423,9 @@ 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 - defusedxml = _soft_import("defusedxml", "reading BrainVision montages") + _soft_import("defusedxml", "reading BrainVision montages") + import defusedxml.ElementTree + 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")] diff --git a/mne/io/nedf/nedf.py b/mne/io/nedf/nedf.py index 63242a51bee..77815a8f7d6 100644 --- a/mne/io/nedf/nedf.py +++ b/mne/io/nedf/nedf.py @@ -55,7 +55,9 @@ def _parse_nedf_header(header): n_samples : int The number of data samples. """ - defusedxml = _soft_import("defusedxml", "reading NEDF data") + _soft_import("defusedxml", "reading NEDF data") + import defusedxml.ElementTree # ty: ignore[unresolved-import] + info = {} # nedf files have three accelerometer channels sampled at 100Hz followed # by five EEG samples + TTL trigger sampled at 500Hz diff --git a/tutorials/preprocessing/40_artifact_correction_ica.py b/tutorials/preprocessing/40_artifact_correction_ica.py index 3d2fc6f7923..637bfe34887 100644 --- a/tutorials/preprocessing/40_artifact_correction_ica.py +++ b/tutorials/preprocessing/40_artifact_correction_ica.py @@ -444,8 +444,8 @@ # necessary to pass a specific channel name. # `~mne.preprocessing.ICA.find_bads_ecg` also has two options for its # ``method`` parameter: ``'ctps'`` (cross-trial phase statistics -# :footcite:`DammersEtAl2008`) and -# ``'correlation'`` (Pearson correlation between data and ECG channel). +# :footcite:`DammersEtAl2008`) and ``'correlation'`` (Pearson correlation +# between data and ECG channel). ica.exclude = [] # find which ICs match the ECG pattern