diff --git a/.gitignore b/.gitignore index 3d09413dc..36a5009dc 100644 --- a/.gitignore +++ b/.gitignore @@ -82,4 +82,5 @@ doc/*.nwb B95.zip grouped_ephys uv.lock -.venv \ No newline at end of file +.venv +.python-version \ No newline at end of file diff --git a/neo/rawio/axonrawio.py b/neo/rawio/axonrawio.py index ef6df08f6..7351a5008 100644 --- a/neo/rawio/axonrawio.py +++ b/neo/rawio/axonrawio.py @@ -680,12 +680,18 @@ def _parse_abf_v1(f, header_description): YY = 1900 MM = 1 DD = 1 - hh = int(header["lFileStartTime"] / 3600.0) - mm = int((header["lFileStartTime"] - hh * 3600) / 60) - ss = header["lFileStartTime"] - hh * 3600 - mm * 60 - ms = int(np.mod(ss, 1) * 1e6) - ss = int(ss) - header["rec_datetime"] = datetime.datetime(YY, MM, DD, hh, mm, ss, ms) + seconds_per_day = 24 * 3600 + # A "no date" file writes the 0xFFFFFFFF sentinel, which reads as a negative time; fall back to + # rec_datetime=None instead of crashing on the resulting out-of-range time-of-day. + if not (0 <= header["lFileStartTime"] < seconds_per_day): + header["rec_datetime"] = None + else: + hh = int(header["lFileStartTime"] / 3600.0) + mm = int((header["lFileStartTime"] - hh * 3600) / 60) + ss = header["lFileStartTime"] - hh * 3600 - mm * 60 + ms = int(np.mod(ss, 1) * 1e6) + ss = int(ss) + header["rec_datetime"] = datetime.datetime(YY, MM, DD, hh, mm, ss, ms) return header @@ -1029,15 +1035,22 @@ def _parse_abf_v2(f, header_description): header["EpochInfo"].append(EpochInfo) # date and time - YY = int(header["uFileStartDate"] / 10000) - MM = int((header["uFileStartDate"] - YY * 10000) / 100) - DD = int(header["uFileStartDate"] - YY * 10000 - MM * 100) - hh = int(header["uFileStartTimeMS"] / 1000.0 / 3600.0) - mm = int((header["uFileStartTimeMS"] / 1000.0 - hh * 3600) / 60) - ss = header["uFileStartTimeMS"] / 1000.0 - hh * 3600 - mm * 60 - ms = int(np.mod(ss, 1) * 1e6) - ss = int(ss) - header["rec_datetime"] = datetime.datetime(YY, MM, DD, hh, mm, ss, ms) + # A "no date" sentinel (0 = unset, 0xFFFFFFFF = all bits set) has no valid date to build, so + # fall back to rec_datetime=None. Any other value is trusted and left to raise if genuinely out + # of range, so a real parsing error surfaces rather than being masked. + no_date_sentinels = (0, 0xFFFFFFFF) + if header["uFileStartDate"] in no_date_sentinels: + header["rec_datetime"] = None + else: + YY = int(header["uFileStartDate"] / 10000) + MM = int((header["uFileStartDate"] - YY * 10000) / 100) + DD = int(header["uFileStartDate"] - YY * 10000 - MM * 100) + hh = int(header["uFileStartTimeMS"] / 1000.0 / 3600.0) + mm = int((header["uFileStartTimeMS"] / 1000.0 - hh * 3600) / 60) + ss = header["uFileStartTimeMS"] / 1000.0 - hh * 3600 - mm * 60 + ms = int(np.mod(ss, 1) * 1e6) + ss = int(ss) + header["rec_datetime"] = datetime.datetime(YY, MM, DD, hh, mm, ss, ms) return header diff --git a/neo/test/rawiotest/test_axonrawio.py b/neo/test/rawiotest/test_axonrawio.py index a9986b692..50c5ce389 100644 --- a/neo/test/rawiotest/test_axonrawio.py +++ b/neo/test/rawiotest/test_axonrawio.py @@ -1,6 +1,6 @@ import unittest -from neo.rawio.axonrawio import AxonRawIO +from neo.rawio.axonrawio import AxonRawIO, parse_axon_soup from neo.test.rawiotest.common_rawio_test import BaseTestRawIO @@ -28,6 +28,18 @@ def test_read_raw_protocol(self): reader.read_raw_protocol() + def test_invalid_date_falls_back_to_none(self): + # Some ABF files store an out-of-range / "no date" sentinel (e.g. 0xFFFFFFFF) + # in the acquisition date header fields. The date is non-essential annotation, + # so parsing must fall back to rec_datetime=None rather than raising and + # blocking access to the signal. + for fixture in [ + "axon/intracellular_data/files_with_errors/invalid_date_abf1.abf", # ABF v1 + "axon/intracellular_data/files_with_errors/invalid_date_abf2.abf", # ABF v2 + ]: + header = parse_axon_soup(self.get_local_path(fixture)) + self.assertIsNone(header["rec_datetime"]) + def test_non_unique_channel_ids_fall_back_to_sequential_ids(self): # Some version < 2.0 ABF files (e.g. re-saved exports) corrupt nADCSamplingSeq so every # entry is identical, which yields non-unique channel ids. AxonRawIO detects this, warns,