Skip to content

Commit 18591ec

Browse files
Merge pull request #1174 from SachsLab/cboulay/br_filespec_3_0
BlackrockRawIO FileSpec 3.0
2 parents 0199770 + 86bef21 commit 18591ec

File tree

3 files changed

+166
-34
lines changed

3 files changed

+166
-34
lines changed

neo/rawio/blackrockrawio.py

Lines changed: 162 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -179,44 +179,64 @@ def __init__(self, filename=None, nsx_override=None, nev_override=None,
179179
self.__nsx_header_reader = {
180180
'2.1': self.__read_nsx_header_variant_a,
181181
'2.2': self.__read_nsx_header_variant_b,
182-
'2.3': self.__read_nsx_header_variant_b}
182+
'2.3': self.__read_nsx_header_variant_b,
183+
'3.0': self.__read_nsx_header_variant_b,
184+
}
183185
self.__nsx_dataheader_reader = {
184186
'2.1': self.__read_nsx_dataheader_variant_a,
185187
'2.2': self.__read_nsx_dataheader_variant_b,
186-
'2.3': self.__read_nsx_dataheader_variant_b}
188+
'2.3': self.__read_nsx_dataheader_variant_b,
189+
'3.0': self.__read_nsx_dataheader_variant_b
190+
}
187191
self.__nsx_data_reader = {
188192
'2.1': self.__read_nsx_data_variant_a,
189193
'2.2': self.__read_nsx_data_variant_b,
190-
'2.3': self.__read_nsx_data_variant_b}
194+
'2.3': self.__read_nsx_data_variant_b,
195+
'3.0': self.__read_nsx_data_variant_b
196+
}
191197
self.__nsx_params = {
192198
'2.1': self.__get_nsx_param_variant_a,
193199
'2.2': self.__get_nsx_param_variant_b,
194-
'2.3': self.__get_nsx_param_variant_b}
200+
'2.3': self.__get_nsx_param_variant_b,
201+
'3.0': self.__get_nsx_param_variant_b
202+
}
195203
# NEV
196204
self.__nev_header_reader = {
197205
'2.1': self.__read_nev_header_variant_a,
198206
'2.2': self.__read_nev_header_variant_b,
199-
'2.3': self.__read_nev_header_variant_c}
207+
'2.3': self.__read_nev_header_variant_c,
208+
'3.0': self.__read_nev_header_variant_c,
209+
}
200210
self.__nev_data_reader = {
201211
'2.1': self.__read_nev_data_variant_a,
202212
'2.2': self.__read_nev_data_variant_a,
203-
'2.3': self.__read_nev_data_variant_b}
213+
'2.3': self.__read_nev_data_variant_b,
214+
'3.0': self.__read_nev_data_variant_c
215+
}
204216
self.__waveform_size = {
205217
'2.1': self.__get_waveform_size_variant_a,
206218
'2.2': self.__get_waveform_size_variant_a,
207-
'2.3': self.__get_waveform_size_variant_b}
219+
'2.3': self.__get_waveform_size_variant_b,
220+
'3.0': self.__get_waveform_size_variant_b
221+
}
208222
self.__channel_labels = {
209223
'2.1': self.__get_channel_labels_variant_a,
210224
'2.2': self.__get_channel_labels_variant_b,
211-
'2.3': self.__get_channel_labels_variant_b}
225+
'2.3': self.__get_channel_labels_variant_b,
226+
'3.0': self.__get_channel_labels_variant_b
227+
}
212228
self.__nonneural_evdicts = {
213229
'2.1': self.__get_nonneural_evdicts_variant_a,
214230
'2.2': self.__get_nonneural_evdicts_variant_a,
215-
'2.3': self.__get_nonneural_evdicts_variant_b}
231+
'2.3': self.__get_nonneural_evdicts_variant_b,
232+
'3.0': self.__get_nonneural_evdicts_variant_b
233+
}
216234
self.__comment_evdict = {
217235
'2.1': self.__get_comment_evdict_variant_a,
218236
'2.2': self.__get_comment_evdict_variant_a,
219-
'2.3': self.__get_comment_evdict_variant_a}
237+
'2.3': self.__get_comment_evdict_variant_a,
238+
'3.0': self.__get_comment_evdict_variant_a
239+
}
220240

221241
def _parse_header(self):
222242

@@ -345,7 +365,7 @@ def _parse_header(self):
345365
sr = float(main_sampling_rate / self.__nsx_basic_header[nsx_nb]['period'])
346366
self.sig_sampling_rates[nsx_nb] = sr
347367

