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,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)