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..8144eda0406 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. + .. versionchanged:: 1.14 + Pre-existing bad channels that are not part of the bridged set are + now excluded from the interpolation. + 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..2653e529c6f 100644 --- a/mne/preprocessing/tests/test_interpolate.py +++ b/mne/preprocessing/tests/test_interpolate.py @@ -96,6 +96,40 @@ def test_interpolate_bridged_electrodes(): # check closer to regular interpolation than original data assert 1e-6 < np.mean(np.abs(data_interp - data_interp_reg)) < 5.4e-5 + # 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") idx1 = inst.ch_names.index("EEG 002")