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)
@@ -189,12 +192,14 @@ def __init__(
189192 "2.2" : self .__read_nsx_dataheader_variant_b ,
190193 "2.3" : self .__read_nsx_dataheader_variant_b ,
191194 "3.0" : self .__read_nsx_dataheader_variant_b ,
195+ "3.0-ptp" : self .__read_nsx_dataheader_variant_c ,
192196 }
193197 self .__nsx_data_reader = {
194198 "2.1" : self .__read_nsx_data_variant_a ,
195199 "2.2" : self .__read_nsx_data_variant_b ,
196200 "2.3" : self .__read_nsx_data_variant_b ,
197201 "3.0" : self .__read_nsx_data_variant_b ,
202+ "3.0-ptp" : self .__read_nsx_data_variant_c ,
198203 }
199204 self .__nsx_params = {
200205 "2.1" : self .__get_nsx_param_variant_a ,
@@ -310,11 +315,17 @@ def _parse_header(self):
310315 # read nsx headers
311316 self .__nsx_basic_header [nsx_nb ], self .__nsx_ext_header [nsx_nb ] = self .__nsx_header_reader [spec ](nsx_nb )
312317
313- # Read nsx data header(s)
318+ # The only way to know if it is the PTP-variant of file spec 3.0
319+ # is to check for nanosecond timestamp resolution.
320+ if "timestamp_resolution" in self .__nsx_basic_header [nsx_nb ].dtype .names \
321+ and self .__nsx_basic_header [nsx_nb ]["timestamp_resolution" ] == 1_000_000_000 :
322+ nsx_dataheader_reader = self .__nsx_dataheader_reader ["3.0-ptp" ]
323+ else :
324+ nsx_dataheader_reader = self .__nsx_dataheader_reader [spec ]
314325 # for nsxdef get_analogsignal_shape(self, block_index, seg_index):
315- self .__nsx_data_header [nsx_nb ] = self . __nsx_dataheader_reader [ spec ] (nsx_nb )
326+ self .__nsx_data_header [nsx_nb ] = nsx_dataheader_reader (nsx_nb )
316327
317- # nsx_to_load can be either int, list, 'max', all' (aka None)
328+ # nsx_to_load can be either int, list, 'max', ' all' (aka None)
318329 # here make a list only
319330 if self .nsx_to_load is None or self .nsx_to_load == "all" :
320331 self .nsx_to_load = list (self ._avail_nsx )
@@ -357,7 +368,14 @@ def _parse_header(self):
357368 if len (self .nsx_to_load ) > 0 :
358369 for nsx_nb in self .nsx_to_load :
359370 spec = self .__nsx_spec [nsx_nb ]
360- self .nsx_datas [nsx_nb ] = self .__nsx_data_reader [spec ](nsx_nb )
371+ # The only way to know if it is the PTP-variant of file spec 3.0
372+ # is to check for nanosecond timestamp resolution.
373+ if "timestamp_resolution" in self .__nsx_basic_header [nsx_nb ].dtype .names \
374+ and self .__nsx_basic_header [nsx_nb ]["timestamp_resolution" ] == 1_000_000_000 :
375+ _data_reader_fun = self .__nsx_data_reader ["3.0-ptp" ]
376+ else :
377+ _data_reader_fun = self .__nsx_data_reader [spec ]
378+ self .nsx_datas [nsx_nb ] = _data_reader_fun (nsx_nb )
361379
362380 sr = float (main_sampling_rate / self .__nsx_basic_header [nsx_nb ]["period" ])
363381 self .sig_sampling_rates [nsx_nb ] = sr
@@ -414,15 +432,28 @@ def _parse_header(self):
414432 for data_bl in range (self ._nb_segment ):
415433 t_stop = 0.0
416434 for nsx_nb in self .nsx_to_load :
435+ spec = self .__nsx_spec [nsx_nb ]
436+ if "timestamp_resolution" in self .__nsx_basic_header [nsx_nb ].dtype .names :
437+ ts_res = self .__nsx_basic_header [nsx_nb ]["timestamp_resolution" ]
438+ elif spec == "2.1" :
439+ ts_res = self .__nsx_params [spec ](nsx_nb )['timestamp_resolution' ]
440+ else :
441+ ts_res = 30_000
442+ period = self .__nsx_basic_header [nsx_nb ]["period" ]
443+ sec_per_samp = period / 30_000 # Maybe 30_000 should be ['sample_resolution']
417444 length = self .nsx_datas [nsx_nb ][data_bl ].shape [0 ]
418445 if self .__nsx_data_header [nsx_nb ] is None :
419446 t_start = 0.0
447+ t_stop = max (t_stop , length / self .sig_sampling_rates [nsx_nb ])
420448 else :
421- t_start = (
422- self .__nsx_data_header [nsx_nb ][data_bl ]["timestamp" ]
423- / self .__nsx_basic_header [nsx_nb ]["timestamp_resolution" ]
424- )
425- t_stop = max (t_stop , t_start + length / self .sig_sampling_rates [nsx_nb ])
449+ timestamps = self .__nsx_data_header [nsx_nb ][data_bl ]["timestamp" ]
450+ if hasattr (timestamps , "size" ) and timestamps .size == length :
451+ # FileSpec 3.0 with PTP -- use the per-sample timestamps
452+ t_start = timestamps [0 ] / ts_res
453+ t_stop = max (t_stop , timestamps [- 1 ] / ts_res + sec_per_samp )
454+ else :
455+ t_start = timestamps / ts_res
456+ t_stop = max (t_stop , t_start + length / self .sig_sampling_rates [nsx_nb ])
426457 self ._sigs_t_starts [nsx_nb ].append (t_start )
427458
428459 if self ._avail_files ["nev" ]:
@@ -793,6 +824,8 @@ def __read_nsx_header_variant_a(self, nsx_nb):
793824 ]
794825
795826 nsx_basic_header = np .fromfile (filename , count = 1 , dtype = dt0 )[0 ]
827+ # Note: it is not possible to use recfunctions to append_fields of 'timestamp_resolution',
828+ # because the size of this object is used as the header size in later read operations.
796829
797830 # "extended" header (last field of file_id: NEURALCD)
798831 # (to facilitate compatibility with higher file specs)
@@ -900,7 +933,7 @@ def __read_nsx_dataheader_variant_b(
900933 ):
901934 """
902935 Reads the nsx data header for each data block following the offset of
903- file spec 2.2 and 2.3.
936+ file spec 2.2, 2.3, and 3.0 .
904937 """
905938 filename = "." .join ([self ._filenames ["nsx" ], f"ns{ nsx_nb } " ])
906939
@@ -931,6 +964,67 @@ def __read_nsx_dataheader_variant_b(
931964
932965 return data_header
933966
967+ def __read_nsx_dataheader_variant_c (
968+ self , nsx_nb , filesize = None , offset = None , ):
969+ """
970+ Reads the nsx data header for each data block for file spec 3.0 with PTP timestamps
971+ """
972+ filename = "." .join ([self ._filenames ["nsx" ], f"ns{ nsx_nb } " ])
973+
974+ filesize = self .__get_file_size (filename )
975+
976+ data_header = {}
977+ index = 0
978+
979+ if offset is None :
980+ offset = self .__nsx_basic_header [nsx_nb ]["bytes_in_headers" ]
981+
982+ ptp_dt = [
983+ ("reserved" , "uint8" ),
984+ ("timestamps" , "uint64" ),
985+ ("num_data_points" , "uint32" ),
986+ ("samples" , "int16" , self .__nsx_basic_header [nsx_nb ]["channel_count" ])
987+ ]
988+ npackets = int ((filesize - offset ) / np .dtype (ptp_dt ).itemsize )
989+ struct_arr = np .memmap (filename , dtype = ptp_dt , shape = npackets , offset = offset , mode = "r" )
990+
991+ if not np .all (struct_arr ["num_data_points" ] == 1 ):
992+ # some packets have more than 1 sample. Not actually ptp. Revert to non-ptp variant.
993+ return self .__read_nsx_dataheader_variant_b (nsx_nb , filesize = filesize , offset = offset )
994+
995+ # It is still possible there was a data break and the file has multiple segments.
996+ # We can no longer rely on the presence of a header indicating a new segment,
997+ # so we look for timestamp differences greater than double the expected interval.
998+ _period = self .__nsx_basic_header [nsx_nb ]["period" ] # 30_000 ^-1 s per sample
999+ _nominal_rate = 30_000 / _period # samples per sec; maybe 30_000 should be ["sample_resolution"]
1000+ _clock_rate = self .__nsx_basic_header [nsx_nb ]["timestamp_resolution" ] # clocks per sec
1001+ clk_per_samp = _clock_rate / _nominal_rate # clk/sec / smp/sec = clk/smp
1002+ seg_thresh_clk = int (2 * clk_per_samp )
1003+ seg_starts = np .hstack (
1004+ (0 , 1 + np .argwhere (np .diff (struct_arr ["timestamps" ]) > seg_thresh_clk ).flatten ())
1005+ )
1006+ for seg_ix , seg_start_idx in enumerate (seg_starts ):
1007+ if seg_ix < (len (seg_starts ) - 1 ):
1008+ seg_stop_idx = seg_starts [seg_ix + 1 ]
1009+ else :
1010+ seg_stop_idx = (len (struct_arr ) - 1 )
1011+ seg_offset = offset + seg_start_idx * struct_arr .dtype .itemsize
1012+ num_data_pts = seg_stop_idx - seg_start_idx
1013+ seg_struct_arr = np .memmap (
1014+ filename ,
1015+ dtype = ptp_dt ,
1016+ shape = num_data_pts ,
1017+ offset = seg_offset ,
1018+ mode = "r"
1019+ )
1020+ data_header [seg_ix ] = {
1021+ "header" : None ,
1022+ "timestamp" : seg_struct_arr ["timestamps" ], # Note, this is an array, not a scalar
1023+ "nb_data_points" : num_data_pts ,
1024+ "offset_to_data_block" : seg_offset
1025+ }
1026+ return data_header
1027+
9341028 def __read_nsx_data_variant_a (self , nsx_nb ):
9351029 """
9361030 Extract nsx data from a 2.1 .nsx file
@@ -949,8 +1043,8 @@ def __read_nsx_data_variant_a(self, nsx_nb):
9491043
9501044 def __read_nsx_data_variant_b (self , nsx_nb ):
9511045 """
952- Extract nsx data (blocks) from a 2.2 or 2.3 . nsx file. Blocks can arise
953- if the recording was paused by the user.
1046+ Extract nsx data (blocks) from a 2.2, 2.3, or 3.0 . nsx file.
1047+ Blocks can arise if the recording was paused by the user.
9541048 """
9551049 filename = "." .join ([self ._filenames ["nsx" ], f"ns{ nsx_nb } " ])
9561050
@@ -968,6 +1062,36 @@ def __read_nsx_data_variant_b(self, nsx_nb):
9681062
9691063 return data
9701064
1065+ def __read_nsx_data_variant_c (self , nsx_nb ):
1066+ """
1067+ Extract nsx data (blocks) from a 3.0 .nsx file with PTP timestamps
1068+ yielding a timestamp per sample. Blocks can arise
1069+ if the recording was paused by the user.
1070+ """
1071+ filename = "." .join ([self ._filenames ["nsx" ], f"ns{ nsx_nb } " ])
1072+
1073+ ptp_dt = [
1074+ ("reserved" , "uint8" ),
1075+ ("timestamps" , "uint64" ),
1076+ ("num_data_points" , "uint32" ),
1077+ ("samples" , "int16" , self .__nsx_basic_header [nsx_nb ]["channel_count" ])
1078+ ]
1079+
1080+ data = {}
1081+ for bl_id , bl_header in self .__nsx_data_header [nsx_nb ].items ():
1082+ struct_arr = np .memmap (
1083+ filename ,
1084+ dtype = ptp_dt ,
1085+ shape = bl_header ["nb_data_points" ],
1086+ offset = bl_header ["offset_to_data_block" ], mode = "r"
1087+ )
1088+ # Does this concretize the data?
1089+ # If yes then investigate np.ndarray with buffer=file,
1090+ # offset=offset+13, and strides that skips 13-bytes per row.
1091+ data [bl_id ] = struct_arr ["samples" ]
1092+
1093+ return data
1094+
9711095 def __read_nev_header (self , ext_header_variants ):
9721096 """
9731097 Extract nev header information from a of specific .nsx header variant
0 commit comments