77 * Samuel Garcia - third version
88 * Lyuba Zehl, Michael Denker - fourth version
99 * Samuel Garcia, Julia Srenger - fifth version
10+ * Chadwick Boulay - FileSpec 3.0 and 3.0-PTP
1011
1112This IO supports reading only.
1213This IO is able to read:
1718 * 2.1
1819 * 2.2
1920 * 2.3
21+ * 3.0
22+ * 3.0 with PTP timestamps (Gemini systems)
2023
2124The neural data channels are 1 - 128.
2225The analog inputs are 129 - 144. (129 - 137 AC coupled, 138 - 144 DC coupled)
@@ -191,12 +194,14 @@ def __init__(
191194 "2.2" : self .__read_nsx_dataheader_variant_b ,
192195 "2.3" : self .__read_nsx_dataheader_variant_b ,
193196 "3.0" : self .__read_nsx_dataheader_variant_b ,
197+ "3.0-ptp" : self .__read_nsx_dataheader_variant_c ,
194198 }
195199 self .__nsx_data_reader = {
196200 "2.1" : self .__read_nsx_data_variant_a ,
197201 "2.2" : self .__read_nsx_data_variant_b ,
198202 "2.3" : self .__read_nsx_data_variant_b ,
199203 "3.0" : self .__read_nsx_data_variant_b ,
204+ "3.0-ptp" : self .__read_nsx_data_variant_c ,
200205 }
201206 self .__nsx_params = {
202207 "2.1" : self .__get_nsx_param_variant_a ,
@@ -312,11 +317,17 @@ def _parse_header(self):
312317 # read nsx headers
313318 self .__nsx_basic_header [nsx_nb ], self .__nsx_ext_header [nsx_nb ] = self .__nsx_header_reader [spec ](nsx_nb )
314319
315- # Read nsx data header(s)
320+ # The only way to know if it is the PTP-variant of file spec 3.0
321+ # is to check for nanosecond timestamp resolution.
322+ if "timestamp_resolution" in self .__nsx_basic_header [nsx_nb ].dtype .names \
323+ and self .__nsx_basic_header [nsx_nb ]["timestamp_resolution" ] == 1_000_000_000 :
324+ nsx_dataheader_reader = self .__nsx_dataheader_reader ["3.0-ptp" ]
325+ else :
326+ nsx_dataheader_reader = self .__nsx_dataheader_reader [spec ]
316327 # for nsxdef get_analogsignal_shape(self, block_index, seg_index):
317- self .__nsx_data_header [nsx_nb ] = self . __nsx_dataheader_reader [ spec ] (nsx_nb )
328+ self .__nsx_data_header [nsx_nb ] = nsx_dataheader_reader (nsx_nb )
318329
319- # nsx_to_load can be either int, list, 'max', all' (aka None)
330+ # nsx_to_load can be either int, list, 'max', ' all' (aka None)
320331 # here make a list only
321332 if self .nsx_to_load is None or self .nsx_to_load == "all" :
322333 self .nsx_to_load = list (self ._avail_nsx )
@@ -359,7 +370,14 @@ def _parse_header(self):
359370 if len (self .nsx_to_load ) > 0 :
360371 for nsx_nb in self .nsx_to_load :
361372 spec = self .__nsx_spec [nsx_nb ]
362- self .nsx_datas [nsx_nb ] = self .__nsx_data_reader [spec ](nsx_nb )
373+ # The only way to know if it is the PTP-variant of file spec 3.0
374+ # is to check for nanosecond timestamp resolution.
375+ if "timestamp_resolution" in self .__nsx_basic_header [nsx_nb ].dtype .names \
376+ and self .__nsx_basic_header [nsx_nb ]["timestamp_resolution" ] == 1_000_000_000 :
377+ _data_reader_fun = self .__nsx_data_reader ["3.0-ptp" ]
378+ else :
379+ _data_reader_fun = self .__nsx_data_reader [spec ]
380+ self .nsx_datas [nsx_nb ] = _data_reader_fun (nsx_nb )
363381
364382 sr = float (main_sampling_rate / self .__nsx_basic_header [nsx_nb ]["period" ])
365383 self .sig_sampling_rates [nsx_nb ] = sr
@@ -415,15 +433,28 @@ def _parse_header(self):
415433 for data_bl in range (self ._nb_segment ):
416434 t_stop = 0.0
417435 for nsx_nb in self .nsx_to_load :
436+ spec = self .__nsx_spec [nsx_nb ]
437+ if "timestamp_resolution" in self .__nsx_basic_header [nsx_nb ].dtype .names :
438+ ts_res = self .__nsx_basic_header [nsx_nb ]["timestamp_resolution" ]
439+ elif spec == "2.1" :
440+ ts_res = self .__nsx_params [spec ](nsx_nb )['timestamp_resolution' ]
441+ else :
442+ ts_res = 30_000
443+ period = self .__nsx_basic_header [nsx_nb ]["period" ]
444+ sec_per_samp = period / 30_000 # Maybe 30_000 should be ['sample_resolution']
418445 length = self .nsx_datas [nsx_nb ][data_bl ].shape [0 ]
419446 if self .__nsx_data_header [nsx_nb ] is None :
420447 t_start = 0.0
448+ t_stop = max (t_stop , length / self .sig_sampling_rates [nsx_nb ])
421449 else :
422- t_start = (
423- self .__nsx_data_header [nsx_nb ][data_bl ]["timestamp" ]
424- / self .__nsx_basic_header [nsx_nb ]["timestamp_resolution" ]
425- )
426- t_stop = max (t_stop , t_start + length / self .sig_sampling_rates [nsx_nb ])
450+ timestamps = self .__nsx_data_header [nsx_nb ][data_bl ]["timestamp" ]
451+ if hasattr (timestamps , "size" ) and timestamps .size == length :
452+ # FileSpec 3.0 with PTP -- use the per-sample timestamps
453+ t_start = timestamps [0 ] / ts_res
454+ t_stop = max (t_stop , timestamps [- 1 ] / ts_res + sec_per_samp )
455+ else :
456+ t_start = timestamps / ts_res
457+ t_stop = max (t_stop , t_start + length / self .sig_sampling_rates [nsx_nb ])
427458 self ._sigs_t_starts [nsx_nb ].append (t_start )
428459
429460 if self ._avail_files ["nev" ]:
@@ -794,6 +825,8 @@ def __read_nsx_header_variant_a(self, nsx_nb):
794825 ]
795826
796827 nsx_basic_header = np .fromfile (filename , count = 1 , dtype = dt0 )[0 ]
828+ # Note: it is not possible to use recfunctions to append_fields of 'timestamp_resolution',
829+ # because the size of this object is used as the header size in later read operations.
797830
798831 # "extended" header (last field of file_id: NEURALCD)
799832 # (to facilitate compatibility with higher file specs)
@@ -901,7 +934,7 @@ def __read_nsx_dataheader_variant_b(
901934 ):
902935 """
903936 Reads the nsx data header for each data block following the offset of
904- file spec 2.2 and 2.3.
937+ file spec 2.2, 2.3, and 3.0 .
905938 """
906939 filename = "." .join ([self ._filenames ["nsx" ], f"ns{ nsx_nb } " ])
907940
@@ -932,6 +965,67 @@ def __read_nsx_dataheader_variant_b(
932965
933966 return data_header
934967
968+ def __read_nsx_dataheader_variant_c (
969+ self , nsx_nb , filesize = None , offset = None , ):
970+ """
971+ Reads the nsx data header for each data block for file spec 3.0 with PTP timestamps
972+ """
973+ filename = "." .join ([self ._filenames ["nsx" ], f"ns{ nsx_nb } " ])
974+
975+ filesize = self .__get_file_size (filename )
976+
977+ data_header = {}
978+ index = 0
979+
980+ if offset is None :
981+ offset = self .__nsx_basic_header [nsx_nb ]["bytes_in_headers" ]
982+
983+ ptp_dt = [
984+ ("reserved" , "uint8" ),
985+ ("timestamps" , "uint64" ),
986+ ("num_data_points" , "uint32" ),
987+ ("samples" , "int16" , self .__nsx_basic_header [nsx_nb ]["channel_count" ])
988+ ]
989+ npackets = int ((filesize - offset ) / np .dtype (ptp_dt ).itemsize )
990+ struct_arr = np .memmap (filename , dtype = ptp_dt , shape = npackets , offset = offset , mode = "r" )
991+
992+ if not np .all (struct_arr ["num_data_points" ] == 1 ):
993+ # some packets have more than 1 sample. Not actually ptp. Revert to non-ptp variant.
994+ return self .__read_nsx_dataheader_variant_b (nsx_nb , filesize = filesize , offset = offset )
995+
996+ # It is still possible there was a data break and the file has multiple segments.
997+ # We can no longer rely on the presence of a header indicating a new segment,
998+ # so we look for timestamp differences greater than double the expected interval.
999+ _period = self .__nsx_basic_header [nsx_nb ]["period" ] # 30_000 ^-1 s per sample
1000+ _nominal_rate = 30_000 / _period # samples per sec; maybe 30_000 should be ["sample_resolution"]
1001+ _clock_rate = self .__nsx_basic_header [nsx_nb ]["timestamp_resolution" ] # clocks per sec
1002+ clk_per_samp = _clock_rate / _nominal_rate # clk/sec / smp/sec = clk/smp
1003+ seg_thresh_clk = int (2 * clk_per_samp )
1004+ seg_starts = np .hstack (
1005+ (0 , 1 + np .argwhere (np .diff (struct_arr ["timestamps" ]) > seg_thresh_clk ).flatten ())
1006+ )
1007+ for seg_ix , seg_start_idx in enumerate (seg_starts ):
1008+ if seg_ix < (len (seg_starts ) - 1 ):
1009+ seg_stop_idx = seg_starts [seg_ix + 1 ]
1010+ else :
1011+ seg_stop_idx = (len (struct_arr ) - 1 )
1012+ seg_offset = offset + seg_start_idx * struct_arr .dtype .itemsize
1013+ num_data_pts = seg_stop_idx - seg_start_idx
1014+ seg_struct_arr = np .memmap (
1015+ filename ,
1016+ dtype = ptp_dt ,
1017+ shape = num_data_pts ,
1018+ offset = seg_offset ,
1019+ mode = "r"
1020+ )
1021+ data_header [seg_ix ] = {
1022+ "header" : None ,
1023+ "timestamp" : seg_struct_arr ["timestamps" ], # Note, this is an array, not a scalar
1024+ "nb_data_points" : num_data_pts ,
1025+ "offset_to_data_block" : seg_offset
1026+ }
1027+ return data_header
1028+
9351029 def __read_nsx_data_variant_a (self , nsx_nb ):
9361030 """
9371031 Extract nsx data from a 2.1 .nsx file
@@ -950,8 +1044,8 @@ def __read_nsx_data_variant_a(self, nsx_nb):
9501044
9511045 def __read_nsx_data_variant_b (self , nsx_nb ):
9521046 """
953- Extract nsx data (blocks) from a 2.2 or 2.3 . nsx file. Blocks can arise
954- if the recording was paused by the user.
1047+ Extract nsx data (blocks) from a 2.2, 2.3, or 3.0 . nsx file.
1048+ Blocks can arise if the recording was paused by the user.
9551049 """
9561050 filename = "." .join ([self ._filenames ["nsx" ], f"ns{ nsx_nb } " ])
9571051
@@ -969,6 +1063,36 @@ def __read_nsx_data_variant_b(self, nsx_nb):
9691063
9701064 return data
9711065
1066+ def __read_nsx_data_variant_c (self , nsx_nb ):
1067+ """
1068+ Extract nsx data (blocks) from a 3.0 .nsx file with PTP timestamps
1069+ yielding a timestamp per sample. Blocks can arise
1070+ if the recording was paused by the user.
1071+ """
1072+ filename = "." .join ([self ._filenames ["nsx" ], f"ns{ nsx_nb } " ])
1073+
1074+ ptp_dt = [
1075+ ("reserved" , "uint8" ),
1076+ ("timestamps" , "uint64" ),
1077+ ("num_data_points" , "uint32" ),
1078+ ("samples" , "int16" , self .__nsx_basic_header [nsx_nb ]["channel_count" ])
1079+ ]
1080+
1081+ data = {}
1082+ for bl_id , bl_header in self .__nsx_data_header [nsx_nb ].items ():
1083+ struct_arr = np .memmap (
1084+ filename ,
1085+ dtype = ptp_dt ,
1086+ shape = bl_header ["nb_data_points" ],
1087+ offset = bl_header ["offset_to_data_block" ], mode = "r"
1088+ )
1089+ # Does this concretize the data?
1090+ # If yes then investigate np.ndarray with buffer=file,
1091+ # offset=offset+13, and strides that skips 13-bytes per row.
1092+ data [bl_id ] = struct_arr ["samples" ]
1093+
1094+ return data
1095+
9721096 def __read_nev_header (self , ext_header_variants ):
9731097 """
9741098 Extract nev header information from a of specific .nsx header variant
0 commit comments