Skip to content

Commit ace8c0b

Browse files
committed
strings
1 parent 205744b commit ace8c0b

File tree

1 file changed

+66
-36
lines changed

1 file changed

+66
-36
lines changed

neo/rawio/blackrockrawio.py

Lines changed: 66 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
* Lyuba Zehl, Michael Denker - fourth version
99
* Samuel Garcia, Julia Srenger - fifth version
1010
* Chadwick Boulay - FileSpec 3.0 and 3.0-PTP
11+
* Heberto Mayorquin - Time segmentation fixes, reporting and refactoring
1112
1213
This IO supports reading only.
1314
This IO is able to read:
@@ -101,6 +102,27 @@ class BlackrockRawIO(BaseRawIO):
101102
must be set at the init before parse_header().
102103
load_nev: bool, default: True
103104
Load (or not) events/spikes by ignoring or not the nev file.
105+
gap_tolerance_ms : float | None, default: None
106+
Maximum acceptable gap size in milliseconds for automatic segmentation.
107+
108+
**Default behavior (None)**: If timestamp gaps are detected, an error is raised
109+
with a detailed gap report. This ensures users are aware of data discontinuities.
110+
111+
**Opt-in segmentation**: Provide a value to automatically segment data at gaps
112+
larger than this threshold. Gaps smaller than the threshold are ignored (data
113+
treated as continuous).
114+
115+
Examples:
116+
117+
- None (default): Error on any detected gaps
118+
- 1.0: Tolerate gaps up to 1 ms, segment on larger gaps
119+
- 10.0: Tolerate gaps up to 10 ms (filters buffer artifacts)
120+
- 100.0: Tolerate gaps up to 100 ms (only major pauses create segments)
121+
122+
Applies to:
123+
124+
- PTP format (v3.0-ptp): Gaps in per-sample timestamps
125+
- Standard format (v2.2/2.3/3.0): Gaps between data blocks
104126
105127
Notes
106128
-----
@@ -135,13 +157,13 @@ class BlackrockRawIO(BaseRawIO):
135157
main_sampling_rate = 30000.0
136158

137159
def __init__(
138-
self, filename=None, nsx_override=None, nev_override=None, nsx_to_load=None, load_nev=True, verbose=False
160+
self, filename=None, nsx_override=None, nev_override=None, nsx_to_load=None, load_nev=True,
161+
verbose=False, gap_tolerance_ms=None
139162
):
140-
"""
141-
Initialize the BlackrockIO class.
142-
"""
143163
BaseRawIO.__init__(self)
144164

165+
self.gap_tolerance_ms = gap_tolerance_ms
166+
145167
self.filename = str(filename)
146168

147169
# remove extension from base _filenames
@@ -917,7 +939,7 @@ def _parse_nsx_data(self, spec, nsx_nb):
917939
│ - nb_data_points (e.g., 1000) │
918940
├─────────────────────────────────────────────┤
919941
│ DATA BLOCK 1 DATA │ ← Actual samples
920-
│ - 1000 samples × N channels │
942+
│ - 1000 samples x N channels │
921943
│ - int16 values │
922944
├─────────────────────────────────────────────┤
923945
│ DATA BLOCK 2 HEADER │ ← Next block
@@ -926,15 +948,13 @@ def _parse_nsx_data(self, spec, nsx_nb):
926948
│ - nb_data_points (e.g., 1000) │
927949
├─────────────────────────────────────────────┤
928950
│ DATA BLOCK 2 DATA │
929-
│ - 1000 samples × N channels │
951+
│ - 1000 samples x N channels │
930952
└─────────────────────────────────────────────┘
931953
932954
Key characteristics:
933955
- Headers are EXPLICIT and SPARSE (only at block boundaries)
934956
- Each block has ONE scalar timestamp for ALL samples in that block
935957
- Reader LOOPS through file, finding headers
936-
- Multiple blocks exist when recording was paused/resumed
937-
- Timestamp indicates when each block started recording
938958
939959
PTP FORMAT (v3.0 with Precision Time Protocol)
940960
-----------------------------------------------
@@ -949,19 +969,19 @@ def _parse_nsx_data(self, spec, nsx_nb):
949969
│ - reserved (1 byte) │
950970
│ - timestamp (8 bytes, e.g., 1000) │
951971
│ - num_data_points (always 1) │
952-
│ - samples (N channels × int16) │
972+
│ - samples (N channels x int16) │
953973
├─────────────────────────────────────────────┤
954974
│ PACKET 1: │
955975
│ - reserved │
956976
│ - timestamp (e.g., 1033) │
957977
│ - num_data_points (1) │
958-
│ - samples (N channels × int16) │
978+
│ - samples (N channels x int16) │
959979
├─────────────────────────────────────────────┤
960980
│ PACKET 2: │
961981
│ - reserved │
962982
│ - timestamp (e.g., 1066) │
963983
│ - num_data_points (1) │
964-
│ - samples (N channels × int16) │
984+
│ - samples (N channels x int16) │
965985
├─────────────────────────────────────────────┤
966986
│ ...thousands more packets... │
967987
│ PACKET 500: │
@@ -973,8 +993,6 @@ def _parse_nsx_data(self, spec, nsx_nb):
973993
- NO separate headers and data - they're INTERLEAVED
974994
- EVERY sample has its own timestamp (per-sample nanosecond precision)
975995
- File is ONE CONTINUOUS ARRAY of uniform packets
976-
- Segments must be INFERRED by detecting timestamp gaps
977-
- Timestamp gap > 2× sampling period indicates segment boundary
978996
979997
V2.1 FORMAT
980998
-----------
@@ -1175,12 +1193,9 @@ def _parse_nsx_data_v30_ptp(self, nsx_nb):
11751193
}
11761194
}
11771195

