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,699 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Takes in an epoch and parses it into timestamps.
|
|
3
|
+
|
|
4
|
+
Currently only supports T1 and T2 epoch unpacking.
|
|
5
|
+
|
|
6
|
+
References:
|
|
7
|
+
[1] File specification: https://github.com/s-fifteen-instruments/qcrypto/blob/Documentation/docs/source/file%20specification.rst
|
|
8
|
+
[2] Epoch header definitions: https://github.com/s-fifteen-instruments/QKDServer/blob/master/S15qkd/utils.py
|
|
9
|
+
|
|
10
|
+
Changelog:
|
|
11
|
+
2023-02-15 Justin: Init
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import datetime as dt
|
|
15
|
+
import pathlib
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from struct import pack, unpack
|
|
18
|
+
from typing import BinaryIO, List, NamedTuple, Optional, Tuple, Union
|
|
19
|
+
|
|
20
|
+
import numpy as np
|
|
21
|
+
from numpy.typing import NDArray
|
|
22
|
+
|
|
23
|
+
import fpfind.lib._logging as logging
|
|
24
|
+
from fpfind import NP_PRECISEFLOAT, TSRES
|
|
25
|
+
from fpfind.lib.typing import DetectorArray, Integer, PathLike, TimestampArray
|
|
26
|
+
|
|
27
|
+
logger, log = logging.get_logger(__name__)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class HeadT1(NamedTuple):
|
|
31
|
+
tag: int
|
|
32
|
+
epoch: int
|
|
33
|
+
length_bits: int
|
|
34
|
+
bits_per_entry: int
|
|
35
|
+
base_bits: int
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class HeadT2(NamedTuple):
|
|
39
|
+
tag: int
|
|
40
|
+
epoch: int
|
|
41
|
+
length_bits: int
|
|
42
|
+
timeorder: int
|
|
43
|
+
base_bits: int
|
|
44
|
+
protocol: int
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class HeadT3(NamedTuple):
|
|
48
|
+
tag: int
|
|
49
|
+
epoch: int
|
|
50
|
+
length_entry: int
|
|
51
|
+
bits_per_entry: int
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class HeadT4(NamedTuple):
|
|
55
|
+
tag: int
|
|
56
|
+
epoch: int
|
|
57
|
+
length: int
|
|
58
|
+
timeorder: int
|
|
59
|
+
base_bits: int
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# Header readers
|
|
63
|
+
# Extracted from ref [2]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def read_T1_header(file_name: PathLike) -> HeadT1:
|
|
67
|
+
file_name = str(file_name)
|
|
68
|
+
with open(file_name, "rb") as f:
|
|
69
|
+
head_info = f.read(4 * 5)
|
|
70
|
+
headt1 = HeadT1._make(unpack("iIIii", head_info))
|
|
71
|
+
if headt1.tag not in (0x101, 1):
|
|
72
|
+
logger.warning(f"{file_name} is not a Type2 header file")
|
|
73
|
+
if hex(headt1.epoch) != ("0x" + file_name.split("/")[-1]):
|
|
74
|
+
logger.warning(
|
|
75
|
+
f"Epoch in header {headt1.epoch} does not match epoc filename {file_name}"
|
|
76
|
+
)
|
|
77
|
+
return headt1
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def read_T2_header(file_name: PathLike) -> HeadT2:
|
|
81
|
+
file_name = str(file_name)
|
|
82
|
+
with open(file_name, "rb") as f:
|
|
83
|
+
head_info = f.read(4 * 6)
|
|
84
|
+
headt2 = HeadT2._make(unpack("iIIiii", head_info))
|
|
85
|
+
if headt2.tag not in (0x102, 2):
|
|
86
|
+
logger.warning(f"{file_name} is not a Type2 header file")
|
|
87
|
+
if hex(headt2.epoch) != ("0x" + file_name.split("/")[-1]):
|
|
88
|
+
logger.warning(
|
|
89
|
+
f"Epoch in header {headt2.epoch} does not match epoc filename {file_name}"
|
|
90
|
+
)
|
|
91
|
+
return headt2
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def read_T3_header(file_name: PathLike) -> HeadT3:
|
|
95
|
+
file_name = str(file_name)
|
|
96
|
+
with open(file_name, "rb") as f:
|
|
97
|
+
head_info = f.read(4 * 4)
|
|
98
|
+
headt3 = HeadT3._make(unpack("iIIi", head_info))
|
|
99
|
+
if headt3.tag not in (0x103, 3):
|
|
100
|
+
logger.warning(f"{file_name} is not a Type3 header file")
|
|
101
|
+
if hex(headt3.epoch) != ("0x" + file_name.split("/")[-1]):
|
|
102
|
+
logger.warning(
|
|
103
|
+
f"Epoch in header {headt3.epoch} does not match epoc filename {file_name}"
|
|
104
|
+
)
|
|
105
|
+
return headt3
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def read_T4_header(file_name: PathLike) -> HeadT4:
|
|
109
|
+
file_name = str(file_name)
|
|
110
|
+
with open(file_name, "rb") as f:
|
|
111
|
+
head_info = f.read(4 * 5)
|
|
112
|
+
headt4 = HeadT4._make(unpack("IIIII", head_info))
|
|
113
|
+
if headt4.tag not in (0x104, 4):
|
|
114
|
+
logger.warning(f"{file_name} is not a Type4 header file")
|
|
115
|
+
if hex(headt4.epoch) != ("0x" + file_name.split("/")[-1]):
|
|
116
|
+
logger.warning(
|
|
117
|
+
f"Epoch in header {headt4.epoch} does not match epoc filename {file_name}"
|
|
118
|
+
)
|
|
119
|
+
return headt4
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# Epoch utility functions
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def date2epoch(datetime: Optional[dt.datetime] = None) -> str:
|
|
126
|
+
"""Returns current epoch number as hex.
|
|
127
|
+
|
|
128
|
+
Epochs are in units of 2^32 * 125e-12 == 0.53687 seconds.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
datetime: Optional datetime to convert to epoch.
|
|
132
|
+
"""
|
|
133
|
+
if datetime is None:
|
|
134
|
+
datetime = dt.datetime.now()
|
|
135
|
+
|
|
136
|
+
total_seconds = int(datetime.timestamp())
|
|
137
|
+
epoch_val = int(total_seconds / 125e-12) >> 32
|
|
138
|
+
return f"{epoch_val:08x}"
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def epoch2date(epoch: Union[str, bytes]) -> dt.datetime:
|
|
142
|
+
"""Returns datetime object corresponding to epoch.
|
|
143
|
+
|
|
144
|
+
Epoch must be in hex format, e.g. "ba1f36c0".
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
epoch: Epoch in hex format, e.g. "ba1f36c0".
|
|
148
|
+
"""
|
|
149
|
+
total_seconds = (int(epoch, base=16) << 32) * 125e-12
|
|
150
|
+
return dt.datetime.fromtimestamp(total_seconds)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def epoch2int(epoch: Union[str, bytes]) -> int:
|
|
154
|
+
return int(epoch, base=16)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def int2epoch(value: int) -> str:
|
|
158
|
+
return hex(value)[2:]
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
# Epoch readers
|
|
162
|
+
# Implemented as per filespec [1]
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def read_T1(
|
|
166
|
+
filename: PathLike,
|
|
167
|
+
full_epoch: bool = False,
|
|
168
|
+
resolution: TSRES = TSRES.NS1,
|
|
169
|
+
fractional: bool = True,
|
|
170
|
+
):
|
|
171
|
+
"""Returns timestamp and detector information from T1 files.
|
|
172
|
+
|
|
173
|
+
Timestamp and detector information follow the formats specified in
|
|
174
|
+
'parse_timestamps' module, for interoperability.
|
|
175
|
+
|
|
176
|
+
By default, the function returns timestamps in units of 1ns with as
|
|
177
|
+
high a resolution as possible (i.e. including fractional values).
|
|
178
|
+
|
|
179
|
+
TODO
|
|
180
|
+
|
|
181
|
+
'full' is False by default, which indicates only the raw-timestamp
|
|
182
|
+
-equivalent information is returned, i.e. timestamp in units of 1ns
|
|
183
|
+
stored as a 64-bit integer.
|
|
184
|
+
'full' set to True returns full timestamps (timestamps + full epoch
|
|
185
|
+
information) stored as 128-bit float.
|
|
186
|
+
The reason for this difference is due to the full timestamp taking up
|
|
187
|
+
more than 64-bits equivalent information in units of 1ns, so only a
|
|
188
|
+
float can support this dynamic range.
|
|
189
|
+
|
|
190
|
+
Note 128-bit float has precision of 113 bits, while 64-bit float only has
|
|
191
|
+
53-bit precision (insufficient resolution).
|
|
192
|
+
|
|
193
|
+
If 'raw' is True, timestamps are stored in units of 4ps instead of 1ns.
|
|
194
|
+
|
|
195
|
+
Raises:
|
|
196
|
+
ValueError: Number of epochs does not match length in header.
|
|
197
|
+
"""
|
|
198
|
+
# Header details
|
|
199
|
+
header = read_T1_header(filename)
|
|
200
|
+
length = header.length_bits
|
|
201
|
+
|
|
202
|
+
timestamps = []
|
|
203
|
+
bases = []
|
|
204
|
+
|
|
205
|
+
# For each timestamp event, 54-bits correspond to timestamp,
|
|
206
|
+
# of which 17-bit MSB is LSB of epoch, 37-bit LSB is 125ps resolution timestamp
|
|
207
|
+
with open(filename, "rb") as f:
|
|
208
|
+
f.read(5 * 4) # dump header
|
|
209
|
+
while True:
|
|
210
|
+
event_high = f.read(4)
|
|
211
|
+
event_low = f.read(4)
|
|
212
|
+
|
|
213
|
+
value = (
|
|
214
|
+
int.from_bytes(event_high, byteorder="little") << 32
|
|
215
|
+
) + int.from_bytes(event_low, byteorder="little")
|
|
216
|
+
|
|
217
|
+
if value == 0:
|
|
218
|
+
break # termination
|
|
219
|
+
|
|
220
|
+
# Timestamp in units of 4ps, stored in 54-bit MSB
|
|
221
|
+
timestamp = value >> 10
|
|
222
|
+
timestamps.append(timestamp)
|
|
223
|
+
|
|
224
|
+
base = value & 0b1111
|
|
225
|
+
bases.append(base)
|
|
226
|
+
|
|
227
|
+
# Validity checks
|
|
228
|
+
if length and len(timestamps) != length:
|
|
229
|
+
raise ValueError("Number of epochs do not match length specified in header")
|
|
230
|
+
|
|
231
|
+
# Add epoch if required, at most 32-17 = 15 bits
|
|
232
|
+
epoch_msb = header.epoch >> 17
|
|
233
|
+
|
|
234
|
+
# Right now in units of 4ps
|
|
235
|
+
# Check if need to store floating point
|
|
236
|
+
if fractional:
|
|
237
|
+
# Convert to desired resolution
|
|
238
|
+
timestamps = np.array(timestamps, dtype=NP_PRECISEFLOAT)
|
|
239
|
+
timestamps = timestamps / (TSRES.PS4.value / resolution.value)
|
|
240
|
+
epoch_msb = NP_PRECISEFLOAT(epoch_msb << 54)
|
|
241
|
+
epoch_msb = epoch_msb / (TSRES.PS4.value / resolution.value)
|
|
242
|
+
|
|
243
|
+
# Python integer objects can be arbitrarily long
|
|
244
|
+
elif resolution in (TSRES.PS4,) and full_epoch:
|
|
245
|
+
timestamps = np.array(timestamps, dtype=object)
|
|
246
|
+
epoch_msb = int(epoch_msb) << 54
|
|
247
|
+
|
|
248
|
+
# Everything of resolution 125 ps and larger can fit
|
|
249
|
+
# in 64-bit unsigned integer, i.e. PS125, NS1.
|
|
250
|
+
else:
|
|
251
|
+
bitdiff = round(np.log2(TSRES.PS4.value / resolution.value))
|
|
252
|
+
timestamps = np.array(timestamps, dtype=np.uint64)
|
|
253
|
+
timestamps = timestamps >> bitdiff
|
|
254
|
+
epoch_msb = np.uint64(epoch_msb) << np.uint64(54 - bitdiff)
|
|
255
|
+
|
|
256
|
+
# Add epoch
|
|
257
|
+
if full_epoch:
|
|
258
|
+
timestamps += epoch_msb
|
|
259
|
+
|
|
260
|
+
return timestamps, np.array(bases)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def extract_bits(
|
|
264
|
+
msb_size: int, buffer: int, size: int, fileobject: Optional[BinaryIO] = None
|
|
265
|
+
) -> Tuple[int, int, int]:
|
|
266
|
+
"""Return 'msb_size'-bits from MSB of buffer, and the rest.
|
|
267
|
+
|
|
268
|
+
If insufficient bits to load value, data is automatically loaded
|
|
269
|
+
from file 'fileobject' provided (must be already open in "rb" mode).
|
|
270
|
+
|
|
271
|
+
Raises:
|
|
272
|
+
ValueError: msb_size > size but fileobject not provided.
|
|
273
|
+
|
|
274
|
+
Example:
|
|
275
|
+
|
|
276
|
+
>>> msb_size = 4
|
|
277
|
+
>>> buffer = 0b0101110; size = 7
|
|
278
|
+
>>> extract_bits(msb_size, buffer, size)
|
|
279
|
+
(5, 6, 3)
|
|
280
|
+
|
|
281
|
+
Explanation:
|
|
282
|
+
- msb => 0b0101 = 4
|
|
283
|
+
- buffer => 0b110 = 5
|
|
284
|
+
|
|
285
|
+
Note:
|
|
286
|
+
Integrating with 'append_word_from_file' since this
|
|
287
|
+
operation always precedes the value retrieval. This function
|
|
288
|
+
can be reused in bit-packing schemes of other epoch types.
|
|
289
|
+
"""
|
|
290
|
+
# Append word from fileobject if insufficient bits
|
|
291
|
+
if size < msb_size and fileobject:
|
|
292
|
+
buffer <<= 32
|
|
293
|
+
buffer += int.from_bytes(fileobject.read(4), byteorder="little")
|
|
294
|
+
# buffer += fileobject.get_next_int()
|
|
295
|
+
size += 32
|
|
296
|
+
|
|
297
|
+
# Extract MSB from buffer
|
|
298
|
+
msb = buffer >> (size - msb_size)
|
|
299
|
+
buffer &= (1 << (size - msb_size)) - 1
|
|
300
|
+
size -= msb_size
|
|
301
|
+
# logger.debug("Extracted %d bytes: %d", msb_size, msb)
|
|
302
|
+
return msb, buffer, size
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
class BufferedFileRead:
|
|
306
|
+
"""Stores internal buffer to minimize overall disk reads.
|
|
307
|
+
|
|
308
|
+
Deprecated.
|
|
309
|
+
|
|
310
|
+
Note:
|
|
311
|
+
Written to stand in for the 'fileobject' object in 'extract_bits', so
|
|
312
|
+
that additional block-based buffering can be performed in-situ.
|
|
313
|
+
"""
|
|
314
|
+
|
|
315
|
+
def __init__(self, f: BinaryIO, buffer_size: int = 100_000):
|
|
316
|
+
self.f = f
|
|
317
|
+
self.buffer_size = buffer_size
|
|
318
|
+
|
|
319
|
+
# Track intermediate buffer
|
|
320
|
+
self.buffer = b""
|
|
321
|
+
self.pos = 0
|
|
322
|
+
|
|
323
|
+
def read(self, num_bytes: int) -> Optional[bytes]:
|
|
324
|
+
# Read desired amount of bytes
|
|
325
|
+
result = self.buffer[self.pos : self.pos + num_bytes]
|
|
326
|
+
self.pos += num_bytes
|
|
327
|
+
if len(result) == num_bytes:
|
|
328
|
+
return result
|
|
329
|
+
|
|
330
|
+
# Insufficient data, load more data into buffer read from file if available
|
|
331
|
+
self.buffer = self.f.read(self.buffer_size * 8)
|
|
332
|
+
if len(self.buffer) == 0:
|
|
333
|
+
return None
|
|
334
|
+
|
|
335
|
+
# Note shortfall must be less than number of bytes loaded into buffer
|
|
336
|
+
# TODO: Allow for more data than in buffer read size, i.e. while loop check.
|
|
337
|
+
shortfall = num_bytes - len(result)
|
|
338
|
+
result += self.buffer[:shortfall]
|
|
339
|
+
self.pos = shortfall
|
|
340
|
+
return result
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
class BufferedFileRead2:
|
|
344
|
+
"""Stores internal buffer to minimize overall disk reads.
|
|
345
|
+
|
|
346
|
+
Deprecated.
|
|
347
|
+
|
|
348
|
+
Here we use 'np.frombuffer' to bulk process bytestring reads into integers,
|
|
349
|
+
then use a pointer to track the next integer to read. This is more similar
|
|
350
|
+
to that in 'costream::get_stream_2'.
|
|
351
|
+
|
|
352
|
+
Note:
|
|
353
|
+
Written to stand in for the 'fileobject' object in 'extract_bits', so
|
|
354
|
+
that additional block-based buffering can be performed in-situ.
|
|
355
|
+
|
|
356
|
+
Usage:
|
|
357
|
+
>>> f = BufferedFileRead(f)
|
|
358
|
+
>>> f.get_next_int() # replaces 'int.from_bytes(f.read(4), "little")'
|
|
359
|
+
"""
|
|
360
|
+
|
|
361
|
+
def __init__(self, f, buffer_size: int = 100_000):
|
|
362
|
+
self.f = f
|
|
363
|
+
self.buffer_size = buffer_size
|
|
364
|
+
|
|
365
|
+
# Track intermediate buffer
|
|
366
|
+
self.buffer = []
|
|
367
|
+
self.pos = 0
|
|
368
|
+
|
|
369
|
+
def read(self, num_bytes) -> bytes:
|
|
370
|
+
"""Reads fixed number of bytes.
|
|
371
|
+
|
|
372
|
+
Note:
|
|
373
|
+
Do not use this function except for compatibility reasons - this
|
|
374
|
+
performs a double conversion from integer then back to bytestring.
|
|
375
|
+
This is done to align with the new internal buffering method, where
|
|
376
|
+
bulk processing of 32-bit integers is performed by 'np.frombuffer'.
|
|
377
|
+
|
|
378
|
+
This new method the costly call of 'int.from_bytes' on individual
|
|
379
|
+
32-bits constructs. For an epoch file of 800k events, this improves
|
|
380
|
+
the readtime from 3.7s to.
|
|
381
|
+
|
|
382
|
+
Update: Fake news! Buffering doesn't really help much due to the
|
|
383
|
+
additional overhead.
|
|
384
|
+
"""
|
|
385
|
+
assert num_bytes % 4 == 0, "must be read in units of 32-bits"
|
|
386
|
+
result = []
|
|
387
|
+
for i in range(num_bytes // 4):
|
|
388
|
+
result.append(self.get_next_int())
|
|
389
|
+
result = b"".join(map(lambda v: v.to_bytes(4, "little"), [304, 23, 12, 2]))
|
|
390
|
+
return result
|
|
391
|
+
|
|
392
|
+
def get_next_int(self):
|
|
393
|
+
# Insufficient data, load more data into buffer read from file if available
|
|
394
|
+
if self.pos == len(self.buffer):
|
|
395
|
+
self.buffer = np.frombuffer(self.f.read(self.buffer_size * 8), dtype="=I")
|
|
396
|
+
self.pos = 0
|
|
397
|
+
if len(self.buffer) == 0:
|
|
398
|
+
return None
|
|
399
|
+
|
|
400
|
+
result = self.buffer[self.pos]
|
|
401
|
+
self.pos += 1
|
|
402
|
+
return result
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def read_T2(
|
|
406
|
+
filename: PathLike,
|
|
407
|
+
full_epoch: bool = False,
|
|
408
|
+
resolution: TSRES = TSRES.NS1,
|
|
409
|
+
fractional: bool = True,
|
|
410
|
+
) -> Tuple[TimestampArray, DetectorArray]:
|
|
411
|
+
"""Returns timestamp and detector information from T2 files.
|
|
412
|
+
|
|
413
|
+
Timestamp and detector information follow the formats specified in
|
|
414
|
+
'parse_timestamps' module, for interoperability.
|
|
415
|
+
|
|
416
|
+
Raises:
|
|
417
|
+
ValueError: Length and termination bits inconsistent with filespec.
|
|
418
|
+
"""
|
|
419
|
+
if resolution not in TSRES:
|
|
420
|
+
raise ValueError("Timestamp resolution must be one of enumeration TSRES")
|
|
421
|
+
|
|
422
|
+
# Header details
|
|
423
|
+
header = read_T2_header(filename)
|
|
424
|
+
timeorder = header.timeorder
|
|
425
|
+
timeorder_extended = 32 # just being pedantic
|
|
426
|
+
basebits = header.base_bits
|
|
427
|
+
length = header.length_bits
|
|
428
|
+
|
|
429
|
+
# Accumulators
|
|
430
|
+
timestamps = [] # all timestamps, units of 125ps
|
|
431
|
+
bases = []
|
|
432
|
+
|
|
433
|
+
# Add 17-bit epoch LSB as per regular timestamp spec,
|
|
434
|
+
# i.e. (17-bit epoch LSB)(32-bit timestamp) => 49-bits
|
|
435
|
+
epoch_lsb = header.epoch & ((1 << 17) - 1)
|
|
436
|
+
timestamp = np.uint64(epoch_lsb << 32) # current timestamp, deltas stored in T2
|
|
437
|
+
buffer = 0
|
|
438
|
+
size = 0 # number of bits in buffer
|
|
439
|
+
with open(filename, "rb") as f:
|
|
440
|
+
# f = BufferedFileRead(f)
|
|
441
|
+
f.read(24) # remove header
|
|
442
|
+
while True:
|
|
443
|
+
# Read timing information
|
|
444
|
+
timing, buffer, size = extract_bits(timeorder, buffer, size, f)
|
|
445
|
+
|
|
446
|
+
# End-of-file check
|
|
447
|
+
if timing == 1:
|
|
448
|
+
base, buffer, size = extract_bits(basebits, buffer, size, f)
|
|
449
|
+
if base != 0:
|
|
450
|
+
raise ValueError(
|
|
451
|
+
"File inconsistent with T2 epoch definition: "
|
|
452
|
+
"Terminal base bits not zero"
|
|
453
|
+
)
|
|
454
|
+
break
|
|
455
|
+
|
|
456
|
+
# Check if timing is actually in extended format, i.e. 32-bits
|
|
457
|
+
if timing == 0:
|
|
458
|
+
timing, buffer, size = extract_bits(timeorder_extended, buffer, size, f)
|
|
459
|
+
|
|
460
|
+
timestamp += timing
|
|
461
|
+
timestamps.append(timestamp)
|
|
462
|
+
|
|
463
|
+
# Extract detector pattern, but not important here
|
|
464
|
+
# Note we are guaranteed (timeorder+basebits < 32)
|
|
465
|
+
base, buffer, size = extract_bits(basebits, buffer, size, f)
|
|
466
|
+
bases.append(base)
|
|
467
|
+
|
|
468
|
+
# Validity checks
|
|
469
|
+
if length and len(timestamps) != length:
|
|
470
|
+
raise ValueError("Number of epochs do not match length specified in header")
|
|
471
|
+
|
|
472
|
+
# Add full epoch if required
|
|
473
|
+
epoch_msb = header.epoch >> 17
|
|
474
|
+
|
|
475
|
+
# Right now in units of 125ps
|
|
476
|
+
# Check if need to store floating point
|
|
477
|
+
if fractional:
|
|
478
|
+
# Convert to desired resolution
|
|
479
|
+
timestamps = np.array(timestamps, dtype=NP_PRECISEFLOAT)
|
|
480
|
+
timestamps = timestamps / (TSRES.PS125.value / resolution.value)
|
|
481
|
+
epoch_msb = NP_PRECISEFLOAT(epoch_msb << 49)
|
|
482
|
+
epoch_msb = epoch_msb / (TSRES.PS125.value / resolution.value)
|
|
483
|
+
|
|
484
|
+
# Python integer objects can be arbitrarily long
|
|
485
|
+
# for resolutions smaller than 125ps, i.e. larger value
|
|
486
|
+
elif resolution.value > TSRES.PS125.value:
|
|
487
|
+
bitdiff = round(np.log2(resolution.value / TSRES.PS125.value))
|
|
488
|
+
timestamps = np.array(timestamps, dtype=object)
|
|
489
|
+
timestamps = timestamps << bitdiff
|
|
490
|
+
epoch_msb = int(epoch_msb) << (49 + bitdiff)
|
|
491
|
+
|
|
492
|
+
# Everything of resolution 125 ps and larger can fit
|
|
493
|
+
# in 64-bit unsigned integer, i.e. PS125, NS1.
|
|
494
|
+
else:
|
|
495
|
+
bitdiff = round(np.log2(TSRES.PS125.value / resolution.value))
|
|
496
|
+
timestamps = np.array(timestamps, dtype=np.uint64)
|
|
497
|
+
timestamps = timestamps >> bitdiff
|
|
498
|
+
epoch_msb = np.uint64(epoch_msb) << np.uint64(49 - bitdiff)
|
|
499
|
+
|
|
500
|
+
# Add epoch
|
|
501
|
+
if full_epoch:
|
|
502
|
+
timestamps += epoch_msb
|
|
503
|
+
|
|
504
|
+
return timestamps, np.array(bases)
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
def write_T1(
|
|
508
|
+
directory: PathLike,
|
|
509
|
+
full_epoch: Integer,
|
|
510
|
+
timestamps: TimestampArray,
|
|
511
|
+
detectors: Union[DetectorArray, Integer],
|
|
512
|
+
) -> pathlib.Path:
|
|
513
|
+
# Fit epoch to within 32-bits LSB
|
|
514
|
+
full_epoch &= (1 << 32) - 1
|
|
515
|
+
|
|
516
|
+
directory = pathlib.Path(directory)
|
|
517
|
+
target = directory / f"{full_epoch:x}"
|
|
518
|
+
|
|
519
|
+
# Broadcast detectors if single value given
|
|
520
|
+
_detectors: DetectorArray
|
|
521
|
+
if np.array(detectors).ndim == 0:
|
|
522
|
+
_detectors = np.ones(len(timestamps), dtype=np.uint32) * np.uint32(detectors)
|
|
523
|
+
elif len(detectors) != len(timestamps): # type: ignore (integer check performed)
|
|
524
|
+
raise ValueError("Lengths of timestamps and detectors must match")
|
|
525
|
+
else:
|
|
526
|
+
_detectors = detectors # type: ignore (array check performed)
|
|
527
|
+
|
|
528
|
+
with open(target, "wb") as f:
|
|
529
|
+
header = pack("iIIii", 0x101, full_epoch, len(timestamps), 49, 4)
|
|
530
|
+
f.write(header)
|
|
531
|
+
|
|
532
|
+
# Write events, 4ps resolution
|
|
533
|
+
# 17-bit LSB epoch fits into a timestamp event
|
|
534
|
+
timestamp_epoch = (full_epoch & ((1 << 17) - 1)) << (32 + 5)
|
|
535
|
+
for timestamp, detector in zip(timestamps, _detectors):
|
|
536
|
+
full_timestamp = timestamp_epoch + timestamp
|
|
537
|
+
event = (full_timestamp << 10) + int(detector)
|
|
538
|
+
|
|
539
|
+
# Write high word, then low word
|
|
540
|
+
f.write(pack("<II", event >> 32, event & 0xFFFF_FFFF))
|
|
541
|
+
|
|
542
|
+
# Termination
|
|
543
|
+
f.write(pack("II", 0, 0))
|
|
544
|
+
|
|
545
|
+
return target
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
@dataclass
|
|
549
|
+
class ServiceT3:
|
|
550
|
+
"""Dataclass for T3 file formats.
|
|
551
|
+
|
|
552
|
+
Coincidence matrix mapped directly as 2D array:
|
|
553
|
+
|
|
554
|
+
[
|
|
555
|
+
(VV), VA , (VH), VD ,
|
|
556
|
+
AV , (AA), AH , (AD),
|
|
557
|
+
(HV), HA , (HH), HD ,
|
|
558
|
+
DV , (DA), DH , (DD),
|
|
559
|
+
]
|
|
560
|
+
"""
|
|
561
|
+
|
|
562
|
+
head: HeadT3
|
|
563
|
+
coinc_matrix: list = field(default_factory=lambda: [0] * 16)
|
|
564
|
+
garbage: list = field(default_factory=lambda: [0, 0])
|
|
565
|
+
okcount: int = 0
|
|
566
|
+
qber: float = 0.5 # default random
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
def read_T3(file_name: PathLike) -> ServiceT3:
|
|
570
|
+
"""Used to process rawkey files.
|
|
571
|
+
|
|
572
|
+
Example data:
|
|
573
|
+
01000001_00100101_00100010_00010100
|
|
574
|
+
\\ \\
|
|
575
|
+
alice bob
|
|
576
|
+
0100 0001
|
|
577
|
+
|
|
578
|
+
Note:
|
|
579
|
+
Unpacking was done wrongly in original diagnosis.c code.
|
|
580
|
+
|
|
581
|
+
Functionally the same as 'service_T3' in QKDServer.utils, with
|
|
582
|
+
some syntatic comments.
|
|
583
|
+
"""
|
|
584
|
+
# Map single-bit detections to index in 4-array for coinc matrix,
|
|
585
|
+
# with multi-bit detections set to -1
|
|
586
|
+
decode = [-1, 0, 1, -1, 2, -1, -1, -1, 3, -1, -1, -1, -1, -1, -1, -1]
|
|
587
|
+
er_coinc_id = [0, 5, 10, 15] # VV, AA, HH, DD
|
|
588
|
+
gd_coinc_id = [2, 7, 8, 13] # VH, AD, HV, DA
|
|
589
|
+
detections = [] # store detection events of pairs
|
|
590
|
+
headt3 = read_T3_header(file_name)
|
|
591
|
+
service = ServiceT3(headt3)
|
|
592
|
+
|
|
593
|
+
# Check if number of bits per entry according to filespec
|
|
594
|
+
if headt3.bits_per_entry != 8:
|
|
595
|
+
logger.warning("Not a service file with 8 bits per entry")
|
|
596
|
+
|
|
597
|
+
# Digest file
|
|
598
|
+
HEADER_INFO_SIZE = 16
|
|
599
|
+
data_b = b"\x00\x00\x00\x00"
|
|
600
|
+
with open(file_name, "rb") as f:
|
|
601
|
+
f.seek(HEADER_INFO_SIZE)
|
|
602
|
+
while True:
|
|
603
|
+
word = f.read(4)
|
|
604
|
+
if word == b"":
|
|
605
|
+
break
|
|
606
|
+
|
|
607
|
+
# Since packing in bytes, byteorder is mostly irrelevant
|
|
608
|
+
# except when verifying zero padding at end of file
|
|
609
|
+
(data,) = unpack("<I", word) # comma for unpacking tuple result
|
|
610
|
+
data_b = data.to_bytes(4, byteorder="big")
|
|
611
|
+
detections.extend(data_b)
|
|
612
|
+
|
|
613
|
+
# Verify detections match length entry in header, ignoring \x00 byte padding
|
|
614
|
+
if headt3.length_entry != len(detections) - data_b.count(0):
|
|
615
|
+
logger.error("Stream 3 size inconsistency: length of entry does not match.")
|
|
616
|
+
|
|
617
|
+
# Assign to coincidence matrix
|
|
618
|
+
for i in range(headt3.length_entry):
|
|
619
|
+
detection = detections[i]
|
|
620
|
+
a = decode[(detection >> 4) & 0xF] # Alice
|
|
621
|
+
b = decode[detection & 0xF] # Bob
|
|
622
|
+
if a >= 0 and b >= 0:
|
|
623
|
+
service.coinc_matrix[a * 4 + b] += 1
|
|
624
|
+
service.okcount += 1
|
|
625
|
+
else:
|
|
626
|
+
if a < 0:
|
|
627
|
+
service.garbage[0] += 1
|
|
628
|
+
if b < 0:
|
|
629
|
+
service.garbage[1] += 1
|
|
630
|
+
|
|
631
|
+
# Calculate QBER
|
|
632
|
+
er_coin = sum(service.coinc_matrix[i] for i in er_coinc_id)
|
|
633
|
+
gd_coin = sum(service.coinc_matrix[i] for i in gd_coinc_id)
|
|
634
|
+
if er_coin + gd_coin != 0:
|
|
635
|
+
service.qber = float(round(er_coin / (er_coin + gd_coin), 3)) # ignore garbage
|
|
636
|
+
|
|
637
|
+
return service
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
def read_T4(filename: PathLike) -> Tuple[NDArray[np.int64], NDArray[np.int64]]:
|
|
641
|
+
"""Returns timestamp indices and detector information from T4 files.
|
|
642
|
+
|
|
643
|
+
Timestamp and detector information follow the formats specified in
|
|
644
|
+
'parse_timestamps' module, for interoperability.
|
|
645
|
+
|
|
646
|
+
Raises:
|
|
647
|
+
ValueError: Length and termination bits inconsistent with filespec.
|
|
648
|
+
"""
|
|
649
|
+
|
|
650
|
+
# Header details
|
|
651
|
+
header = read_T4_header(filename)
|
|
652
|
+
timeorder = header.timeorder
|
|
653
|
+
timeorder_extended = 32
|
|
654
|
+
basebits = header.base_bits
|
|
655
|
+
length = header.length
|
|
656
|
+
|
|
657
|
+
# Accumulators
|
|
658
|
+
indices: List[int] = []
|
|
659
|
+
bases: List[int] = []
|
|
660
|
+
time_index = 0
|
|
661
|
+
buffer = 0
|
|
662
|
+
size = 0 # number of bits in buffer
|
|
663
|
+
with open(filename, "rb") as f:
|
|
664
|
+
# f = BufferedFileRead(f)
|
|
665
|
+
f.read(20) # remove header
|
|
666
|
+
while True:
|
|
667
|
+
# Read timing information
|
|
668
|
+
time_dindex, buffer, size = extract_bits(timeorder, buffer, size, f)
|
|
669
|
+
|
|
670
|
+
# End-of-file check
|
|
671
|
+
if time_dindex == 1:
|
|
672
|
+
base, buffer, size = extract_bits(basebits, buffer, size, f)
|
|
673
|
+
if base != 0:
|
|
674
|
+
raise ValueError(
|
|
675
|
+
"File inconsistent with T4 epoch definition: "
|
|
676
|
+
"Terminal base bits not zero"
|
|
677
|
+
)
|
|
678
|
+
break
|
|
679
|
+
|
|
680
|
+
# Check if timing is actually in extended format, i.e. 32-bits
|
|
681
|
+
if time_dindex == 0:
|
|
682
|
+
time_dindex, buffer, size = extract_bits(
|
|
683
|
+
timeorder_extended, buffer, size, f
|
|
684
|
+
)
|
|
685
|
+
|
|
686
|
+
# Index retrieved, normalize
|
|
687
|
+
time_index += time_dindex - 2
|
|
688
|
+
indices.append(time_index)
|
|
689
|
+
|
|
690
|
+
# Extract detector pattern, but not important here
|
|
691
|
+
# Note we are guaranteed (timeorder+basebits < 32)
|
|
692
|
+
base, buffer, size = extract_bits(basebits, buffer, size, f)
|
|
693
|
+
bases.append(base)
|
|
694
|
+
|
|
695
|
+
# Validity checks
|
|
696
|
+
if length and len(indices) != length:
|
|
697
|
+
raise ValueError("Number of epochs do not match length specified in header")
|
|
698
|
+
|
|
699
|
+
return np.array(indices, dtype=np.int64), np.array(bases, dtype=np.int64)
|