3535"""
3636
3737import math
38+ import warnings
3839import numpy as np
3940
4041from enum import IntEnum , auto
@@ -67,6 +68,7 @@ def __init__(self):
6768 self .sects = []
6869 self .sampFreqUsed = 0 # actual sampling frequency of samples
6970 self .microsPerSampUsed = 0 # microseconds per sample
71+ self .detected_gaps = [] # list of (record_index, gap_size_us) for all detected gaps
7072
7173 def __eq__ (self , other ):
7274 samp_eq = self .sampFreqUsed == other .sampFreqUsed
@@ -216,13 +218,25 @@ def _buildNcsSections(ncsMemMap, sampFreq, gapTolerance=0):
216218 n_samples = n_samples ,
217219 )
218220 ncsSects .sects .append (section0 )
221+ # No gaps detected in fast path
222+ ncsSects .detected_gaps = []
219223
220224 else :
221225 # need to parse all data block to detect gaps
222226 # check when the predicted timestamp is outside the tolerance
223227 delta = (ncsMemMap ["timestamp" ][1 :] - ncsMemMap ["timestamp" ][:- 1 ]).astype (np .int64 )
224228 delta_prediction = ((ncsMemMap ["nb_valid" ][:- 1 ] / sampFreq ) * 1e6 ).astype (np .int64 )
225229
230+ # Always detect all gaps using the strict threshold for reporting
231+ strict_tolerance = round (NcsSectionsFactory ._maxGapSampFrac * 1e6 / sampFreq )
232+ all_gap_inds = np .flatnonzero (np .abs (delta - delta_prediction ) > strict_tolerance )
233+ gap_sizes_us = (delta - delta_prediction )[all_gap_inds ]
234+ ncsSects .detected_gaps = [
235+ (int (record_index + 1 ), int (gap_size ))
236+ for record_index , gap_size in zip (all_gap_inds , gap_sizes_us )
237+ ]
238+
239+ # Use user-provided tolerance for actual segmentation
226240 gap_inds = np .flatnonzero (np .abs (delta - delta_prediction ) > gapTolerance )
227241 gap_inds += 1
228242
@@ -245,7 +259,7 @@ def _buildNcsSections(ncsMemMap, sampFreq, gapTolerance=0):
245259 return ncsSects
246260
247261 @staticmethod
248- def build_for_ncs_file (ncsMemMap , nlxHdr , gapTolerance = None , strict_gap_mode = True ):
262+ def build_for_ncs_file (ncsMemMap , nlxHdr , gap_tolerance_us = None , ** kwargs ):
249263 """
250264 Build an NcsSections object for an NcsFile, given as a memmap and NlxHeader,
251265 handling gap detection appropriately given the file type as specified by the header.
@@ -256,27 +270,60 @@ def build_for_ncs_file(ncsMemMap, nlxHdr, gapTolerance=None, strict_gap_mode=Tru
256270 memory map of file
257271 nlxHdr:
258272 NlxHeader from corresponding file.
273+ gap_tolerance_us : float | None, default: None
274+ Gap tolerance in microseconds for segmentation.
259275
260276 Returns
261277 -------
262278 An NcsSections corresponding to the provided ncsMemMap and nlxHdr
263279 """
280+ # Handle deprecated parameters
281+ gapTolerance = kwargs .pop ("gapTolerance" , None )
282+ strict_gap_mode = kwargs .pop ("strict_gap_mode" , None )
283+ if kwargs :
284+ raise TypeError (f"Unexpected keyword arguments: { list (kwargs .keys ())} " )
285+
286+ if gapTolerance is not None :
287+ warnings .warn (
288+ "The `gapTolerance` parameter is deprecated and will be removed in version 0.16.0. "
289+ "Use `gap_tolerance_us` instead." ,
290+ DeprecationWarning ,
291+ stacklevel = 2 ,
292+ )
293+ if gap_tolerance_us is None :
294+ gap_tolerance_us = gapTolerance
295+
296+ if strict_gap_mode is not None :
297+ warnings .warn (
298+ "The `strict_gap_mode` parameter is deprecated and will be removed in version 0.16.0. "
299+ "Use `gap_tolerance_us` instead." ,
300+ DeprecationWarning ,
301+ stacklevel = 2 ,
302+ )
303+
304+ if gap_tolerance_us is not None and gap_tolerance_us < 0 :
305+ raise ValueError (f"`gap_tolerance_us` must be non-negative, got { gap_tolerance_us } " )
306+
264307 acqType = nlxHdr .type_of_recording ()
265308 freq = nlxHdr ["sampling_rate" ]
266309
310+ # Deprecation shim for strict_gap_mode (the boolean predecessor of gap_tolerance_us).
311+ # strict_gap_mode True/None already match the modern per-type defaults below; only
312+ # strict_gap_mode=False differed, tolerating a quarter-packet gap (PRE4 was 0 either way).
313+ # Translating that single case here keeps the per-type branches on gap_tolerance_us only.
314+ # Remove this block when strict_gap_mode is dropped in v0.16.0.
315+ if gap_tolerance_us is None and strict_gap_mode is not None and not strict_gap_mode and acqType != AcqType .PRE4 :
316+ gap_tolerance_us = round (0.25 * NcsSection ._RECORD_SIZE * 1e6 / freq )
317+
267318 if acqType == AcqType .PRE4 :
268319 # Old Neuralynx style with truncated whole microseconds for actual sampling. This
269320 # restriction arose from the sampling being based on a master 1 MHz clock.
270321 microsPerSampUsed = math .floor (NcsSectionsFactory .get_micros_per_samp_for_freq (freq ))
271322 sampFreqUsed = NcsSectionsFactory .get_freq_for_micros_per_samp (microsPerSampUsed )
272- if gapTolerance is None :
273- if strict_gap_mode :
274- # this is the old behavior, maybe we could put 0.9 sample interval no ?
275- gapTolerance = 0
276- else :
277- gapTolerance = 0
278-
279- ncsSects = NcsSectionsFactory ._buildNcsSections (ncsMemMap , sampFreqUsed , gapTolerance = gapTolerance )
323+ if gap_tolerance_us is None :
324+ gap_tolerance_us = 0
325+
326+ ncsSects = NcsSectionsFactory ._buildNcsSections (ncsMemMap , sampFreqUsed , gapTolerance = gap_tolerance_us )
280327 ncsSects .sampFreqUsed = sampFreqUsed
281328 ncsSects .microsPerSampUsed = microsPerSampUsed
282329
@@ -288,17 +335,12 @@ def build_for_ncs_file(ncsMemMap, nlxHdr, gapTolerance=None, strict_gap_mode=Tru
288335 AcqType .RAWDATAFILE ,
289336 ]:
290337 # digital lynx style with fractional frequency and micros per samp determined from block times
291- if gapTolerance is None :
292- if strict_gap_mode :
293- # this is the old behavior
294- gapTolerance = round (NcsSectionsFactory ._maxGapSampFrac * 1e6 / freq )
295- else :
296- # quarter of paquet size is tolerate
297- gapTolerance = round (0.25 * NcsSection ._RECORD_SIZE * 1e6 / freq )
298- ncsSects = NcsSectionsFactory ._buildNcsSections (ncsMemMap , freq , gapTolerance = gapTolerance )
299-
300- # take longer data block to compute reaal sampling rate
301- # ind_max = np.argmax([section.n_samples for section in ncsSects.sects])
338+ if gap_tolerance_us is None :
339+ # default: strict detection (0.2 of a sample interval)
340+ gap_tolerance_us = round (NcsSectionsFactory ._maxGapSampFrac * 1e6 / freq )
341+ ncsSects = NcsSectionsFactory ._buildNcsSections (ncsMemMap , freq , gapTolerance = gap_tolerance_us )
342+
343+ # take longer data block to compute real sampling rate
302344 ind_max = np .argmax ([section .endRec - section .startRec for section in ncsSects .sects ])
303345 section = ncsSects .sects [ind_max ]
304346 if section .endRec != section .startRec :
@@ -315,13 +357,9 @@ def build_for_ncs_file(ncsMemMap, nlxHdr, gapTolerance=None, strict_gap_mode=Tru
315357
316358 elif acqType == AcqType .BML or acqType == AcqType .ATLAS :
317359 # BML & ATLAS style with fractional frequency and micros per samp
318- if strict_gap_mode :
319- # this is the old behavior, maybe we could put 0.9 sample interval no ?
320- gapTolerance = 0
321- else :
322- # quarter of paquet size is tolerate
323- gapTolerance = round (0.25 * NcsSection ._RECORD_SIZE * 1e6 / freq )
324- ncsSects = NcsSectionsFactory ._buildNcsSections (ncsMemMap , freq , gapTolerance = gapTolerance )
360+ if gap_tolerance_us is None :
361+ gap_tolerance_us = 0
362+ ncsSects = NcsSectionsFactory ._buildNcsSections (ncsMemMap , freq , gapTolerance = gap_tolerance_us )
325363 ncsSects .sampFreqUsed = freq
326364 ncsSects .microsPerSampUsed = NcsSectionsFactory .get_micros_per_samp_for_freq (freq )
327365
0 commit comments