fpfind 3.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1316 @@
1
+ #!/usr/bin/env python3
2
+ """Takes in any set of timestamp data, and reads/converts it.
3
+
4
+ Implemented for timestamp7 data, i.e. 4ps resolution data, but also works for other
5
+ timestamps, e.g. 2ns and 125ps resolutions. Note the timestamp formats:
6
+ '-a0' hex text, '-a1' binary, '-a2' binary text.
7
+
8
+ Usage:
9
+
10
+ Split timestamp data on same channel into two different timestamp files:
11
+
12
+ >>> t, p = read_a1("timestampfile.Aa1X.dat", legacy=True)
13
+
14
+ >>> mask = (p & 1).astype(bool) # events with bit 0 set
15
+ >>> write_a1("alice.Aa1.dat", t[mask], p[mask])
16
+
17
+ >>> mask = p == 8 # events with *only* bit 3 set
18
+ >>> mask &= ((t > 100) & (t < 120) # events with timestamp between 100s and 120s
19
+ >>> write_a2("bob.Aa2X.txt", t[mask], p[mask], legacy=True)
20
+
21
+
22
+ Equivalent command-line usage (faster since using smaller stream buffers):
23
+
24
+ > ./parse_timestamps.py -A1 -X -a1 --pattern 1 --mask \
25
+ timestampfile.Aa1X.dat alice.Aa1.dat
26
+
27
+ > ./parse_timestamps.py -A1 -X -a1 -x --pattern 8 --start 100 --end 120 \
28
+ timestampfile.Aa1X.dat bob.Aa2X.txt
29
+
30
+
31
+ Read large timestamp files in Python (streams in small buffers to avoid OOM):
32
+
33
+ >>> stream, num_batches = sread_a1("timestampfile.Aa1X.dat", legacy=True)
34
+ >>> for i, (t, p) in enumerate(stream, start=1):
35
+ ... print(f"Batch: {i}/{num_batches}")
36
+ ... # Do stuff with t and p
37
+
38
+
39
+ Print event statistics:
40
+
41
+ > ./parse_timestamps.py -X timestampfile.Aa1X.dat
42
+
43
+
44
+ Changelog:
45
+ 2022-10-07 Justin: Dummy events and blinding events all set to 0, not handled
46
+ 2023-05-09 Justin: Add streaming capabilities, minimize inline processing
47
+ 2024-07-09 Justin: Remove references to deprecated typing.Tuple types
48
+ """
49
+
50
+ import argparse
51
+ import bisect
52
+ import io
53
+ import pathlib
54
+ import struct
55
+ import sys
56
+ import warnings
57
+ from typing import Optional, Tuple, Union
58
+
59
+ import numba
60
+ import numpy as np
61
+ import tqdm
62
+ from numpy.typing import NDArray
63
+
64
+ from fpfind import NP_PRECISEFLOAT, TSRES
65
+ from fpfind.lib.typing import (
66
+ DetectorArray,
67
+ Float,
68
+ Integer,
69
+ IntegerArray,
70
+ Number,
71
+ PathLike,
72
+ TimestampArray,
73
+ TimestampDetectorStream,
74
+ )
75
+
76
+ ###########################################
77
+ # PARSERS (from internal integer array) #
78
+ ###########################################
79
+
80
+
81
+ def _parse_a1(
82
+ data: NDArray[np.uint32],
83
+ legacy: bool = False,
84
+ resolution: TSRES = TSRES.NS1,
85
+ fractional: bool = True,
86
+ ignore_rollover: bool = False,
87
+ ) -> Tuple[TimestampArray, DetectorArray]:
88
+ """Internal parser for a1 format.
89
+
90
+ 'data' should have shape (N, 2).
91
+ """
92
+ assert data.ndim == 2 and data.shape[1] == 2
93
+
94
+ high_pos = 1
95
+ low_pos = 0
96
+ if legacy:
97
+ high_pos, low_pos = low_pos, high_pos
98
+
99
+ if ignore_rollover:
100
+ r = (data[:, low_pos] & 0b10000).astype(bool) # check rollover flag
101
+ data = data[~r]
102
+
103
+ high_words = data[:, high_pos].astype(np.uint64) << np.uint(22)
104
+ low_words = data[:, low_pos] >> np.uint(10)
105
+ ts = _format_timestamps(high_words + low_words, resolution, fractional)
106
+ ps = data[:, low_pos] & np.uint32(0xF)
107
+ return ts, ps
108
+
109
+
110
+ def _parse_a0(
111
+ data: NDArray[np.uint32],
112
+ resolution: TSRES = TSRES.NS1,
113
+ fractional: bool = True,
114
+ ignore_rollover: bool = False,
115
+ ) -> Tuple[TimestampArray, DetectorArray]:
116
+ return _parse_a1(data, False, resolution, fractional, ignore_rollover)
117
+
118
+
119
+ def _parse_a2(
120
+ data: NDArray[np.uint64],
121
+ resolution: TSRES = TSRES.NS1,
122
+ fractional: bool = True,
123
+ ignore_rollover: bool = False,
124
+ ) -> Tuple[TimestampArray, DetectorArray]:
125
+ assert data.ndim == 1
126
+
127
+ if ignore_rollover:
128
+ r = (data & 0b10000).astype(bool) # check rollover flag
129
+ data = data[~r]
130
+
131
+ t = data >> np.uint64(10)
132
+ ts = _format_timestamps(t, resolution, fractional)
133
+ ps = (data & 0xF).astype(np.uint32)
134
+ return ts, ps
135
+
136
+
137
+ def _format_timestamps(
138
+ t: NDArray[np.uint64],
139
+ resolution: TSRES,
140
+ fractional: bool,
141
+ ) -> TimestampArray:
142
+ """Returns conversion of timestamps into desired format and resolution.
143
+
144
+ Note:
145
+ Short-circuit when raw timestamps are requested is applied, i.e.
146
+ 'resolution' of TSRES.PS4 and 'fractional' is False. Avoiding computation
147
+ across the entire array has significant time-savings, see below:
148
+
149
+ >>> import timeit
150
+ >>> t = np.random.randint(0, int(1e18), size=(100_000,), dtype=np.uint64)
151
+ >>> f = lambda: _format_timestamps(t, TSRES.PS4, False)
152
+ >>> _ = timeit.timeit(f, number=10_000)
153
+ # 430 us per loop without short-circuit
154
+ # 989 ns per loop with short-circuit
155
+
156
+ In practice, the number of batches to process is not particularly high,
157
+ e.g. 1GB timestamp file corresponds to 1250 loops.
158
+ """
159
+ if fractional:
160
+ tf = t.astype(NP_PRECISEFLOAT)
161
+ return tf / (TSRES.PS4.value / resolution.value)
162
+ elif resolution is not TSRES.PS4: # short-circuit
163
+ return t // np.uint(TSRES.PS4.value // resolution.value)
164
+ return t
165
+
166
+
167
+ ########################
168
+ # UNBUFFERED READERS #
169
+ ########################
170
+
171
+
172
+ def read_a0(
173
+ filename: PathLike,
174
+ legacy: Optional[bool] = None,
175
+ resolution: TSRES = TSRES.NS1,
176
+ fractional: bool = True,
177
+ ignore_rollover: bool = False,
178
+ ) -> Tuple[TimestampArray, DetectorArray]:
179
+ """Converts a0 timestamp format into timestamps and detector pattern.
180
+
181
+ 'legacy' is set at the point of 'readevents' invocation, and determines
182
+ the timestamp storage format.
183
+
184
+ 'resolution' can be set to any of the available TSRES enumeration, and
185
+ denotes the units of the output timestamps. 'fractional' determines
186
+ whether sub-unit precisions should be preserved as well. The combination
187
+ of 'resolution' and 'fractional' will determine the resulting output
188
+ format of the timestamps.
189
+
190
+ Note:
191
+ 128-bit floating point is required since 64-bit float only has a
192
+ precision of 53-bits (timestamps have precision of 54-bits). If
193
+ the bandwidth cost is too high when using 128-bit floats, then
194
+ chances are the application also does not require sub-ns precision.
195
+
196
+ There is no legacy formatting for event types a0 and a2. Preserved in
197
+ the function signature to maintain parity with 'read_a1'.
198
+ """
199
+ data = np.genfromtxt(filename, delimiter="\n", dtype="S8")
200
+ data = np.array([int(v, 16) for v in data], dtype=np.uint32).reshape(-1, 2)
201
+ return _parse_a0(data, resolution, fractional, ignore_rollover)
202
+
203
+
204
+ def _read_a1(
205
+ filename: PathLike,
206
+ legacy: bool = False,
207
+ resolution: TSRES = TSRES.NS1,
208
+ fractional: bool = True,
209
+ ignore_rollover: bool = False,
210
+ ) -> Tuple[TimestampArray, DetectorArray]:
211
+ """See documentation for 'read_a0'."""
212
+ with open(filename, "rb") as f:
213
+ data = np.fromfile(file=f, dtype="<u4").reshape(-1, 2) # uint32
214
+ return _parse_a1(data, legacy, resolution, fractional, ignore_rollover)
215
+
216
+
217
+ def read_a2(
218
+ filename: PathLike,
219
+ legacy: Optional[bool] = None,
220
+ resolution: TSRES = TSRES.NS1,
221
+ fractional: bool = True,
222
+ ignore_rollover: bool = False,
223
+ ) -> Tuple[TimestampArray, DetectorArray]:
224
+ """See documentation for 'read_a0'."""
225
+ data = np.genfromtxt(filename, delimiter="\n", dtype="S16")
226
+ data = np.array([int(v, 16) for v in data], dtype=np.uint64)
227
+ return _parse_a2(data, resolution, fractional, ignore_rollover)
228
+
229
+
230
+ ######################
231
+ # BUFFERED READERS #
232
+ ######################
233
+
234
+
235
+ def read_a1(
236
+ filename: PathLike,
237
+ legacy: bool = False,
238
+ resolution: TSRES = TSRES.NS1,
239
+ fractional: bool = True,
240
+ start: Optional[Number] = None,
241
+ end: Optional[Number] = None,
242
+ pattern: Optional[Integer] = None,
243
+ mask: bool = False,
244
+ invert: bool = False,
245
+ buffer_size: Optional[Integer] = 100_000,
246
+ relative_time: bool = False,
247
+ ignore_rollover: bool = False,
248
+ ) -> Tuple[TimestampArray, DetectorArray]:
249
+ """Wrapper to sread_a1 for efficiently reading subsets of whole file.
250
+
251
+ For descriptions of arguments, read the documentation:
252
+
253
+ - 'read_a0': Importing timestamps.
254
+ - 'sread_a1': Importing timestamps via file streaming.
255
+ - 'get_timing_bounds': Filtering by timing.
256
+ - 'get_pattern_mask': Filtering by detector pattern.
257
+
258
+ If 'buffer_size' of None is supplied, the old legacy '_read_a1' code
259
+ will be used, and all timing/pattern filtering will be disabled.
260
+
261
+ If 'relative_time' is True, 'start' and 'end' values (in seconds) will
262
+ be shifted relative to the first timestamp.
263
+ """
264
+ if buffer_size is None:
265
+ return _read_a1(filename, legacy, resolution, fractional, ignore_rollover)
266
+
267
+ streamer, _ = sread_a1(
268
+ filename, legacy, resolution, fractional, buffer_size, ignore_rollover
269
+ )
270
+ ts = []
271
+ ps = []
272
+ commenced_reading = False
273
+ has_applied_time_offset = False
274
+ for t, p in streamer:
275
+ if relative_time and not has_applied_time_offset:
276
+ has_applied_time_offset = True
277
+ if start is not None:
278
+ start += t[0] * 1e-9
279
+ if end is not None:
280
+ end += t[0] * 1e-9
281
+
282
+ i, j = get_timing_bounds(t, start, end, resolution)
283
+ t = t[i:j]
284
+ p = p[i:j]
285
+ if pattern is not None:
286
+ pmask = get_pattern_mask(p, pattern, mask, invert)
287
+ t = t[pmask]
288
+ p = p[pmask]
289
+
290
+ # Terminate if no more timings readable
291
+ if commenced_reading and len(t) == 0:
292
+ break
293
+ elif len(t) != 0:
294
+ commenced_reading = True
295
+ ts.append(t)
296
+ ps.append(p)
297
+
298
+ # Return array of correct dtype if no data
299
+ if len(ts) == 0:
300
+ empty = np.array([], dtype=np.uint64)
301
+ t = _format_timestamps(empty, resolution, fractional)
302
+ p = np.array([], dtype=np.uint32) # consistent with %I format code
303
+ return t, p
304
+
305
+ t = np.concatenate(ts)
306
+ p = np.concatenate(ps)
307
+ return t, p
308
+
309
+
310
+ def read_a1_from_buffer(
311
+ buffer: Union[bytes, bytearray],
312
+ legacy: bool = False,
313
+ resolution: TSRES = TSRES.NS1,
314
+ fractional: bool = True,
315
+ ignore_rollover: bool = False,
316
+ ):
317
+ data = np.frombuffer(buffer, dtype="<u4").reshape(-1, 2)
318
+ return _parse_a1(data, legacy, resolution, fractional, ignore_rollover)
319
+
320
+
321
+ def read_a0_from_buffer(
322
+ buffer: str,
323
+ legacy: Optional[bool] = None,
324
+ resolution: TSRES = TSRES.NS1,
325
+ fractional: bool = True,
326
+ ignore_rollover: bool = False,
327
+ ):
328
+ data = buffer.strip().split("\n")
329
+ data = np.array([int(v, 16) for v in data]).reshape(-1, 2)
330
+ return _parse_a0(data, resolution, fractional, ignore_rollover)
331
+
332
+
333
+ def read_a2_from_buffer(
334
+ buffer: str,
335
+ legacy: Optional[bool] = None,
336
+ resolution: TSRES = TSRES.NS1,
337
+ fractional: bool = True,
338
+ ignore_rollover: bool = False,
339
+ ):
340
+ data = buffer.strip().split("\n")
341
+ data = np.array([int(v, 16) for v in data])
342
+ return _parse_a2(data, resolution, fractional, ignore_rollover)
343
+
344
+
345
+ ########################
346
+ # BUFFERED STREAMERS #
347
+ ########################
348
+
349
+
350
+ def sread_a1(
351
+ filename: PathLike,
352
+ legacy: bool = False,
353
+ resolution: TSRES = TSRES.NS1,
354
+ fractional: bool = True,
355
+ buffer_size: Integer = 100_000,
356
+ ignore_rollover: bool = False,
357
+ ) -> Tuple[TimestampDetectorStream, int]:
358
+ """Block streaming variant of 'read_a1'.
359
+
360
+ For large timestamp datasets where either not all timestamps need
361
+ to be loaded into memory, or only statistics need to be retrieved.
362
+ 'buffer_size' in bytes, other arguments to follow as documented in
363
+ 'read_a0'.
364
+
365
+ If timestamp formatting is not required, the raw timestamp resolution
366
+ can be used to speed up computation time, i.e. 'resolution' of
367
+ TSRES.PS4 and 'fractional' of False.
368
+
369
+ Note that these streamer can easily be extended to perform some
370
+ pre-processing cleanup, see Usage.
371
+
372
+ Args:
373
+ buffer_size: Buffer size in number of events.
374
+
375
+ Examples:
376
+ >>> for t, p in sread_a1(...):
377
+ ... print(t, p)
378
+
379
+ Note:
380
+ Performance of naive streaming is poor (roughly 2 orders magnitude
381
+ slower), and deprecated thusly. A compromise, whereby blocks are
382
+ streamed instead, yields minimal performance loss while avoiding
383
+ issues with memory (to avoid an OOM kill).
384
+
385
+ A buffer size between 100kB and 1MB is optimal. Disk read latencies
386
+ dominate at low buffer sizes, while memory allocation latencies
387
+ dominate at high buffer sizes.
388
+
389
+ Where efficiency is desired and number of timestamps is small,
390
+ 'read_a1' should be preferred instead.
391
+ """
392
+
393
+ def _sread_a1():
394
+ with open(filename, "rb") as f:
395
+ while True:
396
+ buffer = f.read(buffer_size * 8) # 8 bytes per event
397
+ if len(buffer) == 0:
398
+ break
399
+ yield read_a1_from_buffer(
400
+ buffer, legacy, resolution, fractional, ignore_rollover
401
+ )
402
+
403
+ size = pathlib.Path(filename).stat().st_size
404
+ num_batches = int(((size - 1) // (buffer_size * 8)) + 1)
405
+ return _sread_a1(), num_batches
406
+
407
+
408
+ def sread_a0(
409
+ filename: PathLike,
410
+ legacy: Optional[bool] = None,
411
+ resolution: TSRES = TSRES.NS1,
412
+ fractional: bool = True,
413
+ buffer_size: Integer = 100_000,
414
+ ignore_rollover: bool = False,
415
+ ):
416
+ """See documentation for 'sread_a1'"""
417
+
418
+ def _sread_a0():
419
+ with open(filename, "r") as f:
420
+ while True:
421
+ buffer = f.read(buffer_size * 18) # 16 char per event + 2 newlines
422
+ if len(buffer) == 0:
423
+ break
424
+ yield read_a0_from_buffer(
425
+ buffer, legacy, resolution, fractional, ignore_rollover
426
+ )
427
+
428
+ size = pathlib.Path(filename).stat().st_size
429
+ num_batches = int(((size - 1) // (buffer_size * 18)) + 1)
430
+ return _sread_a0(), num_batches
431
+
432
+
433
+ def sread_a2(
434
+ filename: PathLike,
435
+ legacy: Optional[bool] = None,
436
+ resolution: TSRES = TSRES.NS1,
437
+ fractional: bool = True,
438
+ buffer_size: Integer = 100_000,
439
+ ignore_rollover: bool = False,
440
+ ):
441
+ """See documentation for 'sread_a1'"""
442
+
443
+ def _sread_a2():
444
+ with open(filename, "r") as f:
445
+ while True:
446
+ buffer = f.read(buffer_size * 17) # 16 char per event + 1 char newline
447
+ if len(buffer) == 0:
448
+ break
449
+ yield read_a2_from_buffer(
450
+ buffer, legacy, resolution, fractional, ignore_rollover
451
+ )
452
+
453
+ size = pathlib.Path(filename).stat().st_size
454
+ num_batches = int(((size - 1) // (buffer_size * 17)) + 1)
455
+ return _sread_a2(), num_batches
456
+
457
+
458
+ #########################
459
+ # AUXILIARY FUNCTIONS #
460
+ #########################
461
+
462
+
463
+ def read_a1_start_end(
464
+ filename: PathLike,
465
+ legacy: bool = False,
466
+ resolution: TSRES = TSRES.NS1,
467
+ fractional: bool = True,
468
+ ignore_rollover: bool = False,
469
+ ):
470
+ t, _ = read_a1_kth_timestamp(
471
+ filename,
472
+ [0, -1],
473
+ legacy,
474
+ resolution,
475
+ fractional,
476
+ ignore_rollover,
477
+ )
478
+ return t * 1e-9
479
+
480
+
481
+ def read_a1_kth_timestamp(
482
+ filename: PathLike,
483
+ k: IntegerArray,
484
+ legacy: bool = False,
485
+ resolution: TSRES = TSRES.NS1,
486
+ fractional: bool = True,
487
+ ignore_rollover: bool = False,
488
+ ) -> Tuple[TimestampArray, DetectorArray]:
489
+ filesize = pathlib.Path(filename).stat().st_size
490
+ num_events = filesize / 8 # 8 bytes per event
491
+
492
+ # Parse indices
493
+ ks = np.array(k).astype(int)
494
+ ks = np.array(
495
+ [(num_events + k if k < 0 else k) for k in ks]
496
+ ) # convert to positive ints
497
+ if np.any(ks >= num_events):
498
+ raise ValueError(f"Index out-of-bounds, only {num_events} events available.")
499
+
500
+ buffer = bytearray()
501
+ with open(filename, "rb") as f:
502
+ for _k in ks:
503
+ f.seek(int(_k) * 8, io.SEEK_SET) # should be O(1) in modern filesystems
504
+ buffer.extend(f.read(8))
505
+
506
+ return read_a1_from_buffer(buffer, legacy, resolution, fractional, ignore_rollover)
507
+
508
+
509
+ def read_a1_overlapping(
510
+ *filenames: PathLike,
511
+ legacy: bool = False,
512
+ resolution: TSRES = TSRES.NS1,
513
+ fractional: bool = True,
514
+ duration: Optional[Float] = None,
515
+ ):
516
+ """Reads multiple timestamp files with overlapping durations.
517
+
518
+ Convenience function that replaces a common workflow. Streaming is used,
519
+ and no filtering patterns are used. No detector patterns are also returned.
520
+
521
+ If 'duration' is unspecified, the full overlapping timestamps are extracted.
522
+ Otherwise, the timestamps will be truncated by the corresponding duration,
523
+ in units of seconds.
524
+ """
525
+ # Search for start and end timings
526
+ timings = np.array([read_a1_start_end(f, legacy=legacy) for f in filenames])
527
+ start = np.max(timings[:, 0])
528
+ end = np.min(timings[:, 1])
529
+ if duration is not None:
530
+ if duration > end - start:
531
+ warnings.warn(
532
+ "Duration requested is longer than available data: "
533
+ f"{duration:.2f}s > {end - start:.2f}s"
534
+ )
535
+ end = duration + start
536
+
537
+ # Extract timestamps only
538
+ data = [
539
+ read_a1(
540
+ filename,
541
+ legacy=legacy,
542
+ resolution=resolution,
543
+ fractional=fractional,
544
+ start=start,
545
+ end=end,
546
+ )
547
+ for filename in filenames
548
+ ]
549
+ timestamps, channels = list(zip(*data))
550
+ return timestamps, channels
551
+
552
+
553
+ ########################
554
+ # UNBUFFERED WRITERS #
555
+ ########################
556
+
557
+
558
+ @numba.njit(parallel=True)
559
+ def _duplicate_event_removal(data: NDArray[np.uint64]) -> NDArray[np.uint64]:
560
+ """Subroutine for _consolidate_events().
561
+
562
+ On top of removing duplicate timestamps, this function also merges duplicate
563
+ timestamps across different channels.
564
+
565
+ Note:
566
+ The numba optimization is critical: the runtimes are 31.7s (without numba),
567
+ 0.5s (with numba 1s compile), 0.4s (with numba parallel 1.6s compile), for
568
+ a 15 Mevents timestamp file. This function is compiled lazily, i.e. only
569
+ when duplicate removal is required.
570
+ """
571
+ # Identify unique timestamps and their indices
572
+ ts = data >> np.uint64(10)
573
+ idxs = np.flatnonzero(ts[1:] != ts[:-1]) + 1
574
+ idxs = np.concatenate((np.array([np.int64(0)]), idxs, np.array([ts.size])))
575
+ _t = ts[idxs[:-1]]
576
+ _p = np.zeros_like(_t)
577
+
578
+ # 'p' in principle can be supplied directly from '_consolidate_events()', but
579
+ # we re-extract here from 'data' for clarity + usability. Negligible perf impact.
580
+ p = data & np.uint64(0b1111)
581
+
582
+ # Combine all detector patterns for the same timestamp
583
+ for i in numba.prange(len(idxs) - 1):
584
+ left, right = idxs[i], idxs[i + 1]
585
+ _pi = p[left]
586
+ for j in range(left + 1, right):
587
+ _pi |= p[j]
588
+ _p[i] = _pi
589
+
590
+ # Recombine
591
+ data = (_t << np.uint64(10)) + _p
592
+ return data
593
+
594
+
595
+ def _consolidate_events(
596
+ t: TimestampArray,
597
+ p: DetectorArray,
598
+ resolution: TSRES = TSRES.NS1,
599
+ sort: bool = False,
600
+ allow_duplicates: bool = True,
601
+ ) -> NDArray[np.uint64]:
602
+ """Packs events into standard a1 timestamp format.
603
+
604
+ If sorting is required, such as during timestamp merging or timestamp
605
+ perturbation, set 'sort' to True. This invokes O(NlogN) quick sort.
606
+
607
+ Args:
608
+ t: Timestamp array, in units of TSRES.NS1.
609
+ p: Detector pattern array.
610
+ resolution: Resolution of timestamps in timestamp array.
611
+ sort: If events should be further sorted in chronological order.
612
+ allow_duplicates: If events with the same timestamp should be combined,
613
+ i.e. similar to the output from an actual time tagger.
614
+
615
+ Note:
616
+ float128 is needed, since float64 only encodes 53-bits of precision,
617
+ while the high resolution timestamp has 54-bits precision.
618
+ """
619
+ data = (
620
+ np.array(t, dtype=NP_PRECISEFLOAT) * (TSRES.PS4.value // resolution.value)
621
+ ).astype(np.uint64) << np.uint64(10)
622
+ data += np.array(p).astype(np.uint64)
623
+ if sort:
624
+ data = np.sort(data)
625
+
626
+ # Duplicate removal can only be executed on a sorted array
627
+ if not allow_duplicates:
628
+ data = _duplicate_event_removal(data)
629
+
630
+ return data
631
+
632
+
633
+ def write_a2(
634
+ filename: PathLike,
635
+ t: TimestampArray,
636
+ p: DetectorArray,
637
+ legacy: Optional[bool] = None,
638
+ resolution: TSRES = TSRES.NS1,
639
+ allow_duplicates: bool = True,
640
+ ) -> None:
641
+ data = _consolidate_events(t, p, resolution, allow_duplicates=allow_duplicates)
642
+ with open(filename, "w") as f:
643
+ for line in data:
644
+ f.write(f"{line:016x}\n")
645
+
646
+
647
+ def write_a0(
648
+ filename: PathLike,
649
+ t: TimestampArray,
650
+ p: DetectorArray,
651
+ legacy: Optional[bool] = None,
652
+ resolution: TSRES = TSRES.NS1,
653
+ allow_duplicates: bool = True,
654
+ ) -> None:
655
+ events = _consolidate_events(t, p, resolution, allow_duplicates=allow_duplicates)
656
+ data = np.empty((2 * events.size,), dtype=np.uint32)
657
+ data[0::2] = events & 0xFFFFFFFF
658
+ data[1::2] = events >> 32
659
+ with open(filename, "w") as f:
660
+ for line in data:
661
+ f.write(f"{line:08x}\n")
662
+
663
+
664
+ def write_a1(
665
+ filename: PathLike,
666
+ t: TimestampArray,
667
+ p: DetectorArray,
668
+ legacy: bool = False,
669
+ resolution: TSRES = TSRES.NS1,
670
+ allow_duplicates: bool = True,
671
+ ) -> None:
672
+ events = _consolidate_events(t, p, resolution, allow_duplicates=allow_duplicates)
673
+ with open(filename, "wb") as f:
674
+ for line in events:
675
+ if legacy:
676
+ line = int(line)
677
+ line = ((line & 0xFFFFFFFF) << 32) + (line >> 32)
678
+ f.write(struct.pack("=Q", line))
679
+
680
+
681
+ ######################
682
+ # BUFFERED WRITERS #
683
+ ######################
684
+
685
+
686
+ def swrite_a1(
687
+ filename: PathLike,
688
+ stream: TimestampDetectorStream,
689
+ num_batches: Optional[Integer] = None,
690
+ legacy: bool = False,
691
+ resolution: TSRES = TSRES.NS1,
692
+ display: bool = True,
693
+ allow_duplicates: bool = True,
694
+ ) -> None:
695
+ """Block streaming variant of 'write_a1'.
696
+
697
+ Input stream assumed to have TSRES.NS1 resolution.
698
+
699
+ Args:
700
+ filename: Destination filename.
701
+ stream: Input stream, in TSRES.NS1 resolution.
702
+ num_batches: Number of batches in stream.
703
+ legacy: If output timestamp format should be in legacy format.
704
+ resolution: Resolution of timestamps in input stream.
705
+ display: If progress bar should be displayed during write.
706
+
707
+ Note:
708
+ Settling on a fixed resolution allows for more consistent interface
709
+ for filtering functions. Code runtime with filtering will take a hit,
710
+ however - this is a future todo.
711
+ """
712
+ if display:
713
+ stream = tqdm.tqdm(stream, total=num_batches) # type: ignore (tqdm constraint)
714
+ with open(filename, "wb") as f:
715
+ for t, p in stream:
716
+ events = _consolidate_events(
717
+ t, p, resolution, allow_duplicates=allow_duplicates
718
+ )
719
+ for line in events:
720
+ if legacy:
721
+ line = int(line)
722
+ line = ((line & 0xFFFFFFFF) << 32) + (line >> 32)
723
+ f.write(struct.pack("=Q", line))
724
+
725
+
726
+ def swrite_a0(
727
+ filename: PathLike,
728
+ stream: TimestampDetectorStream,
729
+ num_batches: Optional[Integer] = None,
730
+ legacy: Optional[bool] = None,
731
+ resolution: TSRES = TSRES.NS1,
732
+ display: bool = True,
733
+ allow_duplicates: bool = True,
734
+ ) -> None:
735
+ """See documentation for 'swrite_a1'."""
736
+ if display:
737
+ stream = tqdm.tqdm(stream, total=num_batches) # type: ignore (tqdm constraint)
738
+ with open(filename, "w") as f:
739
+ for t, p in stream:
740
+ events = _consolidate_events(
741
+ t, p, resolution, allow_duplicates=allow_duplicates
742
+ )
743
+ data = np.empty((2 * events.size,), dtype=np.uint32)
744
+ data[0::2] = events & 0xFFFFFFFF
745
+ data[1::2] = events >> 32
746
+ for line in data:
747
+ f.write(f"{line:08x}\n")
748
+
749
+
750
+ def swrite_a2(
751
+ filename: PathLike,
752
+ stream: TimestampDetectorStream,
753
+ num_batches: Optional[Integer] = None,
754
+ legacy: Optional[bool] = None,
755
+ resolution: TSRES = TSRES.NS1,
756
+ display: bool = True,
757
+ allow_duplicates: bool = True,
758
+ ) -> None:
759
+ """See documentation for 'swrite_a1'."""
760
+ if display:
761
+ stream = tqdm.tqdm(stream, total=num_batches) # type: ignore (tqdm constraint)
762
+ with open(filename, "w") as f:
763
+ for t, p in stream:
764
+ data = _consolidate_events(
765
+ t, p, resolution, allow_duplicates=allow_duplicates
766
+ )
767
+ for line in data:
768
+ f.write(f"{line:016x}\n")
769
+
770
+
771
+ ##############
772
+ # PRINTERS #
773
+ ##############
774
+
775
+
776
+ def print_statistics(filename: PathLike, t: TimestampArray, p: DetectorArray) -> None:
777
+ """Prints statistics using timestamp event readers.
778
+
779
+ Note:
780
+ Maintained only for legacy reasons to support older
781
+ reading mechanisms, e.g. 'read_a0'.
782
+ """
783
+ # Collect statistics
784
+ count = np.count_nonzero
785
+ _ch1 = count(p & 0b0001 != 0)
786
+ _ch2 = count(p & 0b0010 != 0)
787
+ _ch3 = count(p & 0b0100 != 0)
788
+ _ch4 = count(p & 0b1000 != 0)
789
+ print_statistics_report(
790
+ filename=filename,
791
+ num_events=len(t),
792
+ num_detections=_ch1 + _ch2 + _ch3 + _ch4,
793
+ ch1_counts=_ch1,
794
+ ch2_counts=_ch2,
795
+ ch3_counts=_ch3,
796
+ ch4_counts=_ch4,
797
+ multi_counts=count(np.isin(p, (0, 1, 2, 4, 8), invert=True)),
798
+ non_counts=count(p == 0),
799
+ start_timestamp=t[0],
800
+ end_timestamp=t[-1],
801
+ patterns=sorted(np.unique(p)),
802
+ )
803
+
804
+
805
+ def print_statistics_stream(
806
+ filename: PathLike,
807
+ stream: TimestampDetectorStream,
808
+ num_batches: Optional[Integer] = None,
809
+ resolution: TSRES = TSRES.NS1,
810
+ display: bool = True,
811
+ ) -> None:
812
+ """Prints statistics using timestamp event streamers."""
813
+ # Calculate statistics
814
+ first_t = None
815
+ last_t = None
816
+ num_events = 0
817
+ num_detections = 0
818
+ count_p1 = 0
819
+ count_p2 = 0
820
+ count_p3 = 0
821
+ count_p4 = 0
822
+ count_mp = 0
823
+ count_np = 0
824
+ set_p = set()
825
+
826
+ # Processs batches of events
827
+ count = np.count_nonzero
828
+ if display:
829
+ stream = tqdm.tqdm(stream, total=num_batches) # type: ignore (tqdm constraint)
830
+ for t, p in stream:
831
+ _ch1 = count(p & 0b0001 != 0)
832
+ _ch2 = count(p & 0b0010 != 0)
833
+ _ch3 = count(p & 0b0100 != 0)
834
+ _ch4 = count(p & 0b1000 != 0)
835
+ num_events += len(p)
836
+ num_detections += _ch1 + _ch2 + _ch3 + _ch4
837
+ count_p1 += _ch1
838
+ count_p2 += _ch2
839
+ count_p3 += _ch3
840
+ count_p4 += _ch4
841
+ count_mp += count(np.isin(p, (0, 1, 2, 4, 8), invert=True))
842
+ count_np += count(p == 0)
843
+ set_p.update(np.unique(p))
844
+
845
+ # Store timing only if timestamps not filtered out
846
+ if len(t) != 0:
847
+ if first_t is None:
848
+ first_t = t[0] / resolution.value # convert to nanoseconds
849
+ last_t = t[-1] / resolution.value
850
+
851
+ print_statistics_report(
852
+ filename=filename,
853
+ num_events=num_events,
854
+ num_detections=num_detections,
855
+ ch1_counts=count_p1,
856
+ ch2_counts=count_p2,
857
+ ch3_counts=count_p3,
858
+ ch4_counts=count_p4,
859
+ multi_counts=count_mp,
860
+ non_counts=count_np,
861
+ start_timestamp=first_t,
862
+ end_timestamp=last_t,
863
+ patterns=sorted(set_p),
864
+ )
865
+
866
+
867
+ def print_statistics_report(
868
+ filename: PathLike,
869
+ num_events: Integer,
870
+ num_detections: Integer,
871
+ ch1_counts: Optional[Integer] = None,
872
+ ch2_counts: Optional[Integer] = None,
873
+ ch3_counts: Optional[Integer] = None,
874
+ ch4_counts: Optional[Integer] = None,
875
+ multi_counts: Optional[Integer] = None,
876
+ non_counts: Optional[Integer] = None,
877
+ start_timestamp: Optional[Float] = None,
878
+ end_timestamp: Optional[Float] = None,
879
+ patterns: Optional[list] = None,
880
+ ) -> None:
881
+ """Prints the statistics report.
882
+
883
+ All optional fields must be present if 'num_events' > 0.
884
+
885
+ Events and detections have the following definitions: multi-channel events
886
+ are counted as a single event for the former, and as separate events for
887
+ the latter.
888
+ """
889
+
890
+ print(f"Name: {str(filename)}")
891
+ if pathlib.Path(filename).is_file():
892
+ filesize = pathlib.Path(filename).stat().st_size / (1 << 20)
893
+ print(f"Filesize (MB): {filesize:.3f}")
894
+ width = 0
895
+ if num_events != 0:
896
+ width = int(np.floor(np.log10(num_events))) + 1
897
+ print(f"Total events : {num_events:>{width}d}")
898
+ if num_events != 0:
899
+ if start_timestamp is None or end_timestamp is None or patterns is None:
900
+ return
901
+ duration = (end_timestamp - start_timestamp) * 1e-9
902
+ print(f" Detections : {num_detections:>{width}d}")
903
+ print(f" Channel 1 : {ch1_counts:>{width}d}")
904
+ print(f" Channel 2 : {ch2_counts:>{width}d}")
905
+ print(f" Channel 3 : {ch3_counts:>{width}d}")
906
+ print(f" Channel 4 : {ch4_counts:>{width}d}")
907
+ print(f" Multi-channel : {multi_counts:>{width}d}")
908
+ print(f" No channel : {non_counts:>{width}d}")
909
+ print(f"Duration (s) : {duration:15.9f}")
910
+ print(f" ~ start : {start_timestamp * 1e-9:>15.9f}")
911
+ print(f" ~ end : {end_timestamp * 1e-9:>15.9f}")
912
+ print(f"Event rate (/s) : {int(num_events // duration)}")
913
+ print(f" Detection rate : {int(num_detections // duration)}")
914
+ print(f"Detection patterns: {sorted(map(int, patterns))}")
915
+
916
+
917
+ ###############
918
+ # FILTERING #
919
+ ###############
920
+
921
+
922
+ def get_pattern_mask(
923
+ p: DetectorArray,
924
+ pattern: Integer,
925
+ mask: bool = False,
926
+ invert: bool = False,
927
+ ) -> Tuple[NDArray[np.bool_], DetectorArray]:
928
+ """Returns a mask with bits set where patterns match, as well as result.
929
+
930
+ The function behaves differently when the pattern is either used as
931
+ a fixed pattern (when 'mask' is False), or as a bitmask (when 'mask'
932
+ is True).
933
+
934
+ When 'mask' is False, pattern matching with select events whose detector
935
+ patterns match the exact pattern. Inverting with 'invert' as False will
936
+ remove these events instead (i.e. pattern blacklist).
937
+
938
+ When 'mask' is True, only events containing any bits in the pattern will
939
+ be selected. Inverting with 'invert' as True will remove detector bits
940
+ corresponding to any of said bits.
941
+
942
+ Args:
943
+ p: List of detector patterns.
944
+ pattern: Pattern to match.
945
+ mask: Whether pattern should be used as a bitmask.
946
+ invert: Whether selection rules should be inverted. See documentation.
947
+
948
+ Examples:
949
+
950
+ >>> p = np.array([0,1,2,3,4,5,6,7,8]); t = np.array(p)
951
+
952
+ >>> mask1, p1 = get_pattern_mask(p, 0b0101, False, False)
953
+ >>> list(t[mask1]) == list(p1) and list(p1) == [5]
954
+ True
955
+
956
+ >>> mask2, p2 = get_pattern_mask(p, 0b0101, False, True)
957
+ >>> list(t[mask2]) == list(p2) and list(p2) == [0,1,2,3,4,6,7,8]
958
+ True
959
+
960
+ >>> mask3, p3 = get_pattern_mask(p, 0b0101, True, False)
961
+ >>> list(t[mask3]) == [1,3,4,5,6,7] and list(p3) == [1,1,4,5,4,5]
962
+ True
963
+
964
+ >>> mask4, p4 = get_pattern_mask(p, 0b0101, True, True)
965
+ >>> list(t[mask4]) == [0,2,3,6,7,8] and list(p4) == [0,2,2,2,2,8]
966
+ True
967
+
968
+ Note:
969
+ The function is designed to return a bitmask instead of directly
970
+ filtering the timestamps, to allow subsequent post-processing based
971
+ on the bitmask, e.g. inverting the returned bitmask, or mark patterns.
972
+ """
973
+ masked: DetectorArray
974
+ pattern = np.uint32(pattern)
975
+
976
+ # Fixed pattern
977
+ if not mask:
978
+ pmask = p == pattern
979
+ if invert:
980
+ pmask = ~pmask
981
+ masked = p[pmask]
982
+
983
+ # Pattern as bitmask
984
+ elif not invert:
985
+ _p = p & pattern # patterns containing bitmask bits
986
+ pmask = _p != 0
987
+ masked = _p[pmask]
988
+ else:
989
+ # Set bit 4 to indicate dummy events, since
990
+ # non-events already do not contain the bit pattern
991
+ _p = np.where(p == 0, 16, p)
992
+ _p = _p ^ (_p & pattern) # patterns with bitmask removed
993
+ pmask = _p != 0
994
+ masked = _p[pmask] # return detector patterns after masking
995
+ masked = np.where(masked == 16, 0, masked) # recover dummy events
996
+
997
+ return pmask, masked
998
+
999
+
1000
+ def get_timing_bounds(
1001
+ t: TimestampArray,
1002
+ start: Optional[Number] = None,
1003
+ end: Optional[Number] = None,
1004
+ resolution: TSRES = TSRES.NS1,
1005
+ ) -> Tuple[int, int]:
1006
+ """Returns indices of start and end for region where timings match.
1007
+
1008
+ The timing array is already assumed to be sorted, as part of the timestamp
1009
+ filespec. If 'start' or 'end' is None, the range is assumed unbounded in respective
1010
+ direction. Start and end time are in seconds (not ns since typical usecase as rough
1011
+ filter anyway).
1012
+
1013
+ This function is preferred over 'get_timing_mask' inline filtering,
1014
+ due to the overheads of masking.
1015
+
1016
+ Args:
1017
+ t: Timestamp array, in units of 'resolution'.
1018
+ start: Start timestamp, in seconds.
1019
+ end: End timestamp, in seconds.
1020
+ resolution: Resolution of timestamps in timestamp array.
1021
+
1022
+ Note:
1023
+ # Masking overhead
1024
+ >>> timeit
1025
+ >>> setup = "import numpy as np; size=1_000_000; a = np.random.random(size);"
1026
+ >>> setup_w_mask = setup + "mask = np.zeros(size).astype(bool);"
1027
+ >>> setup_w_mask += "mask[1000:999000] = True;"
1028
+ >>> timeit.timeit('a[1000:999000]', setup, number=1000)
1029
+ 0.00011489982716739178
1030
+ >>> timeit.timeit('a[mask]', setup_w_mask, number=1000)
1031
+ 1.7692412599921226
1032
+ """
1033
+ # Nothing in array
1034
+ NULL_RESULT = (0, 0)
1035
+ if len(t) == 0:
1036
+ return NULL_RESULT
1037
+
1038
+ if start is not None:
1039
+ start *= 1e9 * resolution.value # convert to same base for comp.
1040
+ if end is not None:
1041
+ end *= 1e9 * resolution.value
1042
+ _start = t[0]
1043
+ _end = t[-1]
1044
+
1045
+ # Find start index
1046
+ if (start is None) or (start <= _start):
1047
+ i = 0
1048
+ elif start > _end:
1049
+ return NULL_RESULT
1050
+ else:
1051
+ i = bisect.bisect_left(t, start) # binary search
1052
+
1053
+ # Find end index
1054
+ if (end is None) or (end >= _end):
1055
+ j = len(t)
1056
+ elif end < _start:
1057
+ return NULL_RESULT
1058
+ else:
1059
+ j = bisect.bisect_right(t, end)
1060
+
1061
+ return i, j
1062
+
1063
+
1064
+ def get_timing_mask(
1065
+ t: TimestampArray,
1066
+ start: Optional[Float] = None,
1067
+ end: Optional[Float] = None,
1068
+ resolution: TSRES = TSRES.NS1,
1069
+ ) -> NDArray[np.bool_]:
1070
+ """Returns a mask where timestamps are bounded between start and end.
1071
+
1072
+ The timing array is already assumed to be sorted, as part of the timestamp
1073
+ filespec. If 'start' or 'end' is None, the range is assumed unbounded in respective
1074
+ direction. Start and end time are in seconds (not ns since typical usecase as rough
1075
+ filter anyway).
1076
+
1077
+ Args:
1078
+ t: Timestamp array, in units of 'resolution'.
1079
+ start: Start timestamp, in seconds.
1080
+ end: End timestamp, in seconds.
1081
+ resolution: Resolution of timestamps in timestamp array.
1082
+
1083
+ Examples:
1084
+
1085
+ >>> to_ns = lambda ts: [t * 1e9 for t in ts]
1086
+ >>> t = np.array(to_ns([2,4,5,8]))
1087
+
1088
+ >>> mask1 = get_timing_mask(t, 4, 8) # range at boundaries
1089
+ >>> list(t[mask1]) == to_ns([4,5,8])
1090
+ True
1091
+
1092
+ >>> mask2 = get_timing_mask(t, 3, 6) # range outside boundaries
1093
+ >>> list(t[mask2]) == to_ns([4,5])
1094
+ True
1095
+
1096
+ >>> mask3 = get_timing_mask(t, 9, 100) # too high
1097
+ >>> list(t[mask3]) == to_ns([])
1098
+ True
1099
+
1100
+ >>> mask4 = get_timing_mask(t, 0, 1) # too low
1101
+ >>> list(t[mask4]) == to_ns([])
1102
+ True
1103
+
1104
+ >>> mask5 = get_timing_mask(t, 5, 5) # end is inclusive
1105
+ >>> list(t[mask5]) == to_ns([5])
1106
+ True
1107
+
1108
+ >>> mask6 = get_timing_mask(t, start=3, end=None) # only lower bound
1109
+ >>> list(t[mask6]) == to_ns([4,5,8])
1110
+ True
1111
+
1112
+ >>> mask7 = get_timing_mask(t, start=None, end=4) # only upper bound
1113
+ >>> list(t[mask7]) == to_ns([2,4])
1114
+ True
1115
+ """
1116
+ mask = np.zeros(len(t), dtype=bool)
1117
+ i, j = get_timing_bounds(t, start, end, resolution)
1118
+ mask[i:j] = True
1119
+ return mask
1120
+
1121
+
1122
+ ##########
1123
+ # MAIN #
1124
+ ##########
1125
+
1126
+
1127
+ def generate_parser():
1128
+ parser = argparse.ArgumentParser(
1129
+ description="Converts between different timestamp7 formats"
1130
+ )
1131
+
1132
+ parser.add_argument(
1133
+ "-q", "--quiet", action="store_true", help="Suppress progress indicators"
1134
+ )
1135
+ # Support for older read-write mechanisms, i.e. disable batch streaming
1136
+ parser.add_argument(
1137
+ "--inmemory",
1138
+ action="store_true",
1139
+ help="Disable batch streaming (retained for legacy reasons)",
1140
+ )
1141
+ parser.add_argument(
1142
+ "--rollover",
1143
+ action="store_true",
1144
+ help="Include rollover dummy events (generally unwanted)",
1145
+ )
1146
+
1147
+ pgroup = parser.add_argument_group("Timestamp formats")
1148
+ pgroup.add_argument(
1149
+ "-A",
1150
+ choices=["0", "1", "2"],
1151
+ default="1",
1152
+ help="Input timestamp format (default: 1)",
1153
+ )
1154
+ pgroup.add_argument(
1155
+ "-X", action="store_true", help="Use legacy input format (default: False)"
1156
+ )
1157
+ pgroup.add_argument(
1158
+ "-a",
1159
+ choices=["0", "1", "2"],
1160
+ default="1",
1161
+ help="Output timestamp format (default: 1)",
1162
+ )
1163
+ pgroup.add_argument(
1164
+ "-x", action="store_true", help="Use legacy output format (default: False)"
1165
+ )
1166
+
1167
+ # Filtering
1168
+ pgroup = parser.add_argument_group("Pattern filtering")
1169
+ pgroup.add_argument(
1170
+ "--pattern",
1171
+ type=int,
1172
+ metavar="",
1173
+ help="Specify pattern for filtering (e.g. 5 for ch1+ch3)",
1174
+ )
1175
+ pgroup.add_argument(
1176
+ "--mask",
1177
+ action="store_true",
1178
+ help="Use pattern as mask instead of fixed (default: False)",
1179
+ )
1180
+ pgroup.add_argument(
1181
+ "--invert",
1182
+ action="store_true",
1183
+ help="Exclude pattern instead of include (default: False)",
1184
+ )
1185
+
1186
+ pgroup = parser.add_argument_group("Timestamp filtering")
1187
+ pgroup.add_argument(
1188
+ "--start", type=float, metavar="", help="Specify start timestamp, in seconds"
1189
+ )
1190
+ pgroup.add_argument(
1191
+ "--end", type=float, metavar="", help="Specify end timestamp, in seconds"
1192
+ )
1193
+ pgroup.add_argument(
1194
+ "--relative-time",
1195
+ action="store_true",
1196
+ help="Use relative time to start timestamp instead.",
1197
+ )
1198
+
1199
+ parser.add_argument("infile", help="Input timestamp file")
1200
+ parser.add_argument(
1201
+ "outfile",
1202
+ nargs="?",
1203
+ const="",
1204
+ help="Output timestamp file (optional: not required for printing)",
1205
+ )
1206
+ return parser
1207
+
1208
+
1209
+ def main(): # noqa: PLR0915
1210
+ # Print help if no arguments supplied
1211
+ parser = generate_parser()
1212
+ if len(sys.argv) == 1:
1213
+ parser.print_help(sys.stderr)
1214
+ sys.exit(1)
1215
+
1216
+ args = parser.parse_args()
1217
+ print_only = args.outfile is None
1218
+ ignore_rollover = not args.rollover
1219
+
1220
+ read = [read_a0, read_a1, read_a2][int(args.A)]
1221
+ sread = [sread_a0, sread_a1, sread_a2][int(args.A)]
1222
+ write = [write_a0, write_a1, write_a2][int(args.a)]
1223
+ swrite = [swrite_a0, swrite_a1, swrite_a2][int(args.a)]
1224
+
1225
+ # Check file size
1226
+ filepath = pathlib.Path(args.infile)
1227
+ if not filepath.is_file():
1228
+ raise ValueError(f"'{args.infile}' is not a file.")
1229
+
1230
+ # Define filter function for event and event stream
1231
+ has_filtering = (
1232
+ (args.pattern is not None) or (args.start is not None) or (args.end is not None)
1233
+ )
1234
+
1235
+ # Apply relative time
1236
+ if args.relative_time:
1237
+ first_t, _ = read_a1_start_end(
1238
+ filepath,
1239
+ args.X,
1240
+ ignore_rollover=ignore_rollover,
1241
+ )
1242
+ if args.start is not None:
1243
+ args.start += first_t # default 1ns resolution
1244
+ if args.end is not None:
1245
+ args.end += first_t
1246
+
1247
+ def event_filter(t, p, resolution: TSRES = TSRES.NS1):
1248
+ if args.pattern is not None:
1249
+ mask, p = get_pattern_mask(p, args.pattern, args.mask, args.invert)
1250
+ t = t[mask]
1251
+ if args.start is not None or args.end is not None:
1252
+ mask = get_timing_mask(t, args.start, args.end, resolution)
1253
+ t = t[mask]
1254
+ p = p[mask]
1255
+ return t, p
1256
+
1257
+ def inline_filter(stream, resolution: TSRES = TSRES.NS1):
1258
+ commenced_reading = False
1259
+ for t, p in stream:
1260
+ _t, _p = event_filter(t, p, resolution)
1261
+ if commenced_reading and len(_t) == 0:
1262
+ break
1263
+ elif len(_t) != 0:
1264
+ commenced_reading = True
1265
+ yield _t, _p
1266
+
1267
+ # Use legacy read-write mechanisms
1268
+ if args.inmemory:
1269
+ t, p = read(filepath, args.X, ignore_rollover=ignore_rollover)
1270
+ t, p = event_filter(t, p)
1271
+ if print_only:
1272
+ print_statistics(filepath, t, p)
1273
+ else:
1274
+ write(args.outfile, t, p, args.x)
1275
+
1276
+ # Check if printing stream first
1277
+ elif print_only:
1278
+ if has_filtering:
1279
+ stream, num_batches = sread(
1280
+ filepath, args.X, ignore_rollover=ignore_rollover
1281
+ ) # default 1ns floating-point resolution
1282
+ stream = inline_filter(stream)
1283
+ print_statistics_stream(
1284
+ filepath, stream, num_batches, display=(not args.quiet)
1285
+ )
1286
+ else:
1287
+ # Disable timestamp formatting to speed up reads
1288
+ stream, num_batches = sread(
1289
+ filepath, args.X, TSRES.PS4, False, ignore_rollover=ignore_rollover
1290
+ )
1291
+ print_statistics_stream(
1292
+ filepath,
1293
+ stream,
1294
+ num_batches,
1295
+ resolution=TSRES.PS4,
1296
+ display=(not args.quiet),
1297
+ )
1298
+
1299
+ # Write out
1300
+ else:
1301
+ stream, num_batches = sread(
1302
+ filepath, args.X, TSRES.PS4, False, ignore_rollover=ignore_rollover
1303
+ )
1304
+ stream = inline_filter(stream, TSRES.PS4)
1305
+ swrite(
1306
+ args.outfile,
1307
+ stream,
1308
+ num_batches,
1309
+ args.x,
1310
+ resolution=TSRES.PS4,
1311
+ display=(not args.quiet),
1312
+ )
1313
+
1314
+
1315
+ if __name__ == "__main__":
1316
+ main()