348-
if spec in ['2.2', '2.3']:
368+
if spec in ['2.2', '2.3', '3.0']:
349369
ext_header = self.__nsx_ext_header[nsx_nb]
350370
elif spec == '2.1':
351371
ext_header = []
@@ -361,7 +381,7 @@ def _parse_header(self):
361381
if len(ext_header) > 0:
362382
signal_streams.append((f'nsx{nsx_nb}', str(nsx_nb)))
363383
for i, chan in enumerate(ext_header):
364-
if spec in ['2.2', '2.3']:
384+
if spec in ['2.2', '2.3', '3.0']:
365385
ch_name = chan['electrode_label'].decode()
366386
ch_id = str(chan['electrode_id'])
367387
units = chan['units'].decode()
@@ -728,7 +748,7 @@ def __extract_nsx_file_spec(self, nsx_nb):
728748
nsx_file_id = np.fromfile(filename, count=1, dtype=dt0)[0]
729749
if nsx_file_id['file_id'].decode() == 'NEURALSG':
730750
spec = '2.1'
731-
elif nsx_file_id['file_id'].decode() == 'NEURALCD':
751+
elif nsx_file_id['file_id'].decode() in ['NEURALCD', 'BRSMPGRP']:
732752
spec = '{}.{}'.format(
733753
nsx_file_id['ver_major'], nsx_file_id['ver_minor'])
734754
else:
@@ -749,12 +769,12 @@ def __extract_nev_file_spec(self):
749769
('ver_minor', 'uint8')]
750770

751771
nev_file_id = np.fromfile(filename, count=1, dtype=dt0)[0]
752-
if nev_file_id['file_id'].decode() == 'NEURALEV':
772+
if nev_file_id['file_id'].decode() in ['NEURALEV', 'BREVENTS']:
753773
spec = '{}.{}'.format(
754774
nev_file_id['ver_major'], nev_file_id['ver_minor'])
755775
else:
756776
raise IOError('NEV file type {} is not supported'.format(
757-
nev_file_id['file_id']))
777+
nev_file_id['file_id'].decode()))
758778

759779
return spec
760780

@@ -797,7 +817,7 @@ def __read_nsx_header_variant_b(self, nsx_nb):
797817

