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.
- fpfind/__init__.py +14 -0
- fpfind/_postinstall +5 -0
- fpfind/apps/fpplot.py +286 -0
- fpfind/apps/g2_two_timestamps.py +38 -0
- fpfind/apps/generate_freqcd_testcase.py +150 -0
- fpfind/apps/generate_freqdetuneg2.py +50 -0
- fpfind/apps/show-timestamps +16 -0
- fpfind/apps/show-timestamps-hex +16 -0
- fpfind/apps/tsviz.py +50 -0
- fpfind/fpfind.py +1008 -0
- fpfind/freqcd +81 -0
- fpfind/freqcd.c +549 -0
- fpfind/freqservo.py +333 -0
- fpfind/lib/_logging.py +177 -0
- fpfind/lib/constants.py +98 -0
- fpfind/lib/getopt.c +106 -0
- fpfind/lib/getopt.h +12 -0
- fpfind/lib/parse_epochs.py +699 -0
- fpfind/lib/parse_timestamps.py +1316 -0
- fpfind/lib/typing.py +27 -0
- fpfind/lib/utils.py +716 -0
- fpfind-3.3.0.data/scripts/freqcd +81 -0
- fpfind-3.3.0.data/scripts/show-timestamps +16 -0
- fpfind-3.3.0.data/scripts/show-timestamps-hex +16 -0
- fpfind-3.3.0.dist-info/METADATA +126 -0
- fpfind-3.3.0.dist-info/RECORD +29 -0
- fpfind-3.3.0.dist-info/WHEEL +4 -0
- fpfind-3.3.0.dist-info/entry_points.txt +7 -0
- fpfind-3.3.0.dist-info/licenses/LICENSE +339 -0
|
@@ -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()
|