@@ -323,13 +323,25 @@ def _parse_header(self):
323323 self ._nsx_basic_header = {}
324324 self ._nsx_ext_header = {}
325325 self ._nsx_data_header = {}
326+ self ._nsx_sampling_frequency = {}
326327
328+ # Read headers
327329 for nsx_nb in self ._avail_nsx :
328330 spec_version = self ._nsx_spec [nsx_nb ] = self ._extract_nsx_file_spec (nsx_nb )
329331 # read nsx headers
330332 nsx_header_reader = self ._nsx_header_reader [spec_version ]
331333 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 ] = sampling_rate
332340
341+
342+ # Parase data packages
343+ for nsx_nb in self ._avail_nsx :
344+
333345 # The only way to know if it is the Precision Time Protocol of file spec 3.0
334346 # is to check for nanosecond timestamp resolution.
335347 is_ptp_variant = (
@@ -1051,7 +1063,7 @@ def _read_nsx_dataheader_spec_v30_ptp(
10511063 ("reserved" , "uint8" ),
10521064 ("timestamps" , "uint64" ),
10531065 ("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" ],) ),
10551067 ]
10561068 npackets = int ((filesize - offset ) / np .dtype (ptp_dt ).itemsize )
10571069 struct_arr = np .memmap (filename , dtype = ptp_dt , shape = npackets , offset = offset , mode = "r" )
@@ -1060,24 +1072,58 @@ def _read_nsx_dataheader_spec_v30_ptp(
10601072 # some packets have more than 1 sample. Not actually ptp. Revert to non-ptp variant.
10611073 return self ._read_nsx_dataheader_spec_v22_30 (nsx_nb , filesize = filesize , offset = offset )
10621074
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+
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+ ideal_clock_rate = self ._nsx_basic_header [nsx_nb ]["timestamp_resolution" ] # clocks per sec uint64 or uint32
1085+ timestamps_in_seconds = raw_timestamps / ideal_clock_rate
1086+
1087+ time_differences = np .diff (timestamps_in_seconds )
1088+ gap_sample_indices = np .argwhere (time_differences > segmentation_threshold ).flatten ()
1089+ segment_start_indices = np .hstack ((0 , 1 + gap_sample_indices ))
1090+
1091+ # Report gaps if any are found
1092+ if len (gap_sample_indices ) > 0 :
1093+ import warnings
1094+ threshold_ms = segmentation_threshold * 1000
1095+
1096+ segmentation_report_message = (
1097+ f"\n Found { len (gap_sample_indices )} gaps where samples are farther apart than { threshold_ms :.3f} ms.\n "
1098+ f"Data will be segmented at these locations to create { len (segment_start_indices )} segments.\n \n "
1099+ "Gap Details:\n "
1100+ "+-----------------+-----------------------+-----------------------+\n "
1101+ "| Sample Index | Sample at | Gap Jump |\n "
1102+ "| | (Seconds) | (Milliseconds) |\n "
1103+ "+-----------------+-----------------------+-----------------------+\n "
1104+ )
1105+
1106+ for sample_index in gap_sample_indices :
1107+ gap_duration_seconds = time_differences [sample_index ] # Actual time difference between timestamps
1108+ gap_duration_ms = gap_duration_seconds * 1000
1109+ gap_position_seconds = timestamps_in_seconds [sample_index ] - timestamps_in_seconds [0 ] # Time position relative to start
1110+
1111+ segmentation_report_message += (
1112+ f"| { sample_index :>15,} | { gap_position_seconds :>21.6f} | { gap_duration_ms :>21.3f} |\n "
1113+ )
1114+
1115+ segmentation_report_message += "+-----------------+-----------------------+-----------------------+\n "
1116+ warnings .warn (segmentation_report_message )
1117+
1118+ for seg_index , seg_start_index in enumerate (segment_start_indices ):
1119+ if seg_index < (len (segment_start_indices ) - 1 ):
1120+ seg_stop_index = segment_start_indices [seg_index + 1 ]
10751121 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
1122+ seg_stop_index = len (struct_arr ) - 1
1123+ seg_offset = offset + seg_start_index * struct_arr .dtype .itemsize
1124+ num_data_pts = seg_stop_index - seg_start_index
10791125 seg_struct_arr = np .memmap (filename , dtype = ptp_dt , shape = num_data_pts , offset = seg_offset , mode = "r" )
1080- data_header [seg_ix ] = {
1126+ data_header [seg_index ] = {
10811127 "header" : None ,
10821128 "timestamp" : seg_struct_arr ["timestamps" ], # Note, this is an array, not a scalar
10831129 "nb_data_points" : num_data_pts ,
@@ -1089,7 +1135,7 @@ def _read_nsx_data_spec_v21(self, nsx_nb):
10891135 """
10901136 Extract nsx data from a 2.1 .nsx file
10911137 """
1092- filename = "." . join ([ self ._filenames [" nsx" ], f" ns{ nsx_nb } "])
1138+ filename = f" { self ._filenames [' nsx' ] } . ns{ nsx_nb } "
10931139
10941140 # get shape of data
10951141 shape = (
@@ -1132,7 +1178,7 @@ def _read_nsx_data_spec_v30_ptp(self, nsx_nb):
11321178 yielding a timestamp per sample. Blocks can arise
11331179 if the recording was paused by the user.
11341180 """
1135- filename = "." . join ([ self ._filenames [" nsx" ], f" ns{ nsx_nb } "])
1181+ filename = f" { self ._filenames [' nsx' ] } . ns{ nsx_nb } "
11361182
11371183 ptp_dt = [
11381184 ("reserved" , "uint8" ),
@@ -1161,7 +1207,7 @@ def _read_nev_header(self, ext_header_variants):
11611207 """
11621208 Extract nev header information from a of specific .nsx header variant
11631209 """
1164- filename = "." . join ([ self ._filenames [" nev" ], " nev"])
1210+ filename = f" { self ._filenames [' nev' ] } . nev"
11651211
11661212 # basic header
11671213 dt0 = [
0 commit comments