Skip to content

Commit ad70604

Browse files
committed
blackrock improved message
1 parent 3b592a8 commit ad70604

File tree

1 file changed

+66
-20
lines changed

1 file changed

+66
-20
lines changed

neo/rawio/blackrockrawio.py

Lines changed: 66 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -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"\nFound {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

Comments
 (0)