Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 15 additions & 6 deletions neo/rawio/axonrawio.py
Original file line number Diff line number Diff line change
Expand Up @@ -232,15 +232,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()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One last question. I remember on the spikeinterface side we had some formats that failed with 'utf-8' decoding. Is there a failure possibility here using this type of decode? Or the replace guarantees this will always work?

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:
Expand Down Expand Up @@ -705,9 +712,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.
Expand Down Expand Up @@ -995,7 +1003,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 = []
Expand Down
27 changes: 27 additions & 0 deletions neo/test/rawiotest/test_axonrawio.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,33 @@ 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_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_integer_overflow_size_raises(self):
# An ABF header that claims more samples than the file can hold must raise a
# clear error instead of silently returning an overflowed signal size.
Expand Down
Loading