798818
# basic header (file_id: NEURALCD)
799819
dt0 = [
800-
('file_id', 'S8'),
820+
('file_id', 'S8'), # achFileType
801821
# file specification split into major and minor version number
802822
('ver_major', 'uint8'),
803823
('ver_minor', 'uint8'),
@@ -859,10 +879,12 @@ def __read_nsx_dataheader(self, nsx_nb, offset):
859879
"""
860880
filename = '.'.join([self._filenames['nsx'], 'ns%i' % nsx_nb])
861881

882+
ts_size = 'uint64' if self.__nsx_basic_header[nsx_nb]['ver_major'] >= 3 else 'uint32'
883+
862884
# dtypes data header
863885
dt2 = [
864886
('header', 'uint8'),
865-
('timestamp', 'uint32'),
887+
('timestamp', ts_size),
866888
('nb_data_points', 'uint32')]
867889

868890
return np.memmap(filename, dtype=dt2, shape=1, offset=offset, mode='r')[0]
@@ -1074,11 +1096,18 @@ def __read_nev_data(self, nev_data_masks, nev_data_types):
10741096
data_size = self.__nev_basic_header['bytes_in_data_packets']
10751097
header_size = self.__nev_basic_header['bytes_in_headers']
10761098

1099+
if self.__nev_basic_header['ver_major'] >= 3:
1100+
ts_format = 'uint64'
1101+
header_skip = 10
1102+
else:
1103+
ts_format = 'uint32'
1104+
header_skip = 6
1105+
10771106
# read all raw data packets and markers
10781107
dt0 = [
1079-
('timestamp', 'uint32'),
1108+
('timestamp', ts_format),
10801109
('packet_id', 'uint16'),
1081-
('value', 'S{}'.format(data_size - 6))]
1110+
('value', 'S{}'.format(data_size - header_skip))]
10821111

10831112
raw_data = np.memmap(filename, offset=header_size, dtype=dt0, mode='r')
10841113

@@ -1113,14 +1142,18 @@ def __get_event_segment_ids(self, raw_event_data, masks, nev_data_masks):
11131142
# No pause or reset mechanism present for file version 2.1 and 2.2
11141143
return np.zeros(len(raw_event_data), dtype=int)
11151144

1116-
elif self.__nev_spec == '2.3':
1145+
elif self.__nev_spec in ['2.3', '3.0']:
11171146
reset_ev_mask = self.__get_reset_event_mask(raw_event_data, masks, nev_data_masks)
11181147
reset_ev_ids = np.where(reset_ev_mask)[0]
11191148

11201149
# consistency check for monotone increasing time stamps
1121-
# explicitly converting to int to allow for negative diff values
1122-
jump_ids = \
1123-
np.where(np.diff(np.asarray(raw_event_data['timestamp'], dtype=int)) < 0)[0] + 1
1150+
# - Use logical comparator (instead of np.diff) to avoid unsigned dtype issues.
1151+
# - Only consider handled/known event types.
1152+
mask_handled = np.any([value[nev_data_masks[key]] for key, value in masks.items()], axis=0)
1153+
jump_ids_handled = np.where(
1154+
raw_event_data['timestamp'][mask_handled][1:] < raw_event_data['timestamp'][mask_handled][:-1]
1155+
)[0] + 1
1156+
jump_ids = np.where(mask_handled)[0][jump_ids_handled] # jump ids in full set of events (incl. unhandled)
11241157
overlap = np.in1d(jump_ids, reset_ev_ids)
11251158
if not all(overlap):
11261159
# additional resets occurred without a reset event being stored
@@ -1137,6 +1170,9 @@ def __get_event_segment_ids(self, raw_event_data, masks, nev_data_masks):
11371170
self._nb_segment_nev = len(reset_ev_ids) + 1
11381171
return event_segment_ids
11391172

1173+
else:
1174+
raise ValueError("Unknown File Spec {}".formate(self.__nev_spec))
1175+
11401176
def __match_nsx_and_nev_segment_ids(self, nsx_nb):
11411177
"""
11421178
Ensure matching ids of segments detected in nsx and nev file for version 2.3
@@ -1278,6 +1314,30 @@ def __read_nev_data_variant_b(self):
12781314

12791315
return self.__read_nev_data(nev_data_masks, nev_data_types)
12801316

1317+
def __read_nev_data_variant_c(self):
1318+
"""
1319+
Extract nev data from a 3.0 .nev file
1320+
"""
1321+
nev_data_masks = {
1322+
'NonNeural': 'a',
1323+
'Spikes': 'b',
1324+
'Comments': 'a',
1325+
'VideoSync': 'a',
1326+
'TrackingEvents': 'a',
1327+
'ButtonTrigger': 'a',
1328+
'ConfigEvent': 'a'}
1329+
1330+
nev_data_types = {
1331+
'NonNeural': 'c',
1332+
'Spikes': 'b',
1333+
'Comments': 'b',
1334+
'VideoSync': 'b',
1335+
'TrackingEvents': 'b',
1336+
'ButtonTrigger': 'b',
1337+
'ConfigEvent': 'b'}
1338+
1339+
return self.__read_nev_data(nev_data_masks, nev_data_types)
1340+
12811341
def __nev_ext_header_types(self):
12821342
"""
12831343
Defines extended header types for different .nev file specifications.
@@ -1440,30 +1500,61 @@ def __nev_data_types(self, data_size):
14401500
('analog_input_channel_3', 'int16'),
14411501
('analog_input_channel_4', 'int16'),
14421502
('analog_input_channel_5', 'int16'),
1443-
('unused', 'S{}'.format(data_size - 20))],
1444-
# Version>=2.3
1503+
('unused', 'S{}'.format(data_size - 20))
1504+
],
1505+
# Version=2.3
14451506
'b': [
14461507
('timestamp', 'uint32'),
14471508
('packet_id', 'uint16'),
14481509
('packet_insertion_reason', 'uint8'),
14491510
('reserved', 'uint8'),
14501511
('digital_input', 'uint16'),
1451-
('unused', 'S{}'.format(data_size - 10))]},
1512+
('unused', 'S{}'.format(data_size - 10))
1513+
],
1514+
# Version >= 3.0
1515+
'c': [
1516+
('timestamp', 'uint64'),
1517+
('packet_id', 'uint16'),
1518+
('packet_insertion_reason', 'uint8'),
1519+
('dlen', 'uint8'),
1520+
('digital_input', 'uint16'),
1521+
('unused', 'S{}'.format(data_size - 14))
1522+
]
1523+
},
14521524
'Spikes': {
14531525
'a': [
14541526
('timestamp', 'uint32'),
14551527
('packet_id', 'uint16'),
14561528
('unit_class_nb', 'uint8'),
14571529
('reserved', 'uint8'),
1458-
('waveform', 'S{}'.format(data_size - 8))]},
1530+
('waveform', 'S{}'.format(data_size - 8))
1531+
],
1532+
'b': [
1533+
('timestamp', 'uint64'),
1534+
('packet_id', 'uint16'),
1535+
('unit_class_nb', 'uint8'),
1536+
('dlen', 'uint8'),
1537+
('waveform', 'S{}'.format(data_size - 12))
1538+
]
1539+
},
14591540
'Comments': {
14601541
'a': [
14611542
('timestamp', 'uint32'),
14621543
('packet_id', 'uint16'),
14631544
('char_set', 'uint8'),
14641545
('flag', 'uint8'),
14651546
('color', 'uint32'),
1466-
('comment', 'S{}'.format(data_size - 12))]},
1547+
('comment', 'S{}'.format(data_size - 12))
1548+
],
1549+
'b': [
1550+
('timestamp', 'uint64'),
1551+
('packet_id', 'uint16'),
1552+
('char_set', 'uint8'),
1553+
('flag', 'uint8'),
1554+
('color', 'uint32'),
1555+
('comment', 'S{}'.format(data_size - 16))
1556+
]
1557+
},
14671558
'VideoSync': {
14681559
'a': [
14691560
('timestamp', 'uint32'),
@@ -1472,7 +1563,18 @@ def __nev_data_types(self, data_size):
14721563
('video_frame_nb', 'uint32'),
14731564
('video_elapsed_time', 'uint32'),
14741565
('video_source_id', 'uint32'),
1475-
('unused', 'int8', (data_size - 20,))]},
1566+
('unused', 'int8', (data_size - 20,))
1567+
],
1568+
'b': [
1569+
('timestamp', 'uint64'),
1570+
('packet_id', 'uint16'),
1571+
('video_file_nb', 'uint16'),
1572+
('video_frame_nb', 'uint32'),
1573+
('video_elapsed_time', 'uint32'),
1574+
('video_source_id', 'uint32'),
1575+
('unused', 'int8', (data_size - 24,))
1576+
]
1577+
},
14761578
'TrackingEvents': {
14771579
'a': [
14781580
('timestamp', 'uint32'),
@@ -1481,19 +1583,47 @@ def __nev_data_types(self, data_size):
14811583
('node_id', 'uint16'),
14821584
('node_count', 'uint16'),
14831585
('point_count', 'uint16'),
1484-
('tracking_points', 'uint16', ((data_size - 14) // 2,))]},
1586+
('tracking_points', 'uint16', ((data_size - 14) // 2,))
1587+
],
1588+
'b': [
1589+
('timestamp', 'uint64'),
1590+
('packet_id', 'uint16'),
1591+
('parent_id', 'uint16'),
1592+
('node_id', 'uint16'),
1593+
('node_count', 'uint16'),
1594+
('point_count', 'uint16'),
1595+
('tracking_points', 'uint16', ((data_size - 18) // 2,))
1596+
]
1597+
},
14851598
'ButtonTrigger': {
14861599
'a': [
14871600
('timestamp', 'uint32'),
14881601
('packet_id', 'uint16'),
14891602
('trigger_type', 'uint16'),
1490-
('unused', 'int8', (data_size - 8,))]},
1603+
('unused', 'int8', (data_size - 8,))
1604+
],
1605+
'b': [
1606+
('timestamp', 'uint64'),
1607+
('packet_id', 'uint16'),
1608+
('trigger_type', 'uint16'),
1609+
('unused', 'int8', (data_size - 12,))
1610+
]
1611+
},
14911612
'ConfigEvent': {
14921613
'a': [
14931614
('timestamp', 'uint32'),
14941615
('packet_id', 'uint16'),
14951616
('config_change_type', 'uint16'),
1496-
('config_changed', 'S{}'.format(data_size - 8))]}}
1617+
('config_changed', 'S{}'.format(data_size - 8))
1618+
],
1619+
'b': [
1620+
('timestamp', 'uint64'),
1621+
('packet_id', 'uint16'),
1622+
('config_change_type', 'uint16'),
1623+
('config_changed', 'S{}'.format(data_size - 12))
1624+
]
1625+
}
1626+
}
14971627

14981628
return __nev_data_types
14991629

neo/test/iotest/test_blackrockio.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ class CommonTests(BaseTestIO, unittest.TestCase):
4242
]
4343
entities_to_test = [
4444
'blackrock/FileSpec2.3001',
45-
'blackrock/blackrock_2_1/l101210-001'
45+
'blackrock/blackrock_2_1/l101210-001',
46+
'blackrock/blackrock_3_0/file_spec_3_0'
4647
]
4748

4849
def test_load_waveforms(self):

neo/test/rawiotest/test_blackrockrawio.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ class TestBlackrockRawIO(BaseTestRawIO, unittest.TestCase, ):
2525
]
2626
entities_to_test = [
2727
'blackrock/FileSpec2.3001',
28-
'blackrock/blackrock_2_1/l101210-001'
28+
'blackrock/blackrock_2_1/l101210-001',
29+
'blackrock/blackrock_3_0/file_spec_3_0'
2930
]
3031

3132
@unittest.skipUnless(HAVE_SCIPY, "requires scipy")

0 commit comments

Comments
 (0)