Skip to content

Commit

Permalink
[WIP] [BUG] Fix reading bad channels in cnt files (mne-tools#12189)
Browse files Browse the repository at this point in the history
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Eric Larson <[email protected]>
  • Loading branch information
3 people authored Nov 14, 2023
1 parent 3fd2c91 commit 9b57c51
Show file tree
Hide file tree
Showing 3 changed files with 69 additions and 7 deletions.
2 changes: 2 additions & 0 deletions doc/changes/devel.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
~~~~~~~~~~~
Expand Down
39 changes: 33 additions & 6 deletions mne/io/cnt/cnt.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -169,6 +169,8 @@ def read_raw_cnt(
emg=(),
data_format="auto",
date_format="mm/dd/yy",
*,
header="auto",
preload=False,
verbose=None,
):
Expand Down Expand Up @@ -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
Expand All @@ -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()
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -478,6 +503,8 @@ def __init__(
emg=(),
data_format="auto",
date_format="mm/dd/yy",
*,
header="auto",
preload=False,
verbose=None,
): # noqa: D102
Expand All @@ -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__(
Expand Down
35 changes: 34 additions & 1 deletion mne/io/cnt/tests/test_cnt.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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."""
Expand Down

0 comments on commit 9b57c51

Please sign in to comment.