@@ -323,13 +323,25 @@ def _parse_header(self):
323
323
self ._nsx_basic_header = {}
324
324
self ._nsx_ext_header = {}
325
325
self ._nsx_data_header = {}
326
+ self ._nsx_sampling_frequency = {}
326
327
328
+ # Read headers
327
329
for nsx_nb in self ._avail_nsx :
328
330
spec_version = self ._nsx_spec [nsx_nb ] = self ._extract_nsx_file_spec (nsx_nb )
329
331
# read nsx headers
330
332
nsx_header_reader = self ._nsx_header_reader [spec_version ]
331
333
self ._nsx_basic_header [nsx_nb ], self ._nsx_ext_header [nsx_nb ] = nsx_header_reader (nsx_nb )
334
+
335
+ # The Blackrock defines period as the number of 1/30_000 seconds between data points
336
+ # E.g. it is 1 for 30_000, 3 for 10_000, etc
337
+ nsx_period = self ._nsx_basic_header [nsx_nb ]["period" ]
338
+ sampling_rate = 30_000.0 / nsx_period
339
+ self ._nsx_sampling_frequency [nsx_nb ] = float (sampling_rate )
332
340
341
+
342
+ # Parase data packages
343
+ for nsx_nb in self ._avail_nsx :
344
+
333
345
# The only way to know if it is the Precision Time Protocol of file spec 3.0
334
346
# is to check for nanosecond timestamp resolution.
335
347
is_ptp_variant = (
@@ -386,7 +398,8 @@ def _parse_header(self):
386
398
self ._match_nsx_and_nev_segment_ids (nsx_nb )
387
399
388
400
self .nsx_datas = {}
389
- self .sig_sampling_rates = {}
401
+ # Keep public attribute for backward compatibility but let's use the private one and maybe deprecate this at some point
402
+ self .sig_sampling_rates = {nsx_number : self ._nsx_sampling_frequency [nsx_number ] for nsx_number in self .nsx_to_load }
390
403
if len (self .nsx_to_load ) > 0 :
391
404
for nsx_nb in self .nsx_to_load :
392
405
basic_header = self ._nsx_basic_header [nsx_nb ]
@@ -403,8 +416,7 @@ def _parse_header(self):
403
416
_data_reader_fun = self ._nsx_data_reader [spec_version ]
404
417
self .nsx_datas [nsx_nb ] = _data_reader_fun (nsx_nb )
405
418
406
- sr = float (self .main_sampling_rate / basic_header ["period" ])
407
- self .sig_sampling_rates [nsx_nb ] = sr
419
+ sr = self ._nsx_sampling_frequency [nsx_nb ]
408
420
409
421
if spec_version in ["2.2" , "2.3" , "3.0" ]:
410
422
ext_header = self ._nsx_ext_header [nsx_nb ]
@@ -473,7 +485,7 @@ def _parse_header(self):
473
485
length = self .nsx_datas [nsx_nb ][data_bl ].shape [0 ]
474
486
if self ._nsx_data_header [nsx_nb ] is None :
475
487
t_start = 0.0
476
- t_stop = max (t_stop , length / self .sig_sampling_rates [nsx_nb ])
488
+ t_stop = max (t_stop , length / self ._nsx_sampling_frequency [nsx_nb ])
477
489
else :
478
490
timestamps = self ._nsx_data_header [nsx_nb ][data_bl ]["timestamp" ]
479
491
if hasattr (timestamps , "size" ) and timestamps .size == length :
@@ -482,7 +494,7 @@ def _parse_header(self):
482
494
t_stop = max (t_stop , timestamps [- 1 ] / ts_res + sec_per_samp )
483
495
else :
484
496
t_start = timestamps / ts_res
485
- t_stop = max (t_stop , t_start + length / self .sig_sampling_rates [nsx_nb ])
497
+ t_stop = max (t_stop , t_start + length / self ._nsx_sampling_frequency [nsx_nb ])
486
498
self ._sigs_t_starts [nsx_nb ].append (t_start )
487
499
488
500
if self ._avail_files ["nev" ]:
@@ -1041,43 +1053,83 @@ def _read_nsx_dataheader_spec_v30_ptp(
1041
1053
filesize = self ._get_file_size (filename )
1042
1054
1043
1055
data_header = {}
1044
- index = 0
1045
-
1046
1056
if offset is None :
1047
1057
# This is read as an uint32 numpy scalar from the header so we transform it to python int
1048
- offset = int (self ._nsx_basic_header [nsx_nb ]["bytes_in_headers" ])
1058
+ header_size = int (self ._nsx_basic_header [nsx_nb ]["bytes_in_headers" ])
1059
+ else :
1060
+ header_size = offset
1049
1061
1050
1062
ptp_dt = [
1051
1063
("reserved" , "uint8" ),
1052
1064
("timestamps" , "uint64" ),
1053
1065
("num_data_points" , "uint32" ),
1054
- ("samples" , "int16" , self ._nsx_basic_header [nsx_nb ]["channel_count" ]),
1066
+ ("samples" , "int16" , ( self ._nsx_basic_header [nsx_nb ]["channel_count" ],) ),
1055
1067
]
1056
- npackets = int ((filesize - offset ) / np .dtype (ptp_dt ).itemsize )
1057
- struct_arr = np .memmap (filename , dtype = ptp_dt , shape = npackets , offset = offset , mode = "r" )
1068
+ npackets = int ((filesize - header_size ) / np .dtype (ptp_dt ).itemsize )
1069
+ struct_arr = np .memmap (filename , dtype = ptp_dt , shape = npackets , offset = header_size , mode = "r" )
1058
1070
1059
1071
if not np .all (struct_arr ["num_data_points" ] == 1 ):
1060
1072
# some packets have more than 1 sample. Not actually ptp. Revert to non-ptp variant.
1061
- return self ._read_nsx_dataheader_spec_v22_30 (nsx_nb , filesize = filesize , offset = offset )
1062
-
1063
- # It is still possible there was a data break and the file has multiple segments.
1064
- # We can no longer rely on the presence of a header indicating a new segment,
1065
- # so we look for timestamp differences greater than double the expected interval.
1066
- _period = self ._nsx_basic_header [nsx_nb ]["period" ] # 30_000 ^-1 s per sample
1067
- _nominal_rate = 30_000 / _period # samples per sec; maybe 30_000 should be ["sample_resolution"]
1068
- _clock_rate = self ._nsx_basic_header [nsx_nb ]["timestamp_resolution" ] # clocks per sec
1069
- clk_per_samp = _clock_rate / _nominal_rate # clk/sec / smp/sec = clk/smp
1070
- seg_thresh_clk = int (2 * clk_per_samp )
1071
- seg_starts = np .hstack ((0 , 1 + np .argwhere (np .diff (struct_arr ["timestamps" ]) > seg_thresh_clk ).flatten ()))
1072
- for seg_ix , seg_start_idx in enumerate (seg_starts ):
1073
- if seg_ix < (len (seg_starts ) - 1 ):
1074
- seg_stop_idx = seg_starts [seg_ix + 1 ]
1075
- else :
1076
- seg_stop_idx = len (struct_arr ) - 1
1077
- seg_offset = offset + seg_start_idx * struct_arr .dtype .itemsize
1078
- num_data_pts = seg_stop_idx - seg_start_idx
1073
+ return self ._read_nsx_dataheader_spec_v22_30 (nsx_nb , filesize = filesize , offset = header_size )
1074
+
1075
+
1076
+ # Segment data, at the moment, we segment, where the data has gaps that are longer
1077
+ # than twice the sampling period.
1078
+ sampling_rate = self ._nsx_sampling_frequency [nsx_nb ]
1079
+ segmentation_threshold = 2.0 / sampling_rate
1080
+
1081
+ # The raw timestamps are the indices of an ideal clock that ticks at `timestamp_resolution` times per second.
1082
+ # We convert this indices to actual timestamps in seconds
1083
+ raw_timestamps = struct_arr ["timestamps" ]
1084
+ timestamps_sampling_rate = self ._nsx_basic_header [nsx_nb ]["timestamp_resolution" ] # clocks per sec uint64 or uint32
1085
+ timestamps_in_seconds = raw_timestamps / timestamps_sampling_rate
1086
+
1087
+ time_differences = np .diff (timestamps_in_seconds )
1088
+ gap_indices = np .argwhere (time_differences > segmentation_threshold ).flatten ()
1089
+ segment_starts = np .hstack ((0 , 1 + gap_indices ))
1090
+
1091
+ # Report gaps if any are found
1092
+ if len (gap_indices ) > 0 :
1093
+ import warnings
1094
+ threshold_ms = segmentation_threshold * 1000
1095
+
1096
+ # Calculate all gap details in vectorized operations
1097
+ gap_durations_seconds = time_differences [gap_indices ]
1098
+ gap_durations_ms = gap_durations_seconds * 1000
1099
+ gap_positions_seconds = timestamps_in_seconds [gap_indices ] - timestamps_in_seconds [0 ]
1100
+
1101
+ # Build gap detail lines all at once
1102
+ gap_detail_lines = [
1103
+ f"| { index :>15,} | { pos :>21.6f} | { dur :>21.3f} |\n "
1104
+ for index , pos , dur in zip (gap_indices , gap_positions_seconds , gap_durations_ms )
1105
+ ]
1106
+
1107
+ segmentation_report_message = (
1108
+ f"\n Found { len (gap_indices )} gaps for nsx { nsx_nb } where samples are farther apart than { threshold_ms :.3f} ms.\n "
1109
+ f"Data will be segmented at these locations to create { len (segment_starts )} segments.\n \n "
1110
+ "Gap Details:\n "
1111
+ "+-----------------+-----------------------+-----------------------+\n "
1112
+ "| Sample Index | Sample at | Gap Jump |\n "
1113
+ "| | (Seconds) | (Milliseconds) |\n "
1114
+ "+-----------------+-----------------------+-----------------------+\n "
1115
+ + '' .join (gap_detail_lines ) +
1116
+ "+-----------------+-----------------------+-----------------------+\n "
1117
+ )
1118
+ warnings .warn (segmentation_report_message )
1119
+
1120
+ # Calculate all segment boundaries and derived values in one operation
1121
+ segment_boundaries = list (segment_starts ) + [len (struct_arr ) - 1 ]
1122
+ segment_num_data_points = [segment_boundaries [i + 1 ] - segment_boundaries [i ] for i in range (len (segment_starts ))]
1123
+
1124
+ size_of_data_block = struct_arr .dtype .itemsize
1125
+ segment_offsets = [header_size + pos * size_of_data_block for pos in segment_starts ]
1126
+
1127
+ num_segments = len (segment_starts )
1128
+ for segment_index in range (num_segments ):
1129
+ seg_offset = segment_offsets [segment_index ]
1130
+ num_data_pts = segment_num_data_points [segment_index ]
1079
1131
seg_struct_arr = np .memmap (filename , dtype = ptp_dt , shape = num_data_pts , offset = seg_offset , mode = "r" )
1080
- data_header [seg_ix ] = {
1132
+ data_header [segment_index ] = {
1081
1133
"header" : None ,
1082
1134
"timestamp" : seg_struct_arr ["timestamps" ], # Note, this is an array, not a scalar
1083
1135
"nb_data_points" : num_data_pts ,
@@ -1089,7 +1141,7 @@ def _read_nsx_data_spec_v21(self, nsx_nb):
1089
1141
"""
1090
1142
Extract nsx data from a 2.1 .nsx file
1091
1143
"""
1092
- filename = "." . join ([ self ._filenames [" nsx" ], f" ns{ nsx_nb } "])
1144
+ filename = f" { self ._filenames [' nsx' ] } . ns{ nsx_nb } "
1093
1145
1094
1146
# get shape of data
1095
1147
shape = (
@@ -1132,13 +1184,13 @@ def _read_nsx_data_spec_v30_ptp(self, nsx_nb):
1132
1184
yielding a timestamp per sample. Blocks can arise
1133
1185
if the recording was paused by the user.
1134
1186
"""
1135
- filename = "." . join ([ self ._filenames [" nsx" ], f" ns{ nsx_nb } "])
1187
+ filename = f" { self ._filenames [' nsx' ] } . ns{ nsx_nb } "
1136
1188
1137
1189
ptp_dt = [
1138
1190
("reserved" , "uint8" ),
1139
1191
("timestamps" , "uint64" ),
1140
1192
("num_data_points" , "uint32" ),
1141
- ("samples" , "int16" , self ._nsx_basic_header [nsx_nb ]["channel_count" ]),
1193
+ ("samples" , "int16" , ( self ._nsx_basic_header [nsx_nb ]["channel_count" ],) ),
1142
1194
]
1143
1195
1144
1196
data = {}
@@ -1161,7 +1213,7 @@ def _read_nev_header(self, ext_header_variants):
1161
1213
"""
1162
1214
Extract nev header information from a of specific .nsx header variant
1163
1215
"""
1164
- filename = "." . join ([ self ._filenames [" nev" ], " nev"])
1216
+ filename = f" { self ._filenames [' nev' ] } . nev"
1165
1217
1166
1218
# basic header
1167
1219
dt0 = [
0 commit comments