Skip to content

Commit 3bf261e

Browse files
authored
Merge branch 'master' into add_buffer_api_like_behavior_for_blackrock
2 parents 68aaffa + 64e929e commit 3bf261e

3 files changed

Lines changed: 274 additions & 7 deletions

File tree

neo/rawio/axonrawio.py

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
4747
"""
4848

49+
import os
4950
import struct
5051
import datetime
5152
from io import open, BufferedReader
@@ -160,9 +161,17 @@ def _parse_header(self):
160161
self._t_starts = {}
161162
self._buffer_descriptions = {0: {}}
162163
self._stream_buffer_slice = {stream_id: None}
164+
# Offsets and segment sizes come from header fields that can be corrupt
165+
# (truncated file, header surgery). Do the arithmetic in Python ints so it
166+
# cannot silently overflow as numpy int32 would, and validate the implied
167+
# data extent against the file on disk so a bad header raises a clear error
168+
# rather than returning garbage or negative signal sizes.
169+
head_offset = int(head_offset)
170+
file_size = os.path.getsize(self.filename)
171+
163172
pos = 0
164173
for seg_index in range(nb_segment):
165-
length = episode_array[seg_index]["len"]
174+
length = int(episode_array[seg_index]["len"])
166175

167176
if version < 2.0:
168177
fSynchTimeUnit = info["fSynchTimeUnit"]
@@ -172,6 +181,11 @@ def _parse_header(self):
172181
if (fSynchTimeUnit != 0) and (mode == 1):
173182
length /= fSynchTimeUnit
174183

184+
if length < 0:
185+
raise NeoReadWriteError(
186+
f"Negative segment size ({length}) parsed from {self.filename}; the file header is corrupt."
187+
)
188+
175189
self._buffer_descriptions[0][seg_index] = {}
176190
self._buffer_descriptions[0][seg_index][buffer_id] = {
177191
"type": "raw",
@@ -190,6 +204,13 @@ def _parse_header(self):
190204
t_start = t_start * fSynchTimeUnit * 1e-6
191205
self._t_starts[seg_index] = t_start
192206

207+
implied_data_end = head_offset + pos * sig_dtype.itemsize
208+
if implied_data_end > file_size:
209+
raise NeoReadWriteError(
210+
f"ABF header implies {pos} samples ending at byte {implied_data_end}, which exceeds the "
211+
f"file size of {file_size} bytes for {self.filename}; the file header is corrupt or the file is truncated."
212+
)
213+
193214
# Create channel header. By default assume channels 0..nbchannel-1 were sampled in order,
194215
# which is always the case for version >= 2.0. For version < 2.0 the channel ids come from
195216
# nADCSamplingSeq (the ADC sampling sequence) and are also used to index the per-channel
@@ -490,8 +511,13 @@ def parse_axon_soup(filename):
490511
491512
Returns
492513
-------
493-
dict or None
494-
Header dictionary with file metadata, or None if file signature is invalid
514+
dict
515+
Header dictionary with file metadata.
516+
517+
Raises
518+
------
519+
NeoReadWriteError
520+
If the file does not start with a valid ABF signature (b"ABF " or b"ABF2").
495521
"""
496522
with open(filename, "rb") as fid:
497523
f = StructFile(fid)
@@ -502,7 +528,14 @@ def parse_axon_soup(filename):
502528
elif signature == b"ABF2":
503529
return _parse_abf_v2(f, headerDescriptionV2)
504530
else:
505-
return None
531+
# The first 4 bytes are the ABF magic; anything else means the file is not an ABF
532+
# file, is corrupt, or is an unsupported variant. Raise here rather than returning
533+
# None so the caller gets a clear error instead of a downstream NoneType access.
534+
raise NeoReadWriteError(
535+
f"Could not parse {filename} as an ABF file: expected the header to start with "
536+
f"signature b'ABF ' or b'ABF2', but found {signature}. The file is not an ABF "
537+
f"file, is corrupt, or is an unsupported variant."
538+
)
506539

507540

508541
def _parse_abf_v1(f, header_description):

neo/resources/neuropixels_probe_features.json

