From ef9cadf26cff249a67cdb246c6b6f2f9e69f79ae Mon Sep 17 00:00:00 2001 From: Deepesh Sonar <18deepnar@gmail.com> Date: Sat, 26 Sep 2026 19:52:45 +0530 Subject: [PATCH 1/2] FIX: Exclude pre-existing bads from bridged interpolation interpolate_bridged_electrodes cleared inst.info['bads'] while building the interpolation, so channels already marked bad could contribute to the bridged interpolation without the caller knowing (gh-14263). Keep bridged channels as the interpolation targets, but pass pre-existing bad channels that are not part of the bridged set via interpolate_bads(exclude=...), so they are neither interpolation sources nor targets and their data is left untouched. Channels that are both bridged and marked bad remain interpolation targets, as before. --- doc/changes/dev/14287.bugfix.rst | 1 + mne/preprocessing/interpolate.py | 11 ++++- mne/preprocessing/tests/test_interpolate.py | 45 ++++++++++++++++++++- 3 files changed, 54 insertions(+), 3 deletions(-) create mode 100644 doc/changes/dev/14287.bugfix.rst diff --git a/doc/changes/dev/14287.bugfix.rst b/doc/changes/dev/14287.bugfix.rst new file mode 100644 index 00000000000..9854d07bbf9 --- /dev/null +++ b/doc/changes/dev/14287.bugfix.rst @@ -0,0 +1 @@ +Exclude pre-existing bad channels that are not bridged from :func:`~mne.preprocessing.interpolate_bridged_electrodes` interpolation, by `Deepesh Sonar`_. diff --git a/mne/preprocessing/interpolate.py b/mne/preprocessing/interpolate.py index c2f41a7ba28..ed7d10bcd5e 100644 --- a/mne/preprocessing/interpolate.py +++ b/mne/preprocessing/interpolate.py @@ -84,6 +84,10 @@ def interpolate_bridged_electrodes(inst, bridged_idx, bad_limit=4): that to aid in interpolation rather than completely discarding the data from the two channels. + Channels in ``inst.info["bads"]`` that are not part of the bridged + set are excluded from the interpolation: they are neither used to + compute the interpolated values nor interpolated themselves. + Parameters ---------- inst : instance of Epochs, Evoked, or Raw @@ -189,7 +193,12 @@ def interpolate_bridged_electrodes(inst, bridged_idx, bad_limit=4): # use the virtual channels to interpolate inst.info["bads"] = list(bads) - inst.interpolate_bads() + # Exclude pre-existing bad channels that are not part of the bridged set + # so they can neither contribute to the interpolation nor be interpolated + # themselves. Channels that are both bridged and marked bad remain + # interpolation targets, as before. + exclude_bads = [ch for ch in bads_orig if ch not in bads] + inst.interpolate_bads(exclude=exclude_bads) # drop virtual channels inst.drop_channels(list(virtual_chs.keys())) diff --git a/mne/preprocessing/tests/test_interpolate.py b/mne/preprocessing/tests/test_interpolate.py index 8d4e0906a3b..df99078a0b5 100644 --- a/mne/preprocessing/tests/test_interpolate.py +++ b/mne/preprocessing/tests/test_interpolate.py @@ -83,18 +83,59 @@ def test_interpolate_bridged_electrodes(): idx0 = inst.ch_names.index("EEG 001") idx1 = inst.ch_names.index("EEG 002") ch_names_orig = inst.ch_names.copy() + inst.info["bads"] = ["EEG 003"] bads_orig = inst.info["bads"].copy() + # reference interpolation using the same source channels, i.e. + # excluding the pre-existing bad like the bridged version does inst2 = inst.copy() - inst2.info["bads"] = ["EEG 001", "EEG 002"] + inst2.info["bads"] = ["EEG 001", "EEG 002", "EEG 003"] inst2.interpolate_bads() data_interp_reg = inst2.get_data(picks=["EEG 001", "EEG 002"]) + # a pre-existing bad that is not bridged must not contribute: poisoning + # its data must leave the bridged interpolation result unchanged ... + inst_clean = interpolate_bridged_electrodes(inst.copy(), [(idx0, idx1)]) + data_clean = inst_clean.get_data(picks=["EEG 001", "EEG 002"]) + inst_pois = inst.copy() + inst_pois.apply_function(lambda x: np.full_like(x, 1.0), picks=["EEG 003"]) + data_bad_poisoned = inst_pois.get_data(picks="EEG 003").copy() + inst_pois = interpolate_bridged_electrodes(inst_pois, [(idx0, idx1)]) + assert np.array_equal( + inst_pois.get_data(picks=["EEG 001", "EEG 002"]), data_clean + ) + # ... and the pre-existing bad itself must be left untouched + assert np.array_equal(inst_pois.get_data(picks="EEG 003"), data_bad_poisoned) + assert inst_pois.info["bads"] == bads_orig inst = interpolate_bridged_electrodes(inst, [(idx0, idx1)]) data_interp = inst.get_data(picks=["EEG 001", "EEG 002"]) assert not any(["virtual" in ch for ch in inst.ch_names]) assert inst.ch_names == ch_names_orig assert inst.info["bads"] == bads_orig + assert np.array_equal(data_interp, data_clean) + inst.info["bads"] = [] # check closer to regular interpolation than original data - assert 1e-6 < np.mean(np.abs(data_interp - data_interp_reg)) < 5.4e-5 + assert 1e-6 < np.mean(np.abs(data_interp - data_interp_reg)) < 6.5e-5 + + # a channel that is both pre-existing bad and bridged must still be + # repaired as a bridged target rather than being excluded (checked on + # copies so the shared instances stay pristine for the checks below) + for inst in (raw, epochs, evoked): + idx0 = inst.ch_names.index("EEG 001") + idx1 = inst.ch_names.index("EEG 002") + inst_pois = inst.copy() + inst_pois.info["bads"] = ["EEG 001", "EEG 003"] + bads_orig = inst_pois.info["bads"].copy() + data_bad_before = inst_pois.get_data(picks="EEG 003").copy() + inst_pois.apply_function(lambda x: np.full_like(x, 1.0), picks=["EEG 001"]) + data_bridged_poisoned = inst_pois.get_data(picks="EEG 001").copy() + inst_pois = interpolate_bridged_electrodes(inst_pois, [(idx0, idx1)]) + assert not any(["virtual" in ch for ch in inst_pois.ch_names]) + assert inst_pois.info["bads"] == bads_orig + # the bridged bad was repaired ... + assert not np.array_equal( + inst_pois.get_data(picks="EEG 001"), data_bridged_poisoned + ) + # ... while the non-bridged bad was left untouched + assert np.array_equal(inst_pois.get_data(picks="EEG 003"), data_bad_before) for inst in (raw, epochs, evoked): idx0 = inst.ch_names.index("EEG 001") From 3a45eeb5de0d5b2a36dbbff36a900e8dba965ce1 Mon Sep 17 00:00:00 2001 From: Deepesh Sonar <18deepnar@gmail.com> Date: Mon, 28 Sep 2026 16:40:49 +0530 Subject: [PATCH 2/2] Address CarinaFo review: versionchanged note, raw-only regression test - Docstring: use .. versionchanged:: 1.14 note for the exclusion behavior instead of a prose paragraph. - Simplify the new regression coverage to raw only; the behavior of interpolate_bridged_electrodes does not depend on the input type. The pre-existing loops over raw/epochs/evoked are unchanged. --- mne/preprocessing/interpolate.py | 6 +- mne/preprocessing/tests/test_interpolate.py | 77 ++++++++++----------- 2 files changed, 38 insertions(+), 45 deletions(-) diff --git a/mne/preprocessing/interpolate.py b/mne/preprocessing/interpolate.py index ed7d10bcd5e..8144eda0406 100644 --- a/mne/preprocessing/interpolate.py +++ b/mne/preprocessing/interpolate.py @@ -84,9 +84,9 @@ def interpolate_bridged_electrodes(inst, bridged_idx, bad_limit=4): that to aid in interpolation rather than completely discarding the data from the two channels. - Channels in ``inst.info["bads"]`` that are not part of the bridged - set are excluded from the interpolation: they are neither used to - compute the interpolated values nor interpolated themselves. + .. versionchanged:: 1.14 + Pre-existing bad channels that are not part of the bridged set are + now excluded from the interpolation. Parameters ---------- diff --git a/mne/preprocessing/tests/test_interpolate.py b/mne/preprocessing/tests/test_interpolate.py index df99078a0b5..2653e529c6f 100644 --- a/mne/preprocessing/tests/test_interpolate.py +++ b/mne/preprocessing/tests/test_interpolate.py @@ -83,59 +83,52 @@ def test_interpolate_bridged_electrodes(): idx0 = inst.ch_names.index("EEG 001") idx1 = inst.ch_names.index("EEG 002") ch_names_orig = inst.ch_names.copy() - inst.info["bads"] = ["EEG 003"] bads_orig = inst.info["bads"].copy() - # reference interpolation using the same source channels, i.e. - # excluding the pre-existing bad like the bridged version does inst2 = inst.copy() - inst2.info["bads"] = ["EEG 001", "EEG 002", "EEG 003"] + inst2.info["bads"] = ["EEG 001", "EEG 002"] inst2.interpolate_bads() data_interp_reg = inst2.get_data(picks=["EEG 001", "EEG 002"]) - # a pre-existing bad that is not bridged must not contribute: poisoning - # its data must leave the bridged interpolation result unchanged ... - inst_clean = interpolate_bridged_electrodes(inst.copy(), [(idx0, idx1)]) - data_clean = inst_clean.get_data(picks=["EEG 001", "EEG 002"]) - inst_pois = inst.copy() - inst_pois.apply_function(lambda x: np.full_like(x, 1.0), picks=["EEG 003"]) - data_bad_poisoned = inst_pois.get_data(picks="EEG 003").copy() - inst_pois = interpolate_bridged_electrodes(inst_pois, [(idx0, idx1)]) - assert np.array_equal( - inst_pois.get_data(picks=["EEG 001", "EEG 002"]), data_clean - ) - # ... and the pre-existing bad itself must be left untouched - assert np.array_equal(inst_pois.get_data(picks="EEG 003"), data_bad_poisoned) - assert inst_pois.info["bads"] == bads_orig inst = interpolate_bridged_electrodes(inst, [(idx0, idx1)]) data_interp = inst.get_data(picks=["EEG 001", "EEG 002"]) assert not any(["virtual" in ch for ch in inst.ch_names]) assert inst.ch_names == ch_names_orig assert inst.info["bads"] == bads_orig - assert np.array_equal(data_interp, data_clean) - inst.info["bads"] = [] # check closer to regular interpolation than original data - assert 1e-6 < np.mean(np.abs(data_interp - data_interp_reg)) < 6.5e-5 + assert 1e-6 < np.mean(np.abs(data_interp - data_interp_reg)) < 5.4e-5 - # a channel that is both pre-existing bad and bridged must still be - # repaired as a bridged target rather than being excluded (checked on - # copies so the shared instances stay pristine for the checks below) - for inst in (raw, epochs, evoked): - idx0 = inst.ch_names.index("EEG 001") - idx1 = inst.ch_names.index("EEG 002") - inst_pois = inst.copy() - inst_pois.info["bads"] = ["EEG 001", "EEG 003"] - bads_orig = inst_pois.info["bads"].copy() - data_bad_before = inst_pois.get_data(picks="EEG 003").copy() - inst_pois.apply_function(lambda x: np.full_like(x, 1.0), picks=["EEG 001"]) - data_bridged_poisoned = inst_pois.get_data(picks="EEG 001").copy() - inst_pois = interpolate_bridged_electrodes(inst_pois, [(idx0, idx1)]) - assert not any(["virtual" in ch for ch in inst_pois.ch_names]) - assert inst_pois.info["bads"] == bads_orig - # the bridged bad was repaired ... - assert not np.array_equal( - inst_pois.get_data(picks="EEG 001"), data_bridged_poisoned - ) - # ... while the non-bridged bad was left untouched - assert np.array_equal(inst_pois.get_data(picks="EEG 003"), data_bad_before) + # pre-existing bads that are not bridged must not influence the bridged + # interpolation (raw only; the behavior does not depend on the input type) + idx0 = raw.ch_names.index("EEG 001") + idx1 = raw.ch_names.index("EEG 002") + ch_names_orig = raw.ch_names.copy() + raw.info["bads"] = ["EEG 003"] + bads_orig = raw.info["bads"].copy() + # Verify non-bridged bad doesn't influence the interpolation result + inst_clean = interpolate_bridged_electrodes(raw.copy(), [(idx0, idx1)]) + data_clean = inst_clean.get_data(picks=["EEG 001", "EEG 002"]) + inst_pois = raw.copy() + inst_pois.apply_function(lambda x: np.full_like(x, 1.0), picks=["EEG 003"]) + data_bad_poisoned = inst_pois.get_data(picks="EEG 003").copy() + inst_pois = interpolate_bridged_electrodes(inst_pois, [(idx0, idx1)]) + assert np.array_equal(inst_pois.get_data(picks=["EEG 001", "EEG 002"]), data_clean) + assert np.array_equal(inst_pois.get_data(picks="EEG 003"), data_bad_poisoned) + assert inst_pois.info["bads"] == bads_orig + assert not any(["virtual" in ch for ch in inst_pois.ch_names]) + assert inst_pois.ch_names == ch_names_orig + # Bridged channel that is also marked bad should be repaired + inst_overlap = raw.copy() + inst_overlap.info["bads"] = ["EEG 001", "EEG 003"] + bads_overlap = inst_overlap.info["bads"].copy() + data_bad_before = inst_overlap.get_data(picks="EEG 003").copy() + inst_overlap.apply_function(lambda x: np.full_like(x, 1.0), picks=["EEG 001"]) + data_bridged_poisoned = inst_overlap.get_data(picks="EEG 001").copy() + inst_overlap = interpolate_bridged_electrodes(inst_overlap, [(idx0, idx1)]) + assert inst_overlap.info["bads"] == bads_overlap + assert not np.array_equal( + inst_overlap.get_data(picks="EEG 001"), data_bridged_poisoned + ) + assert np.array_equal(inst_overlap.get_data(picks="EEG 003"), data_bad_before) + raw.info["bads"] = [] for inst in (raw, epochs, evoked): idx0 = inst.ch_names.index("EEG 001")