From f7c184ad4fd10a72f367769f03c2ac4f7cf1f5c8 Mon Sep 17 00:00:00 2001 From: Heberto Mayorquin Date: Thu, 25 Jun 2026 16:17:04 -0600 Subject: [PATCH 1/2] fix v1 date --- neo/rawio/axonrawio.py | 18 ++++++++++++++---- neo/test/rawiotest/test_axonrawio.py | 15 ++++++++++++++- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/neo/rawio/axonrawio.py b/neo/rawio/axonrawio.py index 166446a90..5b9d84f07 100644 --- a/neo/rawio/axonrawio.py +++ b/neo/rawio/axonrawio.py @@ -665,15 +665,24 @@ def _parse_abf_v1(f, header_description): header["sProtocolPath"] = header["sProtocolPath"].replace(b"\\", b"/") # date and time - YY = 1900 - MM = 1 - DD = 1 + # lFileStartDate is a YYYYMMDD-packed integer, parsed the same way as uFileStartDate in ABF2. + # An unset/sentinel value (0 or 0xFFFFFFFF) yields an out-of-range date that the try/except + # below turns into rec_datetime=None. + YY = int(header["lFileStartDate"] / 10000) + MM = int((header["lFileStartDate"] - YY * 10000) / 100) + DD = int(header["lFileStartDate"] - YY * 10000 - MM * 100) 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) + try: + header["rec_datetime"] = datetime.datetime(YY, MM, DD, hh, mm, ss, ms) + except (ValueError, OverflowError): + # Date/time header fields hold an out-of-range or "no date" sentinel + # (e.g. 0xFFFFFFFF). The acquisition date is non-essential annotation, + # so fall back to None rather than blocking the read of the signal. + header["rec_datetime"] = None return header @@ -1060,6 +1069,7 @@ def safe_decode_units(s): ("lActualAcqLength", 10, "i"), ("nNumPointsIgnored", 14, "h"), ("lActualEpisodes", 16, "i"), + ("lFileStartDate", 20, "i"), ("lFileStartTime", 24, "i"), ("lDataSectionPtr", 40, "i"), ("lTagSectionPtr", 44, "i"), diff --git a/neo/test/rawiotest/test_axonrawio.py b/neo/test/rawiotest/test_axonrawio.py index a1cb7dd2b..883b5406e 100644 --- a/neo/test/rawiotest/test_axonrawio.py +++ b/neo/test/rawiotest/test_axonrawio.py @@ -1,6 +1,7 @@ +import datetime 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 +29,18 @@ def test_read_raw_protocol(self): reader.read_raw_protocol() + def test_v1_reads_real_acquisition_date(self): + # ABF1 stores the calendar date in lFileStartDate (a YYYYMMDD-packed integer). Older neo + # ignored that field and hardcoded 1900-01-01, so the recording date was always wrong for + # ABF1 files. It must now be read from the header. + expected_datetime = datetime.datetime(2005, 6, 11, 14, 15, 0) + header = parse_axon_soup(self.get_local_path("axon/File_axon_2.abf")) + rec_datetime = header["rec_datetime"] + # Drop sub-second precision: the millisecond field round-trips through a float, so the + # microsecond is a rounding artifact, not a meaningful value to assert. + self.assertEqual(rec_datetime.replace(microsecond=0), expected_datetime) + if __name__ == "__main__": unittest.main() + \ No newline at end of file From 74458a53125e8629e5dccc594de7ed5a7bef3ea0 Mon Sep 17 00:00:00 2001 From: Heberto Mayorquin Date: Thu, 9 Jul 2026 16:06:15 +0100 Subject: [PATCH 2/2] zach review --- neo/rawio/axonrawio.py | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/neo/rawio/axonrawio.py b/neo/rawio/axonrawio.py index 5b9d84f07..59bc74028 100644 --- a/neo/rawio/axonrawio.py +++ b/neo/rawio/axonrawio.py @@ -666,23 +666,23 @@ def _parse_abf_v1(f, header_description): # date and time # lFileStartDate is a YYYYMMDD-packed integer, parsed the same way as uFileStartDate in ABF2. - # An unset/sentinel value (0 or 0xFFFFFFFF) yields an out-of-range date that the try/except - # below turns into rec_datetime=None. - YY = int(header["lFileStartDate"] / 10000) - MM = int((header["lFileStartDate"] - YY * 10000) / 100) - DD = int(header["lFileStartDate"] - YY * 10000 - MM * 100) - 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) - try: - header["rec_datetime"] = datetime.datetime(YY, MM, DD, hh, mm, ss, ms) - except (ValueError, OverflowError): - # Date/time header fields hold an out-of-range or "no date" sentinel - # (e.g. 0xFFFFFFFF). The acquisition date is non-essential annotation, - # so fall back to None rather than blocking the read of the signal. + # A "no date" sentinel means there is no date to build, so fall back to rec_datetime=None. The + # field is signed, so the all-bits-set 0xFFFFFFFF sentinel is read as -1, and 0 is the unset + # value. 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, -1) + if header["lFileStartDate"] in no_date_sentinels: header["rec_datetime"] = None + else: + YY = int(header["lFileStartDate"] / 10000) + MM = int((header["lFileStartDate"] - YY * 10000) / 100) + DD = int(header["lFileStartDate"] - YY * 10000 - MM * 100) + 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