7
7
* Samuel Garcia - third version
8
8
* Lyuba Zehl, Michael Denker - fourth version
9
9
* Samuel Garcia, Julia Srenger - fifth version
10
+ * Chadwick Boulay - FileSpec 3.0 and 3.0-PTP
10
11
11
12
This IO supports reading only.
12
13
This IO is able to read:
17
18
* 2.1
18
19
* 2.2
19
20
* 2.3
21
+ * 3.0
22
+ * 3.0 with PTP timestamps (Gemini systems)
20
23
21
24
The neural data channels are 1 - 128.
22
25
The analog inputs are 129 - 144. (129 - 137 AC coupled, 138 - 144 DC coupled)
@@ -191,12 +194,14 @@ def __init__(
191
194
"2.2" : self .__read_nsx_dataheader_variant_b ,
192
195
"2.3" : self .__read_nsx_dataheader_variant_b ,
193
196
"3.0" : self .__read_nsx_dataheader_variant_b ,
197
+ "3.0-ptp" : self .__read_nsx_dataheader_variant_c ,
194
198
}
195
199
self .__nsx_data_reader = {
196
200
"2.1" : self .__read_nsx_data_variant_a ,
197
201
"2.2" : self .__read_nsx_data_variant_b ,
198
202
"2.3" : self .__read_nsx_data_variant_b ,
199
203
"3.0" : self .__read_nsx_data_variant_b ,
204
+ "3.0-ptp" : self .__read_nsx_data_variant_c ,
200
205
}
201
206
self .__nsx_params = {
202
207
"2.1" : self .__get_nsx_param_variant_a ,
@@ -312,11 +317,17 @@ def _parse_header(self):
312
317
# read nsx headers
313
318
self .__nsx_basic_header [nsx_nb ], self .__nsx_ext_header [nsx_nb ] = self .__nsx_header_reader [spec ](nsx_nb )
314
319
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 ]
316
327
# 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 )
318
329
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)
320
331
# here make a list only
321
332
if self .nsx_to_load is None or self .nsx_to_load == "all" :
322
333
self .nsx_to_load = list (self ._avail_nsx )
@@ -359,7 +370,14 @@ def _parse_header(self):
359
370
if len (self .nsx_to_load ) > 0 :
360
371
for nsx_nb in self .nsx_to_load :
361
372
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 )
363
381
364
382
sr = float (main_sampling_rate / self .__nsx_basic_header [nsx_nb ]["period" ])
365
383
self .sig_sampling_rates [nsx_nb ] = sr
@@ -415,15 +433,28 @@ def _parse_header(self):
415
433
for data_bl in range (self ._nb_segment ):
416
434
t_stop = 0.0
417
435
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']
418
445
length = self .nsx_datas [nsx_nb ][data_bl ].shape [0 ]
419
446
if self .__nsx_data_header [nsx_nb ] is None :
420
447
t_start = 0.0
448
+ t_stop = max (t_stop , length / self .sig_sampling_rates [nsx_nb ])
421
449
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 ])
427
458
self ._sigs_t_starts [nsx_nb ].append (t_start )
428
459
429
460
if self ._avail_files ["nev" ]:
@@ -794,6 +825,8 @@ def __read_nsx_header_variant_a(self, nsx_nb):
794
825
]
795
826
796
827
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.
797
830
798
831
# "extended" header (last field of file_id: NEURALCD)
799
832
# (to facilitate compatibility with higher file specs)
@@ -901,7 +934,7 @@ def __read_nsx_dataheader_variant_b(
901
934
):
902
935
"""
903
936
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 .
905
938
"""
906
939
filename = "." .join ([self ._filenames ["nsx" ], f"ns{ nsx_nb } " ])
907
940
@@ -932,6 +965,67 @@ def __read_nsx_dataheader_variant_b(
932
965
933
966
return data_header
934
967
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
+
935
1029
def __read_nsx_data_variant_a (self , nsx_nb ):
936
1030
"""
937
1031
Extract nsx data from a 2.1 .nsx file
@@ -950,8 +1044,8 @@ def __read_nsx_data_variant_a(self, nsx_nb):
950
1044
951
1045
def __read_nsx_data_variant_b (self , nsx_nb ):
952
1046
"""
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.
955
1049
"""
956
1050
filename = "." .join ([self ._filenames ["nsx" ], f"ns{ nsx_nb } " ])
957
1051
@@ -969,6 +1063,36 @@ def __read_nsx_data_variant_b(self, nsx_nb):
969
1063
970
1064
return data
971
1065
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
+
972
1096
def __read_nev_header (self , ext_header_variants ):
973
1097
"""
974
1098
Extract nev header information from a of specific .nsx header variant
0 commit comments