From ba8a36a0d3777306618634f23bfff1601649aeeb Mon Sep 17 00:00:00 2001 From: PragnyaKhandelwal Date: Fri, 14 Aug 2026 01:38:14 +0530 Subject: [PATCH 1/2] ENH: read EGI MFF channelStatus into raw.info['bads'] When categories.xml is present in an MFF directory, collect the union of all channels marked exclusion="badChannels" across every category segment and populate raw.info["bads"] with their MNE channel names. --- doc/changes/dev/14156.newfeature.rst | 1 + mne/io/egi/egimff.py | 44 ++++++++++++++++++++++++++++ mne/io/egi/tests/test_egi.py | 29 ++++++++++++++++++ 3 files changed, 74 insertions(+) create mode 100644 doc/changes/dev/14156.newfeature.rst diff --git a/doc/changes/dev/14156.newfeature.rst b/doc/changes/dev/14156.newfeature.rst new file mode 100644 index 00000000000..514fba3e9fc --- /dev/null +++ b/doc/changes/dev/14156.newfeature.rst @@ -0,0 +1 @@ +:func:`mne.io.read_raw_egi` now reads bad channels from ``categories.xml`` (when present in the MFF directory) and populates :attr:`mne.Info.bads` with channels marked ``exclusion="badChannels"`` in NetStation. By `Pragnya Khandelwal`_. diff --git a/mne/io/egi/egimff.py b/mne/io/egi/egimff.py index 8dfee16292e..ea56a33ec1c 100644 --- a/mne/io/egi/egimff.py +++ b/mne/io/egi/egimff.py @@ -67,6 +67,42 @@ def _disk_range_to_epochs(egi_info, disk_start, disk_stop): yield ei, t0, dt, ov_start - disk_start, ov_stop - disk_start +def _read_channel_status_bads(filepath, ch_names, pns_names): + """Return bad channel names from categories.xml channelStatus, if present. + + EGI NetStation writes per-epoch bad-channel lists into ``categories.xml`` + as ```` elements. This function collects the union of all + channels marked ``exclusion="badChannels"`` across every category and + segment and maps them back to MNE channel names. + + Returns an empty list when ``categories.xml`` is absent or unparseable. + """ + cats_path = op.join(filepath, "categories.xml") + if not op.isfile(cats_path): + return [] + from mffpy.xml_files import XML + + try: + cats_obj = XML.from_file(cats_path) + except Exception: + return [] + bads = set() + for segments in cats_obj.categories.values(): + for seg in segments: + for entry in seg.get("channelStatus") or []: + if entry["exclusion"] != "badChannels": + continue + if entry["signalBin"] == 1: + for ch in entry["channels"]: + if 1 <= ch <= len(ch_names): + bads.add(ch_names[ch - 1]) + elif entry["signalBin"] == 2: + for ch in entry["channels"]: + if 1 <= ch <= len(pns_names): + bads.add(pns_names[ch - 1]) + return sorted(bads) + + def _read_mff_header(filepath): """Read mff header.""" _soft_import("mffpy", "reading EGI MFF data") @@ -514,6 +550,14 @@ def __init__( if chan["kind"] == FIFF.FIFFV_EEG_CH: chan["loc"][3:6] = ref_coords + # Mark bad channels from categories.xml channelStatus if present + bads = _read_channel_status_bads( + input_fname, ch_names, egi_info.get("pns_names", []) + ) + if bads: + with info._unlock(): + info["bads"] = bads + file_bin = op.join(input_fname, egi_info["eeg_fname"]) egi_info["egi_events"] = egi_events egi_info["mff_path"] = input_fname diff --git a/mne/io/egi/tests/test_egi.py b/mne/io/egi/tests/test_egi.py index 10a7435666e..88d9377724c 100644 --- a/mne/io/egi/tests/test_egi.py +++ b/mne/io/egi/tests/test_egi.py @@ -604,6 +604,35 @@ def test_egi_mff_bad_xml(tmp_path): assert "DIN1" in raw.annotations.description +@requires_testing_data +def test_egi_mff_channel_status(tmp_path): + """Test that bad channels from categories.xml channelStatus are read.""" + mff_fname = copytree_rw(egi_pause_fname, tmp_path / "paused_status.mff") + # Minimal categories.xml marking EEG channels 5 and 23 as bad + cats_xml = """\ + + + + Recording + + + 0 + 1300000 + 0 + 0 + + 5 23 + + + + + +""" + (mff_fname / "categories.xml").write_text(cats_xml, encoding="utf-8") + raw = read_raw_egi(mff_fname, events_as_annotations=False, verbose=False) + assert raw.info["bads"] == ["E23", "E5"] # sorted alphabetically + + @requires_testing_data @pytest.mark.parametrize( "fname, expected", From 5b70061d3fac974e19278459f9a5c501304cd032 Mon Sep 17 00:00:00 2001 From: PragnyaKhandelwal Date: Fri, 14 Aug 2026 01:54:37 +0530 Subject: [PATCH 2/2] fix codespell: unparseable -> unparsable --- mne/io/egi/egimff.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mne/io/egi/egimff.py b/mne/io/egi/egimff.py index ea56a33ec1c..8091b34b721 100644 --- a/mne/io/egi/egimff.py +++ b/mne/io/egi/egimff.py @@ -75,7 +75,7 @@ def _read_channel_status_bads(filepath, ch_names, pns_names): channels marked ``exclusion="badChannels"`` across every category and segment and maps them back to MNE channel names. - Returns an empty list when ``categories.xml`` is absent or unparseable. + Returns an empty list when ``categories.xml`` is absent or unparsable. """ cats_path = op.join(filepath, "categories.xml") if not op.isfile(cats_path):