Lines changed: 199 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1519,6 +1519,55 @@
15191519
"tip_length_um": "209",
15201520
"total_electrodes": "960"
15211521
},
1522+
"NP1400": {
1523+
"adc_bit_depth": "10",
1524+
"adc_range_vpp": "1.2",
1525+
"ap_band_list_hz": "300,10000",
1526+
"ap_gain_list": "50,125,250,500,1000,1500,2000,3000",
1527+
"ap_sample_frequency_hz": "30000",
1528+
"banks_per_shank": "2.50",
1529+
"blue_emitter0_dist_from_elec0_um": "74",
1530+
"blue_emitter_count": "14",
1531+
"channel_mapping_type": "simple bank",
1532+
"channels_per_bank": "384",
1533+
"cols_per_shank": "2",
1534+
"databus_decoder_type": "1.0",
1535+
"datasheet": "N/A",
1536+
"description": "Opto-II pre-Alpha",
1537+
"electrode_layout_type": "linear",
1538+
"electrode_pitch_horz_um": "48",
1539+
"electrode_pitch_vert_um": "20",
1540+
"electrode_select_api_type": "selectElectrode",
1541+
"electrode_size_horz_direction_um": "12",
1542+
"electrode_size_vert_direction_um": "12",
1543+
"electrodes_per_shank": "960",
1544+
"emitter_pitch_um": "100",
1545+
"even_row_horz_offset_left_edge_to_leftmost_electrode_center_um": "11",
1546+
"has_adc_calibration_file": "Y",
1547+
"has_ap_bandpass": "Y",
1548+
"has_on_chip_gnd_ref": "N",
1549+
"has_tip_ref": "Y",
1550+
"imro_table_format_type": "imro_np1000",
1551+
"is_commercial": "N",
1552+
"lf_band_list_hz": "0.5,500",
1553+
"lf_gain_list": "50,125,250,500,1000,1500,2000,3000",
1554+
"lf_sample_frequency_hz": "2500",
1555+
"mux_table_format_type": "mux_np1000",
1556+
"num_readout_channels": "384",
1557+
"num_shanks": "1",
1558+
"odd_row_horz_offset_left_edge_to_leftmost_electrode_center_um": "11",
1559+
"on_shank_ref_chan": "191",
1560+
"part_number": "NP1400",
1561+
"red_emitter0_dist_from_elec0_um": "68",
1562+
"red_emitter_count": "14",
1563+
"rows_per_shank": "480",
1564+
"shank_pitch_um": "0",
1565+
"shank_thickness_um": "35",
1566+
"shank_tip_to_base_um": "10000",
1567+
"shank_width_um": "70",
1568+
"tip_length_um": "209",
1569+
"total_electrodes": "960"
1570+
},
15221571
"NP2000": {
15231572
"adc_bit_depth": "14",
15241573
"adc_range_vpp": "1",
@@ -2009,6 +2058,104 @@
20092058
"tip_length_um": "206",
20102059
"total_electrodes": "5120"
20112060
},
2061+
"NP2022": {
2062+
"adc_bit_depth": "12",
2063+
"adc_range_vpp": "1.24",
2064+
"ap_band_list_hz": "0.5,10000",
2065+
"ap_gain_list": "100",
2066+
"ap_sample_frequency_hz": "30000",
2067+
"banks_per_shank": "3.33",
2068+
"blue_emitter0_dist_from_elec0_um": "0",
2069+
"blue_emitter_count": "0",
2070+
"channel_mapping_type": "2.0SS scrambled",
2071+
"channels_per_bank": "384",
2072+
"cols_per_shank": "2",
2073+
"databus_decoder_type": "2.0 Ph2C",
2074+
"datasheet": "NP2022 NEUROPIXELS 2.0 QB SAPIENS DATASHEET",
2075+
"description": "Neuropixels 2.0 multi shank probe quad base NHP with cap (Ph 2C), cabling, sharpened, sterile packaging",
2076+
"electrode_layout_type": "staggered",
2077+
"electrode_pitch_horz_um": "20",
2078+
"electrode_pitch_vert_um": "15",
2079+
"electrode_select_api_type": "selectElectrode",
2080+
"electrode_size_horz_direction_um": "12",
2081+
"electrode_size_vert_direction_um": "12",
2082+
"electrodes_per_shank": "1280",
2083+
"emitter_pitch_um": "0",
2084+
"even_row_horz_offset_left_edge_to_leftmost_electrode_center_um": "27",
2085+
"has_adc_calibration_file": "N",
2086+
"has_ap_bandpass": "N",
2087+
"has_on_chip_gnd_ref": "Y",
2088+
"has_tip_ref": "Y",
2089+
"imro_table_format_type": "imro_np2020",
2090+
"is_commercial": "Y",
2091+
"lf_band_list_hz": "0",
2092+
"lf_gain_list": "1",
2093+
"lf_sample_frequency_hz": "0",
2094+
"mux_table_format_type": "mux_np2020",
2095+
"num_readout_channels": "1536",
2096+
"num_shanks": "4",
2097+
"odd_row_horz_offset_left_edge_to_leftmost_electrode_center_um": "11",
2098+
"on_shank_ref_chan": "-1",
2099+
"part_number": "NP2022",
2100+
"red_emitter0_dist_from_elec0_um": "0",
2101+
"red_emitter_count": "0",
2102+
"rows_per_shank": "640",
2103+
"shank_pitch_um": "2141.45",
2104+
"shank_thickness_um": "122",
2105+
"shank_tip_to_base_um": "10000",
2106+
"shank_width_um": "70",
2107+
"tip_length_um": "206",
2108+
"total_electrodes": "5120"
2109+
},
2110+
"NP2300": {
2111+
"adc_bit_depth": "12",
2112+
"adc_range_vpp": "1.24",
2113+
"ap_band_list_hz": "0.5,10000",
2114+
"ap_gain_list": "100",
2115+
"ap_sample_frequency_hz": "30000",
2116+
"banks_per_shank": "3.33",
2117+
"blue_emitter0_dist_from_elec0_um": "56",
2118+
"blue_emitter_count": "44",
2119+
"channel_mapping_type": "2.0SS scrambled",
2120+
"channels_per_bank": "384",
2121+
"cols_per_shank": "2",
2122+
"databus_decoder_type": "2.0 Ph2B",
2123+
"datasheet": "",
2124+
"description": "Opto-II Alpha",
2125+
"electrode_layout_type": "linear",
2126+
"electrode_pitch_horz_um": "48",
2127+
"electrode_pitch_vert_um": "15",
2128+
"electrode_select_api_type": "selectElectrodeMask",
2129+
"electrode_size_horz_direction_um": "12",
2130+
"electrode_size_vert_direction_um": "12",
2131+
"electrodes_per_shank": "1280",
2132+
"emitter_pitch_um": "100",
2133+
"even_row_horz_offset_left_edge_to_leftmost_electrode_center_um": "11",
2134+
"has_adc_calibration_file": "N",
2135+
"has_ap_bandpass": "N",
2136+
"has_on_chip_gnd_ref": "Y",
2137+
"has_tip_ref": "Y",
2138+
"imro_table_format_type": "imro_np2003",
2139+
"is_commercial": "N",
2140+
"lf_band_list_hz": "0",
2141+
"lf_gain_list": "1",
2142+
"lf_sample_frequency_hz": "0",
2143+
"mux_table_format_type": "mux_np2000",
2144+
"num_readout_channels": "384",
2145+
"num_shanks": "1",
2146+
"odd_row_horz_offset_left_edge_to_leftmost_electrode_center_um": "11",
2147+
"on_shank_ref_chan": "-1",
2148+
"part_number": "NP2300",
2149+
"red_emitter0_dist_from_elec0_um": "56",
2150+
"red_emitter_count": "44",
2151+
"rows_per_shank": "640",
2152+
"shank_pitch_um": "0",
2153+
"shank_thickness_um": "30",
2154+
"shank_tip_to_base_um": "10000",
2155+
"shank_width_um": "70",
2156+
"tip_length_um": "206",
2157+
"total_electrodes": "1280"
2158+
},
20122159
"NP3000": {
20132160
"adc_bit_depth": "10",
20142161
"adc_range_vpp": "1.2",
@@ -2156,6 +2303,55 @@
21562303
"tip_length_um": "203",
21572304
"total_electrodes": "1280"
21582305
},
2306+
"NP3012": {
2307+
"adc_bit_depth": "12",
2308+
"adc_range_vpp": "1.34",
2309+
"ap_band_list_hz": "0.5,10000",
2310+
"ap_gain_list": "100",
2311+
"ap_sample_frequency_hz": "30000",
2312+
"banks_per_shank": "1.40",
2313+
"blue_emitter0_dist_from_elec0_um": "0",
2314+
"blue_emitter_count": "0",
2315+
"channel_mapping_type": "simple bank",
2316+
"channels_per_bank": "912",
2317+
"cols_per_shank": "2",
2318+
"databus_decoder_type": "channel reordering",
2319+
"datasheet": "N/A",
2320+
"description": "Neuropixels NXT alpha version B single shank metal cap",
2321+
"electrode_layout_type": "linear",
2322+
"electrode_pitch_horz_um": "32",
2323+
"electrode_pitch_vert_um": "15",
2324+
"electrode_select_api_type": "selectElectrode",
2325+
"electrode_size_horz_direction_um": "12",
2326+
"electrode_size_vert_direction_um": "12",
2327+
"electrodes_per_shank": "1280",
2328+
"emitter_pitch_um": "0",
2329+
"even_row_horz_offset_left_edge_to_leftmost_electrode_center_um": "27",
2330+
"has_adc_calibration_file": "N",
2331+
"has_ap_bandpass": "N",
2332+
"has_on_chip_gnd_ref": "Y",
2333+
"has_tip_ref": "Y",
2334+
"imro_table_format_type": "imro_np3010",
2335+
"is_commercial": "N",
2336+
"lf_band_list_hz": "0",
2337+
"lf_gain_list": "1",
2338+
"lf_sample_frequency_hz": "0",
2339+
"mux_table_format_type": "mux_np3010",
2340+
"num_readout_channels": "1536",
2341+
"num_shanks": "1",
2342+
"odd_row_horz_offset_left_edge_to_leftmost_electrode_center_um": "27",
2343+
"on_shank_ref_chan": "-1",
2344+
"part_number": "NP3012",
2345+
"red_emitter0_dist_from_elec0_um": "0",
2346+
"red_emitter_count": "0",
2347+
"rows_per_shank": "640",
2348+
"shank_pitch_um": "0",
2349+
"shank_thickness_um": "30",
2350+
"shank_tip_to_base_um": "10000",
2351+
"shank_width_um": "70",
2352+
"tip_length_um": "203",
2353+
"total_electrodes": "1280"
2354+
},
21592355
"NP3020": {
21602356
"adc_bit_depth": "12",
21612357
"adc_range_vpp": "1.34",
@@ -2317,7 +2513,7 @@
23172513
"cols_per_shank": "2",
23182514
"databus_decoder_type": "channel reordering",
23192515
"datasheet": "N/A",
2320-
"description": "Neuropixels 3.0 alpha version B multishank silicon cap",
2516+
"description": "Neuropixels NXT alpha version B multishank silicon cap",
23212517
"electrode_layout_type": "linear",
23222518
"electrode_pitch_horz_um": "32",
23232519
"electrode_pitch_vert_um": "15",
@@ -2366,7 +2562,7 @@
23662562
"cols_per_shank": "2",
23672563
"databus_decoder_type": "channel reordering",
23682564
"datasheet": "N/A",
2369-
"description": "Neuropixels 3.0 alpha version B multishank metal cap",
2565+
"description": "Neuropixels NXT alpha version B multishank metal cap",
23702566
"electrode_layout_type": "linear",
23712567
"electrode_pitch_horz_um": "32",
23722568
"electrode_pitch_vert_um": "15",
@@ -2695,7 +2891,7 @@
26952891
"imro_np3020_val_def": "type:pn|{3020,3022} num_channels:num_readout_channels channel:[0,num_readout_channels-1] shank:[0,num_shanks-1] bank:[0,banks_per_shank-1] ref_id:(0,ext)(1,gnd)(2,tip0)(3,tip1)(4,tip2)(5,tip3) electrode:[0,electrodes_per_shank-1]"
26962892
},
26972893
"z_meta": {
2698-
"table_version": "1.7"
2894+
"table_version": "1.8"
26992895
},
27002896
"z_mux_tables": {
27012897
"mux_np1000": "(32,12)(0 1 24 25 48 49 72 73 96 97 120 121 144 145 168 169 192 193 216 217 240 241 264 265 288 289 312 313 336 337 360 361)(2 3 26 27 50 51 74 75 98 99 122 123 146 147 170 171 194 195 218 219 242 243 266 267 290 291 314 315 338 339 362 363)(4 5 28 29 52 53 76 77 100 101 124 125 148 149 172 173 196 197 220 221 244 245 268 269 292 293 316 317 340 341 364 365)(6 7 30 31 54 55 78 79 102 103 126 127 150 151 174 175 198 199 222 223 246 247 270 271 294 295 318 319 342 343 366 367)(8 9 32 33 56 57 80 81 104 105 128 129 152 153 176 177 200 201 224 225 248 249 272 273 296 297 320 321 344 345 368 369)(10 11 34 35 58 59 82 83 106 107 130 131 154 155 178 179 202 203 226 227 250 251 274 275 298 299 322 323 346 347 370 371)(12 13 36 37 60 61 84 85 108 109 132 133 156 157 180 181 204 205 228 229 252 253 276 277 300 301 324 325 348 349 372 373)(14 15 38 39 62 63 86 87 110 111 134 135 158 159 182 183 206 207 230 231 254 255 278 279 302 303 326 327 350 351 374 375)(16 17 40 41 64 65 88 89 112 113 136 137 160 161 184 185 208 209 232 233 256 257 280 281 304 305 328 329 352 353 376 377)(18 19 42 43 66 67 90 91 114 115 138 139 162 163 186 187 210 211 234 235 258 259 282 283 306 307 330 331 354 355 378 379)(20 21 44 45 68 69 92 93 116 117 140 141 164 165 188 189 212 213 236 237 260 261 284 285 308 309 332 333 356 357 380 381)(22 23 46 47 70 71 94 95 118 119 142 143 166 167 190 191 214 215 238 239 262 263 286 287 310 311 334 335 358 359 382 383)",

neo/test/rawiotest/test_axonrawio.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import unittest
33

44
from neo.rawio.axonrawio import AxonRawIO, parse_axon_soup
5+
from neo.core import NeoReadWriteError
56

67
from neo.test.rawiotest.common_rawio_test import BaseTestRawIO
78

@@ -29,6 +30,43 @@ def test_read_raw_protocol(self):
2930

3031
reader.read_raw_protocol()
3132

33+
def test_integer_overflow_size_raises(self):
34+
# An ABF header that claims more samples than the file can hold must raise a
35+
# clear error instead of silently returning an overflowed signal size.
36+
path = self.get_local_path("axon/intracellular_data/files_with_errors/integer_overflow_size.abf")
37+
expected_error = (
38+
"ABF header implies 3221225472 samples ending at byte 6442457600, which exceeds the "
39+
f"file size of 8704 bytes for {path}; the file header is corrupt or the file is truncated."
40+
)
41+
reader = AxonRawIO(filename=path)
42+
with self.assertRaises(NeoReadWriteError) as cm:
43+
reader.parse_header()
44+
self.assertEqual(str(cm.exception), expected_error)
45+
46+
def test_negative_segment_size_raises(self):
47+
# An ABF header with a negative segment size must raise a clear error
48+
# instead of silently returning a negative signal size.
49+
path = self.get_local_path("axon/intracellular_data/files_with_errors/negative_synch_length.abf")
50+
expected_error = f"Negative segment size (-1041598657) parsed from {path}; the file header is corrupt."
51+
reader = AxonRawIO(filename=path)
52+
with self.assertRaises(NeoReadWriteError) as cm:
53+
reader.parse_header()
54+
self.assertEqual(str(cm.exception), expected_error)
55+
56+
def test_unparseable_file_raises(self):
57+
# A file whose header does not start with a valid ABF signature must raise a clear error
58+
# rather than a cryptic NoneType error deep in parsing. The fixture has a zeroed signature.
59+
path = self.get_local_path("axon/intracellular_data/files_with_errors/unparseable_header.abf")
60+
expected_msg = (
61+
f"Could not parse {path} as an ABF file: expected the header to start with signature "
62+
f"b'ABF ' or b'ABF2', but found b'\\x00\\x00\\x00\\x00'. The file is not an ABF file, "
63+
f"is corrupt, or is an unsupported variant."
64+
)
65+
reader = AxonRawIO(filename=path)
66+
with self.assertRaises(NeoReadWriteError) as cm:
67+
reader.parse_header()
68+
self.assertEqual(str(cm.exception), expected_msg)
69+
3270
def test_v1_reads_real_acquisition_date(self):
3371
# ABF1 stores the calendar date in lFileStartDate (a YYYYMMDD-packed integer). Older neo
3472
# ignored that field and hardcoded 1900-01-01, so the recording date was always wrong for

0 commit comments

Comments
 (0)