From 2464de4305d79d4383340d2cfeb2ca8a356db39d Mon Sep 17 00:00:00 2001 From: Heberto Mayorquin Date: Fri, 10 Jul 2026 19:39:41 +0100 Subject: [PATCH 1/2] axon channel names improvements --- neo/rawio/axonrawio.py | 11 +++++++++-- neo/test/rawiotest/test_axonrawio.py | 19 +++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/neo/rawio/axonrawio.py b/neo/rawio/axonrawio.py index 75457ce57..b57a3ccf3 100644 --- a/neo/rawio/axonrawio.py +++ b/neo/rawio/axonrawio.py @@ -211,15 +211,22 @@ def _parse_header(self): signal_channels = [] adc_nums = [] for chan_index, chan_id in enumerate(channel_ids): + # Name fields are fixed-width and right-padded with spaces (v1) or already trimmed (v2). + # Strip the padding but keep interior spaces (e.g. "IN 1"); errors="replace" so an odd + # byte can never crash the read. if version < 2.0: - name = info["sADCChannelName"][chan_id].replace(b" ", b"") + name = info["sADCChannelName"][chan_id].decode("utf-8", errors="replace").strip() units = safe_decode_units(info["sADCUnits"][chan_id]) adc_num = info["nADCPtoLChannelMap"][chan_id] elif version >= 2.0: ADCInfo = info["listADCInfo"][chan_id] - name = ADCInfo["ADCChNames"].replace(b" ", b"") + name = ADCInfo["ADCChNames"].decode("utf-8", errors="replace").strip() units = safe_decode_units(ADCInfo["ADCChUnits"]) adc_num = ADCInfo["nADCNum"] + if not name: + # A blank name leaves the channel unaddressable; fall back to a positional name so + # every channel keeps a usable id. + name = f"ch{chan_id}" adc_nums.append(adc_num) if info["nDataFormat"] == 0: diff --git a/neo/test/rawiotest/test_axonrawio.py b/neo/test/rawiotest/test_axonrawio.py index cb1f07a6f..cc6f335f6 100644 --- a/neo/test/rawiotest/test_axonrawio.py +++ b/neo/test/rawiotest/test_axonrawio.py @@ -29,6 +29,25 @@ def test_read_raw_protocol(self): reader.read_raw_protocol() + def test_empty_channel_name_gets_fallback(self): + # Some ABF files store a blank ADC channel name, which collapses to "" after space + # stripping and leaves the channel unaddressable by name. A positional fallback (ch{id}) + # must be used instead so every channel keeps a usable name. + path = self.get_local_path("axon/intracellular_data/abf1_episodic_empty_channel_name.abf") + reader = AxonRawIO(filename=path) + reader.parse_header() + names = list(reader.header["signal_channels"]["name"]) + self.assertNotIn("", names) + self.assertEqual(names, ["ch0"]) + + def test_channel_name_keeps_interior_space(self): + # Channel names are stripped of padding but keep interior spaces (e.g. "IN 1", not "IN1") + # and are returned as str. + reader = AxonRawIO(filename=self.get_local_path("axon/File_axon_7.abf")) + reader.parse_header() + names = list(reader.header["signal_channels"]["name"]) + self.assertEqual(names, ["IN 1"]) + 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 From 8cc0db60bccd3407838cedfc4b62f0abf0c7b1fc Mon Sep 17 00:00:00 2001 From: Heberto Mayorquin Date: Fri, 10 Jul 2026 20:02:56 +0100 Subject: [PATCH 2/2] protocol bug --- neo/rawio/axonrawio.py | 10 ++++++---- neo/test/rawiotest/test_axonrawio.py | 8 ++++++++ 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/neo/rawio/axonrawio.py b/neo/rawio/axonrawio.py index b57a3ccf3..e1ce37255 100644 --- a/neo/rawio/axonrawio.py +++ b/neo/rawio/axonrawio.py @@ -679,9 +679,10 @@ def _parse_abf_v1(f, header_description): listTag.append(tag) header["listTag"] = listTag - # protocol name formatting - header["sProtocolPath"] = clean_string(header["sProtocolPath"]) - header["sProtocolPath"] = header["sProtocolPath"].replace(b"\\", b"/") + # protocol name formatting. Decode to str (like the channel names) so consumers get a plain + # string rather than a bytes value, whose str() would bake the "b'...'" repr into the path. + header["sProtocolPath"] = clean_string(header["sProtocolPath"]).decode("utf-8", errors="replace") + header["sProtocolPath"] = header["sProtocolPath"].replace("\\", "/") # date and time # lFileStartDate is a YYYYMMDD-packed integer, parsed the same way as uFileStartDate in ABF2. @@ -969,7 +970,8 @@ def _parse_abf_v2(f, header_description): else: protocol[key] = np.array(val) header["protocol"] = protocol - header["sProtocolPath"] = strings[header["uProtocolPathIndex"]] + # Decode to str (like the channel names) so consumers get a plain string, not raw bytes. + header["sProtocolPath"] = strings[header["uProtocolPathIndex"]].decode("utf-8", errors="replace") # tags listTag = [] diff --git a/neo/test/rawiotest/test_axonrawio.py b/neo/test/rawiotest/test_axonrawio.py index cc6f335f6..ae1125a39 100644 --- a/neo/test/rawiotest/test_axonrawio.py +++ b/neo/test/rawiotest/test_axonrawio.py @@ -48,6 +48,14 @@ def test_channel_name_keeps_interior_space(self): names = list(reader.header["signal_channels"]["name"]) self.assertEqual(names, ["IN 1"]) + def test_protocol_path_decoded_to_str(self): + # String header fields should be decoded to str, not left as raw bytes; otherwise a caller + # doing str(value) gets the "b'...'" byte-literal repr baked into the path. + for fixture in ["axon/File_axon_1.abf", "axon/File_axon_2.abf"]: # v2 and v1 + reader = AxonRawIO(filename=self.get_local_path(fixture)) + reader.parse_header() + self.assertIsInstance(reader._axon_info["sProtocolPath"], str) + 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