1178-
def _report_nsx_timestamp_gaps(self, gap_indices, timestamps_in_seconds, time_differences, threshold, nsx_nb):
1196+
def _format_gap_report(self, gap_indices, timestamps_in_seconds, time_differences, nsx_nb):
11791197
"""
1180-
Report detected timestamp gaps with detailed table.
1181-
1182-
This function generates a warning message with a detailed table showing
1183-
where gaps were detected in the timestamp sequence.
1198+
Format a detailed gap report showing where timestamp discontinuities occur.
11841199
11851200
Parameters
11861201
----------
@@ -1190,16 +1205,14 @@ def _report_nsx_timestamp_gaps(self, gap_indices, timestamps_in_seconds, time_di
11901205
All timestamps converted to seconds
11911206
time_differences : np.ndarray
11921207
Time differences between consecutive timestamps
1193-
threshold : float
1194-
Gap threshold in seconds
11951208
nsx_nb : int
11961209
NSX file number for the report
1197-
"""
1198-
import warnings
1199-
1200-
threshold_ms = threshold * 1000
1201-
num_segments = len(gap_indices) + 1
12021210
1211+
Returns
1212+
-------
1213+
str
1214+
Formatted gap report with table
1215+
"""
12031216
# Calculate gap details
12041217
gap_durations_seconds = time_differences[gap_indices]
12051218
gap_durations_ms = gap_durations_seconds * 1000
@@ -1211,17 +1224,16 @@ def _report_nsx_timestamp_gaps(self, gap_indices, timestamps_in_seconds, time_di
12111224
for index, pos, dur in zip(gap_indices, gap_positions_seconds, gap_durations_ms)
12121225
]
12131226

1214-
segmentation_report_message = (
1215-
f"\nFound {len(gap_indices)} gaps for nsx {nsx_nb} where samples are farther apart than {threshold_ms:.3f} ms.\n"
1216-
f"Data will be segmented at these locations to create {num_segments} segments.\n\n"
1227+
return (
1228+
f"Gap Report for ns{nsx_nb}:\n"
1229+
f"Found {len(gap_indices)} timestamp gaps (detection threshold: 2 x sampling period)\n\n"
12171230
"Gap Details:\n"
12181231
"+-----------------+-----------------------+-----------------------+\n"
1219-
"| Sample Index | Sample at (Seconds) | Gap Jump (ms) |\n"
1232+
"| Sample Index | Sample at (Seconds) | Gap Size (ms) |\n"
12201233
"+-----------------+-----------------------+-----------------------+\n"
12211234
+ "".join(gap_detail_lines)
12221235
+ "+-----------------+-----------------------+-----------------------+\n"
12231236
)
1224-
warnings.warn(segmentation_report_message)
12251237

12261238
def _segment_nsx_data(self, data_blocks_dict, nsx_nb):
12271239
"""
@@ -1279,21 +1291,39 @@ def _segment_nsx_data(self, data_blocks_dict, nsx_nb):
12791291
if isinstance(timestamps, np.ndarray):
12801292
# Analyze timestamp gaps
12811293
sampling_rate = self._nsx_sampling_frequency[nsx_nb]
1282-
segmentation_threshold = 2.0 / sampling_rate
1294+
1295+
# Detection threshold: use strict 2x sampling period to find ALL gaps
1296+
detection_threshold = 2.0 / sampling_rate
12831297

12841298
timestamps_sampling_rate = self._nsx_basic_header[nsx_nb]["timestamp_resolution"]
12851299
timestamps_in_seconds = timestamps / timestamps_sampling_rate
12861300

12871301
time_differences = np.diff(timestamps_in_seconds)
1288-
gap_indices = np.argwhere(time_differences > segmentation_threshold).flatten()
1302+
gap_indices = np.argwhere(time_differences > detection_threshold).flatten()
12891303

1290-
# Report gaps if found
1304+
# If gaps found, check user's tolerance
12911305
if len(gap_indices) > 0:
1292-
self._report_nsx_timestamp_gaps(
1293-
gap_indices, timestamps_in_seconds, time_differences,
1294-
segmentation_threshold, nsx_nb
1306+
gap_report = self._format_gap_report(
1307+
gap_indices, timestamps_in_seconds, time_differences, nsx_nb
12951308
)
12961309

1310+
# Error by default - user must opt-in to segmentation
1311+
if self.gap_tolerance_ms is None:
1312+
raise ValueError(
1313+
f"Detected {len(gap_indices)} timestamp gaps in ns{nsx_nb} file.\n"
1314+
f"{gap_report}\n"
1315+
f"To load this data, provide gap_tolerance_ms parameter to automatically "
1316+
f"segment at gaps larger than the specified tolerance."
1317+
)
1318+
1319+
# User provided tolerance - filter gaps and segment
1320+
gap_tolerance_s = self.gap_tolerance_ms / 1000.0
1321+
significant_gap_mask = time_differences[gap_indices] > gap_tolerance_s
1322+
significant_gap_indices = gap_indices[significant_gap_mask]
1323+
1324+
# Use significant gaps for segmentation (no warning - user opted in)
1325+
gap_indices = significant_gap_indices
1326+
12971327
# Create segments based on gaps
12981328
segment_starts = np.hstack((0, gap_indices + 1))
12991329
segment_boundaries = list(segment_starts) + [len(data)]

0 commit comments

Comments
 (0)