From dbf6fd2be473f2442ff4d8f990b6ecee85eca835 Mon Sep 17 00:00:00 2001 From: Teon Brooks Date: Tue, 16 Dec 2025 18:53:07 +0000 Subject: [PATCH 01/14] Update values to int64 --- mne/io/cnt/cnt.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mne/io/cnt/cnt.py b/mne/io/cnt/cnt.py index 196a87564d1..123efeaa3f2 100644 --- a/mne/io/cnt/cnt.py +++ b/mne/io/cnt/cnt.py @@ -310,7 +310,7 @@ def _get_cnt_info(input_fname, eog, ecg, emg, misc, data_format, date_format, he meas_date = _session_date_2_meas_date(session_date, date_format) fid.seek(370) - n_channels = np.fromfile(fid, dtype=" Date: Tue, 16 Dec 2025 14:17:02 -0500 Subject: [PATCH 02/14] FIX: Numbers --- doc/changes/dev/bugfix.rst | 1 + mne/io/cnt/_utils.py | 6 +++--- mne/io/cnt/cnt.py | 44 ++++++++++++++++++++++++-------------- 3 files changed, 32 insertions(+), 19 deletions(-) create mode 100644 doc/changes/dev/bugfix.rst diff --git a/doc/changes/dev/bugfix.rst b/doc/changes/dev/bugfix.rst new file mode 100644 index 00000000000..01c0d2eafcc --- /dev/null +++ b/doc/changes/dev/bugfix.rst @@ -0,0 +1 @@ +Fix bug with reading large CNT files by `Teon Brooks`_. diff --git a/mne/io/cnt/_utils.py b/mne/io/cnt/_utils.py index cf2d45cb1ef..25dfcad4886 100644 --- a/mne/io/cnt/_utils.py +++ b/mne/io/cnt/_utils.py @@ -124,7 +124,7 @@ def _compute_robust_event_table_position(fid, data_format="int32"): if fid.seek(0, SEEK_END) < 2e9: fid.seek(SETUP_EVENTTABLEPOS_OFFSET) - (event_table_pos,) = np.frombuffer(fid.read(4), dtype="= 0] fid.seek(438) - lowpass_toggle = np.fromfile(fid, "i1", count=1).item() - highpass_toggle = np.fromfile(fid, "i1", count=1).item() + lowpass_toggle = bool(np.fromfile(fid, "i1", count=1).item()) + highpass_toggle = bool(np.fromfile(fid, "i1", count=1).item()) # Header has a field for number of samples, but it does not seem to be # too reliable. That's why we have option for setting n_bytes manually. fid.seek(864) - n_samples = np.fromfile(fid, dtype=" 0: info["lowpass"] = highcutoff - if highpass_toggle == 1: + if highpass_toggle and lowcutoff > 0: info["highpass"] = lowcutoff subject_info = { "hand": hand, @@ -540,7 +550,9 @@ def __init__( else: _date_format = "%m/%d/%y %H:%M:%S" - input_fname = path.abspath(input_fname) + input_fname = _check_fname( + input_fname, overwrite="read", must_exist=True, name="input_fname" + ) try: info, cnt_info = _get_cnt_info( input_fname, eog, ecg, emg, misc, data_format, _date_format, header From 61c4a8dee8fd27f05cf23782ba36bd839d289d77 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Tue, 16 Dec 2025 19:17:38 +0000 Subject: [PATCH 03/14] [autofix.ci] apply automated fixes --- doc/changes/dev/{bugfix.rst => 13548.bugfix.rst} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename doc/changes/dev/{bugfix.rst => 13548.bugfix.rst} (100%) diff --git a/doc/changes/dev/bugfix.rst b/doc/changes/dev/13548.bugfix.rst similarity index 100% rename from doc/changes/dev/bugfix.rst rename to doc/changes/dev/13548.bugfix.rst From 272ba472b252884c5759dd45ae69ca9733c3b024 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Tue, 16 Dec 2025 14:26:29 -0500 Subject: [PATCH 04/14] FIX: Unify --- mne/io/cnt/_utils.py | 22 +++++++++++++--------- mne/io/cnt/cnt.py | 35 ++++++++++++++++++++--------------- 2 files changed, 33 insertions(+), 24 deletions(-) diff --git a/mne/io/cnt/_utils.py b/mne/io/cnt/_utils.py index 25dfcad4886..28dd1471445 100644 --- a/mne/io/cnt/_utils.py +++ b/mne/io/cnt/_utils.py @@ -12,6 +12,12 @@ from ...utils import warn +_NCHANNELS_OFFSET = 370 +_NSAMPLES_OFFSET = 864 +_EVENTTABLEPOS_OFFSET = 886 +_DATA_OFFSET = 900 # Size of the 'SETUP' header. +_CH_SIZE = 75 # Size of each channel in bytes + def _read_teeg(f, teeg_offset): """ @@ -116,14 +122,10 @@ def _compute_robust_event_table_position(fid, data_format="int32"): Otherwise, the address of the table position is computed from: n_samples, n_channels, and the bytes size. """ - SETUP_NCHANNELS_OFFSET = 370 - SETUP_NSAMPLES_OFFSET = 864 - SETUP_EVENTTABLEPOS_OFFSET = 886 - fid_origin = fid.tell() # save the state if fid.seek(0, SEEK_END) < 2e9: - fid.seek(SETUP_EVENTTABLEPOS_OFFSET) + fid.seek(_EVENTTABLEPOS_OFFSET) event_table_pos = int(np.frombuffer(fid.read(4), dtype=" 0 - fid.seek(SETUP_NCHANNELS_OFFSET) + fid.seek(_NCHANNELS_OFFSET) n_channels = int(np.frombuffer(fid.read(2), dtype=" 0 event_table_pos = ( - 900 + 75 * int(n_channels) + n_bytes * int(n_channels) * int(n_samples) + _DATA_OFFSET + _CH_SIZE * n_channels + n_bytes * n_channels * n_samples ) fid.seek(fid_origin) # restore the state diff --git a/mne/io/cnt/cnt.py b/mne/io/cnt/cnt.py index 17a7a2d357f..ea94d9cc88c 100644 --- a/mne/io/cnt/cnt.py +++ b/mne/io/cnt/cnt.py @@ -22,6 +22,10 @@ ) from ..base import BaseRaw from ._utils import ( + _CH_SIZE, + _DATA_OFFSET, + _NCHANNELS_OFFSET, + _NSAMPLES_OFFSET, CNTEventType3, _compute_robust_event_table_position, _get_event_parser, @@ -69,7 +73,7 @@ def _translating_function(offset, n_channels, event_type, data_format=data_forma n_bytes = 2 if data_format == "int16" else 4 if event_type == CNTEventType3: offset *= n_bytes * n_channels - event_time = offset - 900 - (75 * n_channels) + event_time = offset - _DATA_OFFSET - (_CH_SIZE * n_channels) event_time //= n_channels * n_bytes event_time = event_time - 1 # Prevent negative event times @@ -281,7 +285,6 @@ def read_raw_cnt( 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() # Reading only the fields of interest. Structure of the whole header at # http://paulbourke.net/dataformats/eeg/ @@ -314,7 +317,7 @@ def _get_cnt_info(input_fname, eog, ecg, emg, misc, data_format, date_format, he session_date = f"{read_str(fid, 10)} {read_str(fid, 12)}" meas_date = _session_date_2_meas_date(session_date, date_format) - fid.seek(370) + fid.seek(_NCHANNELS_OFFSET) n_channels = np.fromfile(fid, dtype=" Date: Wed, 17 Dec 2025 14:38:03 +0000 Subject: [PATCH 05/14] Remove code used for debugging --- mne/io/cnt/cnt.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/mne/io/cnt/cnt.py b/mne/io/cnt/cnt.py index ea94d9cc88c..5573a667d5f 100644 --- a/mne/io/cnt/cnt.py +++ b/mne/io/cnt/cnt.py @@ -332,8 +332,6 @@ def _get_cnt_info(input_fname, eog, ecg, emg, misc, data_format, date_format, he # too reliable. That's why we have option for setting n_bytes manually. fid.seek(_NSAMPLES_OFFSET) n_samples = np.fromfile(fid, dtype=" Date: Sat, 20 Dec 2025 00:19:27 +0000 Subject: [PATCH 06/14] added note --- mne/io/cnt/cnt.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/mne/io/cnt/cnt.py b/mne/io/cnt/cnt.py index 5573a667d5f..f030ee9739d 100644 --- a/mne/io/cnt/cnt.py +++ b/mne/io/cnt/cnt.py @@ -328,8 +328,12 @@ def _get_cnt_info(input_fname, eog, ecg, emg, misc, data_format, date_format, he lowpass_toggle = bool(np.fromfile(fid, "i1", count=1).item()) highpass_toggle = bool(np.fromfile(fid, "i1", count=1).item()) + # Reference: https://paulbourke.net/dataformats/eeg/ # Header has a field for number of samples, but it does not seem to be # too reliable. That's why we have option for setting n_bytes manually. + # According to link above, the number of samples should be calculated as follows: + # nsamples = SETUP.EventTablePos - (900 + 75 * nchannels) / (2 * nchannels) + # where 2 likely refers to the data format with default 2 bytes. fid.seek(_NSAMPLES_OFFSET) n_samples = np.fromfile(fid, dtype=" Date: Sat, 20 Dec 2025 00:26:21 +0000 Subject: [PATCH 07/14] revert back to " Date: Sat, 20 Dec 2025 00:40:00 +0000 Subject: [PATCH 08/14] formatting --- mne/io/cnt/cnt.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mne/io/cnt/cnt.py b/mne/io/cnt/cnt.py index 0b0b03ec8ae..ba30146e341 100644 --- a/mne/io/cnt/cnt.py +++ b/mne/io/cnt/cnt.py @@ -331,7 +331,8 @@ def _get_cnt_info(input_fname, eog, ecg, emg, misc, data_format, date_format, he # Reference: https://paulbourke.net/dataformats/eeg/ # Header has a field for number of samples, but it does not seem to be # too reliable. That's why we have option for setting n_bytes manually. - # According to link above, the number of samples should be calculated as follows: + # According to link above, the number of samples should be + # calculated as follows: # nsamples = SETUP.EventTablePos - (900 + 75 * nchannels) / (2 * nchannels) # where 2 likely refers to the data format with default 2 bytes. fid.seek(_NSAMPLES_OFFSET) From 246da2f46ca4b3e1836ef2db6793fbee4ddb1184 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Thu, 8 Jan 2026 15:03:54 -0500 Subject: [PATCH 09/14] TST: Skip From 519106749fb501070984382d9091448da6d0efee Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Thu, 8 Jan 2026 15:04:02 -0500 Subject: [PATCH 10/14] TST: Skip [ci skip] From 2d4e3394c1be11c1b99829aa72fc4b5ce15b715f Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Fri, 9 Jan 2026 12:14:12 -0500 Subject: [PATCH 11/14] WIP --- mne/annotations.py | 13 +++- mne/io/cnt/_utils.py | 110 +++++++++++++++++++++++++-------- mne/io/cnt/cnt.py | 114 ++++++++--------------------------- mne/io/cnt/tests/test_cnt.py | 42 ++++++++----- 4 files changed, 152 insertions(+), 127 deletions(-) diff --git a/mne/annotations.py b/mne/annotations.py index e298e80918c..9effac61865 100644 --- a/mne/annotations.py +++ b/mne/annotations.py @@ -1366,7 +1366,13 @@ def _write_annotations_txt(fname, annot): @fill_doc def read_annotations( - fname, sfreq="auto", uint16_codec=None, encoding="utf8", ignore_marker_types=False + fname, + sfreq="auto", + uint16_codec=None, + encoding="utf8", + ignore_marker_types=False, + data_format="auto", + n_samples_kind="computed", ) -> Annotations: r"""Read annotations from a file. @@ -1400,6 +1406,10 @@ def read_annotations( ignore_marker_types : bool If ``True``, ignore marker types in BrainVision files (and only use their descriptions). Defaults to ``False``. + data_format : str + Only used by CNT files, see :func:`mne.io.read_raw_cnt` for details. + n_samples_kind : str + Only used by CNT files, see :func:`mne.io.read_raw_cnt` for details. Returns ------- @@ -1444,6 +1454,7 @@ def read_annotations( kwargs = { ".vmrk": {"sfreq": sfreq, "ignore_marker_types": ignore_marker_types}, ".amrk": {"sfreq": sfreq, "ignore_marker_types": ignore_marker_types}, + ".cnt": {"data_format": data_format, "n_samples_kind": n_samples_kind}, ".dat": {"sfreq": sfreq}, ".cdt": {"sfreq": sfreq}, ".cef": {"sfreq": sfreq}, diff --git a/mne/io/cnt/_utils.py b/mne/io/cnt/_utils.py index 28dd1471445..590cd4ed6fe 100644 --- a/mne/io/cnt/_utils.py +++ b/mne/io/cnt/_utils.py @@ -10,10 +10,12 @@ import numpy as np -from ...utils import warn +from ...utils import _check_option, logger, warn +# Offsets from SETUP structure in http://paulbourke.net/dataformats/eeg/ _NCHANNELS_OFFSET = 370 _NSAMPLES_OFFSET = 864 +_RATE_OFFSET = 376 _EVENTTABLEPOS_OFFSET = 886 _DATA_OFFSET = 900 # Size of the 'SETUP' header. _CH_SIZE = 75 # Size of each channel in bytes @@ -111,8 +113,8 @@ def _session_date_2_meas_date(session_date, date_format): return (int_part, frac_part) -def _compute_robust_event_table_position(fid, data_format="int32"): - """Compute `event_table_position`. +def _compute_robust_sizes(*, fid, data_format): + """Compute n_channels, n_samples, n_bytes, and event_table_position. When recording event_table_position is computed (as accomulation). If the file recording is large then this value overflows and ends up pointing @@ -121,34 +123,94 @@ def _compute_robust_event_table_position(fid, data_format="int32"): If the file is smaller than 2G the value in the SETUP is returned. Otherwise, the address of the table position is computed from: n_samples, n_channels, and the bytes size. - """ - fid_origin = fid.tell() # save the state - if fid.seek(0, SEEK_END) < 2e9: + Reference: https://paulbourke.net/dataformats/eeg/ + Header has a field for number of samples, but it does not seem to be + too reliable. + """ + _check_option("data_format", data_format, ["auto", "int16", "int32"]) + # Read the number of channels and samples from the header + fid.seek(_NCHANNELS_OFFSET) + n_channels = int(np.fromfile(fid, dtype=" file_size: + problem = ( + f"Event table offset from header ({event_offset}) is larger than file " + f"size ({file_size})" + ) + if data_format == "auto": + raise RuntimeError( + f"{problem}, cannot automatically compute data format, {workaround}" + ) + warn( + f"Event table offset from header ({event_offset}) is larger than file " + f"size ({file_size}), recomputing event table offset." + ) + n_bytes = 2 if data_format == "int16" else 4 + event_offset = samples_offset + n_samples * n_channels * n_bytes + n_data_bytes = event_offset - samples_offset if data_format == "auto": + n_bytes_per_samp, rem = divmod(n_data_bytes, n_channels) + why = "" + n_bytes = 2 + if rem != 0: + why = ( + f"number of data bytes {n_data_bytes} is not evenly divisible by " + f"{n_channels=}" + ) + elif n_samples == 0: + why = "number of read samples is 0" + else: + n_bytes, rem = divmod(n_bytes_per_samp, n_samples) + if rem != 0 or n_bytes not in [2, 4]: + why = ( + f"number of bytes per sample {n_bytes_per_samp} is not evenly " + f"divisible by {n_samples=} or does not result in 2 or 4 bytes " + f"per sample ({n_bytes=})" + ) + logger.debug("Inferred data format with %d bytes per sample", n_bytes) + if why: + raise RuntimeError( + "Could not automatically compute number of bytes per sample as the " + f"{why}. set data_format manually." + ) + else: + n_bytes = 2 if data_format == "int16" else 4 + logger.debug( + "Using %d bytes per sample from data_format=%s", n_bytes, data_format + ) + n_samples, rem = divmod(n_data_bytes, (n_channels * n_bytes)) + logger.debug("Computed number of samples: %d", n_samples) + if rem != 0: warn( + "Inconsistent file information detected, number of data bytes " + f"({n_data_bytes}) not evenly divisible by number of channels " + f"({n_channels}) times number of bytes ({n_bytes})" + ) + else: + logger.debug("File size >= 2GB, computing event table offset") + if data_format == "auto": + raise RuntimeError( "Using `data_format='auto' for a CNT file larger" - " than 2Gb is not granted to work. Please pass" - " 'int16' or 'int32'.` (assuming int32)" + " than 2Gb is not supported, explicitly pass data_format as " + "'int16' or 'int32'" ) - n_bytes = 2 if data_format == "int16" else 4 - - fid.seek(_NSAMPLES_OFFSET) - n_samples = int(np.frombuffer(fid.read(4), dtype=" 0 - - fid.seek(_NCHANNELS_OFFSET) - n_channels = int(np.frombuffer(fid.read(2), dtype=" 0 - - event_table_pos = ( + event_offset = ( _DATA_OFFSET + _CH_SIZE * n_channels + n_bytes * n_channels * n_samples ) + logger.debug("Computed event table offset: %d", event_offset) - fid.seek(fid_origin) # restore the state - return event_table_pos + return n_channels, n_samples, n_bytes, event_offset diff --git a/mne/io/cnt/cnt.py b/mne/io/cnt/cnt.py index ba30146e341..b6533ae5528 100644 --- a/mne/io/cnt/cnt.py +++ b/mne/io/cnt/cnt.py @@ -17,46 +17,32 @@ _check_option, _explain_exception, _validate_type, + _verbose_safe_false, fill_doc, - warn, + verbose, ) from ..base import BaseRaw from ._utils import ( _CH_SIZE, _DATA_OFFSET, - _NCHANNELS_OFFSET, - _NSAMPLES_OFFSET, + _RATE_OFFSET, CNTEventType3, - _compute_robust_event_table_position, + _compute_robust_sizes, _get_event_parser, _read_teeg, _session_date_2_meas_date, ) -def _read_annotations_cnt(fname, data_format="int16"): +@verbose +def _read_annotations_cnt(fname, *, data_format, verbose=None): """CNT Annotation File Reader. This method opens the .cnt files, searches all the metadata to construct the annotations and parses the event table. Notice that CNT files, can point to a different file containing the events. This case when the event table is separated from the main .cnt is not supported. - - Parameters - ---------- - fname: path-like - Path to CNT file containing the annotations. - data_format : 'int16' | 'int32' - Defines the data format the data is read in. - - Returns - ------- - annot : instance of Annotations - The annotations. """ - # Offsets from SETUP structure in http://paulbourke.net/dataformats/eeg/ - SETUP_NCHANNELS_OFFSET = 370 - SETUP_RATE_OFFSET = 376 def _accept_reject_function(keypad_accept): accept_list = [] @@ -69,8 +55,7 @@ def _accept_reject_function(keypad_accept): accept_list.append("NA") return np.array(accept_list) - def _translating_function(offset, n_channels, event_type, data_format=data_format): - n_bytes = 2 if data_format == "int16" else 4 + def _translating_function(offset, n_channels, event_type, *, n_bytes): if event_type == CNTEventType3: offset *= n_bytes * n_channels event_time = offset - _DATA_OFFSET - (_CH_SIZE * n_channels) @@ -122,22 +107,18 @@ def _update_bad_span_onset(accept_reject, onset, duration, description): ) with open(fname, "rb") as fid: - fid.seek(SETUP_NCHANNELS_OFFSET) - (n_channels,) = np.frombuffer(fid.read(2), dtype="= 0] fid.seek(438) lowpass_toggle = bool(np.fromfile(fid, "i1", count=1).item()) highpass_toggle = bool(np.fromfile(fid, "i1", count=1).item()) - - # Reference: https://paulbourke.net/dataformats/eeg/ - # Header has a field for number of samples, but it does not seem to be - # too reliable. That's why we have option for setting n_bytes manually. - # According to link above, the number of samples should be - # calculated as follows: - # nsamples = SETUP.EventTablePos - (900 + 75 * nchannels) / (2 * nchannels) - # where 2 likely refers to the data format with default 2 bytes. - fid.seek(_NSAMPLES_OFFSET) - n_samples = np.fromfile(fid, dtype=" n_samples: - n_bytes = 4 - n_samples = n_samples_header - warn( - "Annotations are outside data range. " - "Changing data format to 'int32'." - ) - else: - n_bytes = data_size // (n_samples * n_channels) - else: - n_bytes = 2 if data_format == "int16" else 4 - n_samples = data_size // (n_bytes * n_channels) - - # See PR #12393 - if n_samples_header != 0: - n_samples = n_samples_header # Channel offset refers to the size of blocks per channel in the file. cnt_info["channel_offset"] = np.fromfile(fid, dtype=" 1: @@ -536,6 +471,7 @@ class RawCNT(BaseRaw): mne.io.Raw : Documentation of attributes and methods. """ + @verbose def __init__( self, input_fname, @@ -583,7 +519,9 @@ def __init__( data_format = "int32" if cnt_info["n_bytes"] == 4 else "int16" self.set_annotations( - _read_annotations_cnt(input_fname, data_format=data_format) + _read_annotations_cnt( + input_fname, data_format=data_format, verbose=_verbose_safe_false() + ) ) def _read_segment_file(self, data, idx, fi, start, stop, cals, mult): diff --git a/mne/io/cnt/tests/test_cnt.py b/mne/io/cnt/tests/test_cnt.py index f98253b1317..8171844b4ba 100644 --- a/mne/io/cnt/tests/test_cnt.py +++ b/mne/io/cnt/tests/test_cnt.py @@ -24,9 +24,15 @@ @testing.requires_testing_data def test_old_data(): """Test reading raw cnt files.""" - with _no_parse, pytest.warns(RuntimeWarning, match="number of bytes"): + with _no_parse, pytest.raises(RuntimeError, match="number of bytes"): + read_raw_cnt(input_fname=fname, eog="auto", misc=["NA1", "LEFT_EAR"]) + with _no_parse: raw = _test_raw_reader( - read_raw_cnt, input_fname=fname, eog="auto", misc=["NA1", "LEFT_EAR"] + read_raw_cnt, + input_fname=fname, + eog="auto", + misc=["NA1", "LEFT_EAR"], + data_format="int16", ) # make sure we use annotations event if we synthesized stim @@ -44,7 +50,9 @@ def test_old_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") + raw = read_raw_cnt( + input_fname=fname_bad_spans, header="new", data_format="int32" + ) assert raw.info["bads"] == ["F8"] # test bads @@ -52,18 +60,24 @@ def test_new_data(): @testing.requires_testing_data def test_auto_data(): """Test reading raw cnt files with automatic header.""" - first = pytest.warns(RuntimeWarning, match="Could not define the number of bytes.*") - second = pytest.warns(RuntimeWarning, match="Annotations are outside") - third = pytest.warns(RuntimeWarning, match="Omitted 6 annot") - with first, second, third: - raw = read_raw_cnt(input_fname=fname_bad_spans) + inconsistent = pytest.warns(RuntimeWarning, match="Inconsistent file information") + outside = pytest.warns(RuntimeWarning, match="outside the data") + omitted = pytest.warns(RuntimeWarning, match="Omitted 4 annot") + with inconsistent, outside, omitted: + raw = read_raw_cnt( + input_fname=fname_bad_spans, data_format="int16", verbose="debug" + ) # Test that responses are read properly assert "KeyPad Response 1" in raw.annotations.description assert raw.info["bads"] == ["F8"] - with _no_parse, pytest.warns(RuntimeWarning, match="number of bytes"): + with _no_parse: raw = _test_raw_reader( - read_raw_cnt, input_fname=fname, eog="auto", misc=["NA1", "LEFT_EAR"] + read_raw_cnt, + input_fname=fname, + eog="auto", + misc=["NA1", "LEFT_EAR"], + data_format="int16", ) # make sure we use annotations event if we synthesized stim @@ -80,8 +94,8 @@ def test_auto_data(): @testing.requires_testing_data def test_compare_events_and_annotations(): """Test comparing annotations and events.""" - with _no_parse, pytest.warns(RuntimeWarning, match="Could not define the num"): - raw = read_raw_cnt(fname) + with _no_parse: + raw = read_raw_cnt(fname, data_format="int32") events = np.array( [[333, 0, 7], [1010, 0, 7], [1664, 0, 109], [2324, 0, 7], [2984, 0, 109]] ) @@ -96,8 +110,8 @@ def test_compare_events_and_annotations(): @pytest.mark.filterwarnings("ignore::RuntimeWarning") def test_reading_bytes(): """Test reading raw cnt files with different header.""" - raw_16 = read_raw_cnt(fname, preload=True) - raw_32 = read_raw_cnt(fname_bad_spans, preload=True) + raw_16 = read_raw_cnt(fname, preload=True, data_format="int16") + raw_32 = read_raw_cnt(fname_bad_spans, preload=True, data_format="int32") # Verify that the number of bytes read is correct assert len(raw_16) == 3070 From dc160f0a32aaa2c7ca55da0e6cd4056de562707f Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Wed, 4 Feb 2026 13:45:13 -0500 Subject: [PATCH 12/14] FIX: Passing? --- mne/annotations.py | 5 +---- mne/io/cnt/tests/test_cnt.py | 11 ++++++----- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/mne/annotations.py b/mne/annotations.py index 9effac61865..6fee30b7b93 100644 --- a/mne/annotations.py +++ b/mne/annotations.py @@ -1372,7 +1372,6 @@ def read_annotations( encoding="utf8", ignore_marker_types=False, data_format="auto", - n_samples_kind="computed", ) -> Annotations: r"""Read annotations from a file. @@ -1408,8 +1407,6 @@ def read_annotations( descriptions). Defaults to ``False``. data_format : str Only used by CNT files, see :func:`mne.io.read_raw_cnt` for details. - n_samples_kind : str - Only used by CNT files, see :func:`mne.io.read_raw_cnt` for details. Returns ------- @@ -1454,7 +1451,7 @@ def read_annotations( kwargs = { ".vmrk": {"sfreq": sfreq, "ignore_marker_types": ignore_marker_types}, ".amrk": {"sfreq": sfreq, "ignore_marker_types": ignore_marker_types}, - ".cnt": {"data_format": data_format, "n_samples_kind": n_samples_kind}, + ".cnt": {"data_format": data_format}, ".dat": {"sfreq": sfreq}, ".cdt": {"sfreq": sfreq}, ".cef": {"sfreq": sfreq}, diff --git a/mne/io/cnt/tests/test_cnt.py b/mne/io/cnt/tests/test_cnt.py index 8171844b4ba..0ffb50b5484 100644 --- a/mne/io/cnt/tests/test_cnt.py +++ b/mne/io/cnt/tests/test_cnt.py @@ -19,6 +19,7 @@ _no_parse = pytest.warns(RuntimeWarning, match="Could not parse") +inconsistent = pytest.warns(RuntimeWarning, match="Inconsistent file information") @testing.requires_testing_data @@ -60,7 +61,6 @@ def test_new_data(): @testing.requires_testing_data def test_auto_data(): """Test reading raw cnt files with automatic header.""" - inconsistent = pytest.warns(RuntimeWarning, match="Inconsistent file information") outside = pytest.warns(RuntimeWarning, match="outside the data") omitted = pytest.warns(RuntimeWarning, match="Omitted 4 annot") with inconsistent, outside, omitted: @@ -95,12 +95,12 @@ def test_auto_data(): def test_compare_events_and_annotations(): """Test comparing annotations and events.""" with _no_parse: - raw = read_raw_cnt(fname, data_format="int32") + raw = read_raw_cnt(fname, data_format="int16") events = np.array( [[333, 0, 7], [1010, 0, 7], [1664, 0, 109], [2324, 0, 7], [2984, 0, 109]] ) - annot = read_annotations(fname) + annot = read_annotations(fname, data_format="int16") assert len(annot) == 6 assert_array_equal(annot.onset[:-1], events[:, 0] / raw.info["sfreq"]) assert "STI 014" not in raw.info["ch_names"] @@ -115,12 +115,13 @@ def test_reading_bytes(): # Verify that the number of bytes read is correct assert len(raw_16) == 3070 - assert len(raw_32) == 90000 + assert len(raw_32) == 143765 # TODO: Used to be 90000! Need to eyeball @testing.requires_testing_data def test_bad_spans(): """Test reading raw cnt files with bad spans.""" - annot = read_annotations(fname_bad_spans) + with inconsistent: + annot = read_annotations(fname_bad_spans, data_format="int32") temp = "\t".join(annot.description) assert "BAD" in temp From 5d58f0f93e4ec5d7654fc6e936211adb3e002a7f Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Wed, 4 Feb 2026 14:42:08 -0500 Subject: [PATCH 13/14] FIX: More --- mne/io/cnt/tests/test_cnt.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/mne/io/cnt/tests/test_cnt.py b/mne/io/cnt/tests/test_cnt.py index 0ffb50b5484..d41cbfcd122 100644 --- a/mne/io/cnt/tests/test_cnt.py +++ b/mne/io/cnt/tests/test_cnt.py @@ -20,6 +20,8 @@ _no_parse = pytest.warns(RuntimeWarning, match="Could not parse") inconsistent = pytest.warns(RuntimeWarning, match="Inconsistent file information") +outside = pytest.warns(RuntimeWarning, match="outside the data") +omitted = pytest.warns(RuntimeWarning, match="Omitted 4 annot") @testing.requires_testing_data @@ -50,7 +52,7 @@ def test_old_data(): @testing.requires_testing_data def test_new_data(): """Test reading raw cnt files with different header.""" - with pytest.warns(RuntimeWarning): + with inconsistent, outside, omitted: raw = read_raw_cnt( input_fname=fname_bad_spans, header="new", data_format="int32" ) @@ -61,8 +63,6 @@ def test_new_data(): @testing.requires_testing_data def test_auto_data(): """Test reading raw cnt files with automatic header.""" - outside = pytest.warns(RuntimeWarning, match="outside the data") - omitted = pytest.warns(RuntimeWarning, match="Omitted 4 annot") with inconsistent, outside, omitted: raw = read_raw_cnt( input_fname=fname_bad_spans, data_format="int16", verbose="debug" @@ -107,11 +107,12 @@ def test_compare_events_and_annotations(): @testing.requires_testing_data -@pytest.mark.filterwarnings("ignore::RuntimeWarning") def test_reading_bytes(): """Test reading raw cnt files with different header.""" - raw_16 = read_raw_cnt(fname, preload=True, data_format="int16") - raw_32 = read_raw_cnt(fname_bad_spans, preload=True, data_format="int32") + with _no_parse: + raw_16 = read_raw_cnt(fname, preload=True, data_format="int16") + with inconsistent, outside, omitted: + raw_32 = read_raw_cnt(fname_bad_spans, preload=True, data_format="int32") # Verify that the number of bytes read is correct assert len(raw_16) == 3070 From 05c85cf8dda452f48adb55b2c14a32282a85f425 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Wed, 4 Feb 2026 14:44:11 -0500 Subject: [PATCH 14/14] FIX: Better --- mne/io/fieldtrip/tests/helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mne/io/fieldtrip/tests/helpers.py b/mne/io/fieldtrip/tests/helpers.py index 0a0f6671d6b..59df5c61108 100644 --- a/mne/io/fieldtrip/tests/helpers.py +++ b/mne/io/fieldtrip/tests/helpers.py @@ -49,7 +49,7 @@ system_to_reader_fn_dict = { "neuromag306": mne.io.read_raw_fif, - "CNT": partial(mne.io.read_raw_cnt), + "CNT": partial(mne.io.read_raw_cnt, data_format="int16"), "CTF": partial(mne.io.read_raw_ctf, clean_names=True), "BTI": partial( mne.io.read_raw_bti,