Skip to content

Commit b83cee6

Browse files
committed
Blackrock: read NEV 2.2 spikes on electrodes above 255
Ripple Trellis writes 2.2 files with electrode IDs by front-end port (1-128, 129-256, 257-384...). Spike packets of a 2.2 file were recognized only for IDs 1-255, so every spike on a higher electrode matched no packet type and was dropped without a warning. The 2.2 range is now 1-2048, as for 2.3. Also document next to the table which packet IDs of 3.0 files written by Central are ignored on purpose (preview packets, recording events).
1 parent 442658a commit b83cee6

2 files changed

Lines changed: 48 additions & 1 deletion

File tree

‎neo/rawio/blackrockrawio.py‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2464,14 +2464,24 @@ def _is_set(self, flag, pos):
24642464
# Used to create masks that filter raw data packets by their packet ID field.
24652465
# Single values indicate equality check, tuples (min, max) indicate range check.
24662466
# According to NEV spec: packet IDs < 32768 identify channels, IDs >= 32768 are system events.
2467+
# Packets whose ID is not listed for a spec are not read. Known cases, ignored on purpose:
2468+
# - 0x8000 + channel (e.g. 32769): preview packets of the live protocol (cbproto's "Preview Data Packet
2469+
# Definitions"), found in 3.0 files written by Central. They carry no neural data.
2470+
# - 0xFFF9: recording events of 3.0 files written by Central (header type 0 start, 1 stop, 2 pause,
2471+
# 3 resume; Central's CNevFile30::AddRecordingEvent). Not used to split segments, which come from
2472+
# resets and gaps.
24672473
NEV_PACKET_IDENTIFIERS_BY_SPEC = {
24682474
"2.1": {
24692475
"NonNeural": 0,
24702476
"Spikes": (1, 255), # Packet IDs in this range identify spike events on electrodes
24712477
},
24722478
"2.2": {
24732479
"NonNeural": 0,
2474-
"Spikes": (1, 255), # Packet IDs in this range identify spike events on electrodes
2480+
# Packet IDs in this range identify spike events on electrodes. Wider than 2.1's range:
2481+
# Ripple Trellis writes 2.2 files with electrode IDs above 255 (its front-end ports are
2482+
# numbered 1-128, 129-256, 257-384...), and every spike on them was dropped without a
2483+
# warning (NeuralEnsemble/python-neo#1918). 2048 is the range of 2.3.
2484+
"Spikes": (1, 2048),
24752485
},
24762486
"2.3": {
24772487
"NonNeural": 0,

‎neo/test/rawiotest/test_blackrockrawio.py‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
Tests of neo.rawio.examplerawio
33
"""
44

5+
import tempfile
56
import unittest
7+
from pathlib import Path
68

79
from neo.rawio.blackrockrawio import BlackrockRawIO
810
from neo.test.rawiotest.common_rawio_test import BaseTestRawIO
@@ -374,6 +376,41 @@ def test_gap_tolerance_ms_parameter(self):
374376
segments_strict = reader_strict.segment_count(0)
375377
self.assertEqual(segments_strict, 3) #
376378

379+
def test_nev_22_reads_spikes_on_electrodes_above_255(self):
380+
"""
381+
A FileSpec 2.2 NEV with spikes on an electrode above 255 (Ripple Trellis numbers its front-end ports
382+
1-128, 129-256, 257-384...) keeps them, as 2.3 does (NeuralEnsemble/python-neo#1918). Built from neo's
383+
2.3 test NEV: marked 2.2, with electrode 1 renumbered 300 in its NEUEVWAV header and spike packets.
384+
"""
385+
source_base = self.get_local_path("blackrock/FileSpec2.3001")
386+
original = BlackrockRawIO(filename=source_base, nsx_to_load=None)
387+
original.parse_header()
388+
units_on_1 = [i for i, (channel, _) in enumerate(original.internal_unit_ids) if channel == 1]
389+
self.assertTrue(units_on_1) # electrode 1 has spikes to move
390+
expected = np.concatenate([original.get_spike_timestamps(0, 0, i, None, None) for i in units_on_1])
391+
392+
data = bytearray(Path(source_base + ".nev").read_bytes())
393+
data[9] = 2 # FileSpec 2.3 -> 2.2
394+
header_size, packet_size = np.frombuffer(bytes(data[12:20]), dtype="<u4")
395+
for at in range(336, int(header_size), 32): # extended headers follow the 336-byte basic header
396+
if data[at : at + 8] == b"NEUEVWAV" and int.from_bytes(data[at + 8 : at + 10], "little") == 1:
397+
data[at + 8 : at + 10] = (300).to_bytes(2, "little")
398+
# 2.x packets: a uint32 timestamp, then the uint16 packet id
399+
for at in range(int(header_size), len(data) - int(packet_size) + 1, int(packet_size)):
400+
if int.from_bytes(data[at + 4 : at + 6], "little") == 1:
401+
data[at + 4 : at + 6] = (300).to_bytes(2, "little")
402+
403+
with tempfile.TemporaryDirectory() as folder:
404+
base = str(Path(folder) / "trellis_like")
405+
Path(base + ".nev").write_bytes(bytes(data))
406+
reader = BlackrockRawIO(filename=base, nsx_to_load=None)
407+
reader.parse_header()
408+
self.assertEqual(reader._nev_spec, "2.2")
409+
units_on_300 = [i for i, (channel, _) in enumerate(reader.internal_unit_ids) if channel == 300]
410+
self.assertTrue(units_on_300, "no spike channel on electrode 300")
411+
got = np.concatenate([reader.get_spike_timestamps(0, 0, i, None, None) for i in units_on_300])
412+
assert_equal(np.sort(got), np.sort(expected))
413+
377414

378415
if __name__ == "__main__":
379416
unittest.main()

0 commit comments

Comments
 (0)