@@ -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
0 commit comments