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
fpfind/freqservo.py
ADDED
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Reads costream output and provides frequency compensation updates.
|
|
3
|
+
|
|
4
|
+
It reads the costream logging output (either written to '-n' argument, or to stdout otherwise),
|
|
5
|
+
and performs a linear estimate of the timing drift (frequency offset). These frequency offsets are compatible
|
|
6
|
+
with the formats read by 'freqcd'. See the documentation for 'freqservo:evaluate_freqshift' on how to
|
|
7
|
+
tune this estimation. Example scenario:
|
|
8
|
+
|
|
9
|
+
"freqcd -F freq.pipe -f freqdiff -o raws &
|
|
10
|
+
chopper2 -i raws -D t1dir -U &
|
|
11
|
+
costream -D t1dir -n log.pipe -V 5 &
|
|
12
|
+
freqservo -n log.pipe -V 5 -F freq.pipe -f freqdiff &".
|
|
13
|
+
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import logging
|
|
17
|
+
import sys
|
|
18
|
+
import time
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
import configargparse
|
|
22
|
+
import numpy as np
|
|
23
|
+
|
|
24
|
+
from fpfind.lib import parse_epochs as eparser
|
|
25
|
+
from fpfind.lib.constants import EPOCH_DURATION
|
|
26
|
+
from fpfind.lib.utils import ArgparseCustomFormatter, parse_docstring_description
|
|
27
|
+
|
|
28
|
+
_LOGGING_FMT = "{asctime}\t{levelname:<7s}\t{funcName}:{lineno}\t| {message}"
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
30
|
+
if not logger.handlers:
|
|
31
|
+
handler = logging.StreamHandler(stream=sys.stderr)
|
|
32
|
+
handler.setFormatter(
|
|
33
|
+
logging.Formatter(fmt=_LOGGING_FMT, datefmt="%Y%m%d_%H%M%S", style="{")
|
|
34
|
+
)
|
|
35
|
+
logger.addHandler(handler)
|
|
36
|
+
logger.propagate = False
|
|
37
|
+
|
|
38
|
+
# Conversion factors for frequency units, i.e. 2^-34 <-> 1e-10
|
|
39
|
+
FCORR_DTOB = (1 << 34) / 10_000_000_000
|
|
40
|
+
FCORR_BTOD = 1 / FCORR_DTOB
|
|
41
|
+
|
|
42
|
+
# Container to hold previously servoed time differences
|
|
43
|
+
DT_HISTORY = []
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def parse(line, format=None):
|
|
47
|
+
"""Reads costream stdout and returns epoch and servoed time.
|
|
48
|
+
|
|
49
|
+
The returned time is in units of ns. If 'format' is not supplied, 'line' is
|
|
50
|
+
expected to be of the form '<EPOCH:str> <DT:float>\n', rather than a costream
|
|
51
|
+
format.
|
|
52
|
+
|
|
53
|
+
Default format (space-delimited):
|
|
54
|
+
bead38d4 -77845
|
|
55
|
+
|
|
56
|
+
costream V=4 format (space-delimited):
|
|
57
|
+
epoch: bead378b, 2-evnts: 86180, 4-evnts: 2997, new bw4: 7, ft: 346, acc: 2876, true: 2997, 1-events: 86324
|
|
58
|
+
|
|
59
|
+
costream V=5 format (tab-delimited):
|
|
60
|
+
Epoch Raw Sent Cmpr Track Acc Coinc Singles (headers, not in output)
|
|
61
|
+
bead38d4 116271 4144 7 -77845 3897 4144 111068
|
|
62
|
+
"""
|
|
63
|
+
if format is None:
|
|
64
|
+
epoch, dt = line.strip().split(" ")
|
|
65
|
+
dt = float(dt)
|
|
66
|
+
elif format == 5:
|
|
67
|
+
tokens = line.split("\t")
|
|
68
|
+
epoch = tokens[0]
|
|
69
|
+
dt = int(tokens[4]) / 8 # convert 1/8ns -> 1ns
|
|
70
|
+
elif format == 4:
|
|
71
|
+
tokens = line.split(" ")
|
|
72
|
+
epoch = tokens[1][:-1]
|
|
73
|
+
dt = int(tokens[10][:-1]) / 8
|
|
74
|
+
else:
|
|
75
|
+
raise ValueError(f"Unrecognised costream format: '{format}'")
|
|
76
|
+
logger.debug("Successfully parsed epoch = %s, dt = %.3f ns", epoch, dt)
|
|
77
|
+
return epoch, dt
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def evaluate_freqshift(epoch, dt, ignore=5, average=3, separation=12, cap=100e-9):
|
|
81
|
+
"""Returns desired clock skew correction, in absolute units.
|
|
82
|
+
|
|
83
|
+
This function accumulates the dt values, and performs some degree of
|
|
84
|
+
averaging (low-pass) to smoothen the frequency correction adjustment.
|
|
85
|
+
|
|
86
|
+
Example:
|
|
87
|
+
Suppose the dt history contains the set of time differences (in ns),
|
|
88
|
+
with parameters (ignore=5, average=3, separation=12, cap=10e-9):
|
|
89
|
+
|
|
90
|
+
dt_history = (
|
|
91
|
+
{ 43.250, 99.250, 148.625, 176.500, 197.375,} # ignored
|
|
92
|
+
{220.250, 229.750, 247.750,} 255.125, 268.000, # initial sample
|
|
93
|
+
271.875, 277.625, 281.625, 278.125, 282.250,
|
|
94
|
+
292.625, 300.000, {304.000, 315.000, 314.125,} # final sample
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
The averaged servoed timediff in the initial and final samples are
|
|
98
|
+
232.58333 and 311.04166 ns, which in span of 12 epochs corresponds to
|
|
99
|
+
a frequency drift of 12.178 ppb. The compensation frequency is thus
|
|
100
|
+
-12.2 ppb, to the nearest ppb, and then capped to -10.0 ppb.
|
|
101
|
+
|
|
102
|
+
Note:
|
|
103
|
+
Assuming a bimodal clock skew, the algorithm used should not be a
|
|
104
|
+
sliding mean, since the coincidence matching requires an accurate
|
|
105
|
+
frequency compensation value. We instead collect a set of samples,
|
|
106
|
+
then calculate the frequency difference.
|
|
107
|
+
|
|
108
|
+
Race condition may be possible - no guarantees on the continuity
|
|
109
|
+
of epochs. This is resolved with an epoch continuity check at every
|
|
110
|
+
function call.
|
|
111
|
+
|
|
112
|
+
This function was ported over from QKDServer.S15qkd.readevents[1].
|
|
113
|
+
|
|
114
|
+
References:
|
|
115
|
+
[1]: <https://github.com/s-fifteen-instruments/QKDServer/blob/master/S15qkd/readevents.py>
|
|
116
|
+
"""
|
|
117
|
+
# Verify epochs are contiguous
|
|
118
|
+
# Needed because costream may terminate prematurely, and no calls to
|
|
119
|
+
# flush the dt history is made, resulting in large time gaps.
|
|
120
|
+
if len(DT_HISTORY) > 0:
|
|
121
|
+
# DT_HISTORY format: [(epoch:str, dt:float),...]
|
|
122
|
+
prev_epoch = DT_HISTORY[-1][0]
|
|
123
|
+
if eparser.epoch2int(prev_epoch) + 1 != eparser.epoch2int(epoch):
|
|
124
|
+
DT_HISTORY.clear()
|
|
125
|
+
|
|
126
|
+
# Collect epochs
|
|
127
|
+
DT_HISTORY.append((epoch, dt))
|
|
128
|
+
required = ignore + average + separation
|
|
129
|
+
if len(DT_HISTORY) < required:
|
|
130
|
+
return
|
|
131
|
+
|
|
132
|
+
# Ignore first few epochs to allow freqcorr to propagate
|
|
133
|
+
history = DT_HISTORY[ignore:required] # make a copy
|
|
134
|
+
epochs, dts = list(zip(*history))
|
|
135
|
+
logger.debug("Active frequency correction triggered, measurements: %s", dts)
|
|
136
|
+
|
|
137
|
+
# Calculate averaged frequency difference
|
|
138
|
+
dt_early = np.mean(dts[:average])
|
|
139
|
+
dt_late = np.mean(dts[-average:])
|
|
140
|
+
dt_change = (dt_late - dt_early) * 1e-9 # convert to seconds
|
|
141
|
+
df = dt_change / (separation * EPOCH_DURATION)
|
|
142
|
+
df_toapply = 1 / (1 + df) - 1 # note: equal to -df ~ 1e-10 if df**2 < 0.5e-10
|
|
143
|
+
DT_HISTORY.clear() # restart collection
|
|
144
|
+
|
|
145
|
+
# Cap correction if positive value supplied
|
|
146
|
+
df_applied = df_toapply
|
|
147
|
+
if cap > 0:
|
|
148
|
+
df_applied = max(-cap, min(cap, df_toapply))
|
|
149
|
+
|
|
150
|
+
logger.info(
|
|
151
|
+
"Correction: %5.1f ppb (%5.1f ppb @ %s)",
|
|
152
|
+
df_applied * 1e9,
|
|
153
|
+
df_toapply * 1e9,
|
|
154
|
+
epochs[-1],
|
|
155
|
+
)
|
|
156
|
+
return df_applied
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def main():
|
|
160
|
+
script_name = Path(sys.argv[0]).name
|
|
161
|
+
parser = configargparse.ArgumentParser(
|
|
162
|
+
add_config_file_help=False,
|
|
163
|
+
default_config_files=[f"{script_name}.default.conf"],
|
|
164
|
+
description=parse_docstring_description(__doc__),
|
|
165
|
+
formatter_class=ArgparseCustomFormatter,
|
|
166
|
+
add_help=False,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
# fmt: off
|
|
170
|
+
# Boilerplate
|
|
171
|
+
pgroup_config = parser.add_argument_group("display/configuration")
|
|
172
|
+
pgroup_config.add_argument(
|
|
173
|
+
"-h", "--help", action="store_true",
|
|
174
|
+
help="Show this help message and exit")
|
|
175
|
+
pgroup_config.add_argument(
|
|
176
|
+
"-v", "--verbosity", action="count", default=0,
|
|
177
|
+
help="Specify debug verbosity, e.g. -vv for more verbosity")
|
|
178
|
+
pgroup_config.add_argument(
|
|
179
|
+
"-L", "--logging", metavar="",
|
|
180
|
+
help="Log to file, if specified. Log level follows verbosity.")
|
|
181
|
+
pgroup_config.add_argument(
|
|
182
|
+
"--config", metavar="file", is_config_file_arg=True,
|
|
183
|
+
help="Path to configuration file")
|
|
184
|
+
pgroup_config.add_argument(
|
|
185
|
+
"--save", metavar="", is_write_out_config_file_arg=True,
|
|
186
|
+
help="Path to configuration file for saving, then immediately exit")
|
|
187
|
+
|
|
188
|
+
# freqcalc parameters
|
|
189
|
+
pgroup = parser.add_argument_group("freqservo parameters")
|
|
190
|
+
pgroup.add_argument(
|
|
191
|
+
"-i", metavar="ignore", type=int, default=5,
|
|
192
|
+
help="Ignore initial epochs during calculation (default: %(default)d)")
|
|
193
|
+
pgroup.add_argument(
|
|
194
|
+
"-a", metavar="average", type=int, default=3,
|
|
195
|
+
help="Number of epochs to average servoed time over (default: %(default)d)")
|
|
196
|
+
pgroup.add_argument(
|
|
197
|
+
"-s", metavar="separation", type=int, default=12,
|
|
198
|
+
help="Separation width for frequency calculation, epoch units (default: %(default)d)")
|
|
199
|
+
pgroup.add_argument(
|
|
200
|
+
"-c", metavar="cap", type=float, default=100e-9,
|
|
201
|
+
help="Correction limit/cap per frequency update, disable with 0 (default: 100e-9)")
|
|
202
|
+
|
|
203
|
+
# costream parameters
|
|
204
|
+
pgroup = parser.add_argument_group("costream parameters")
|
|
205
|
+
pgroup.add_argument(
|
|
206
|
+
"-n", metavar="logfile5",
|
|
207
|
+
help="Optional path to pipe of costream/freq outputs (default: stdin)")
|
|
208
|
+
pgroup.add_argument(
|
|
209
|
+
"-V", metavar="level", type=int, choices=(4,5),
|
|
210
|
+
help="Output format of costream (default: '<epoch:str> <dt:float_ns>\\n')")
|
|
211
|
+
pgroup.add_argument(
|
|
212
|
+
"-e", action="store_true",
|
|
213
|
+
help="Echo costream output back into stderr")
|
|
214
|
+
|
|
215
|
+
# freqcd parameters
|
|
216
|
+
pgroup = parser.add_argument_group("freqcd parameters")
|
|
217
|
+
pgroup.add_argument(
|
|
218
|
+
"-F", metavar="freqfilename",
|
|
219
|
+
help="Path to freqcd frequency correction pipe (default: stdout)")
|
|
220
|
+
pgroup.add_argument(
|
|
221
|
+
"-f", metavar="freqcorr", type=int, default=0,
|
|
222
|
+
help="freqcd initial frequency offset, used if '-u' not supplied (default: %(default)d)")
|
|
223
|
+
pgroup.add_argument(
|
|
224
|
+
"-d", action="store_true",
|
|
225
|
+
help="freqcd in 'decimal mode' (default: False)")
|
|
226
|
+
pgroup.add_argument(
|
|
227
|
+
"-u", action="store_true",
|
|
228
|
+
help="freqcd in 'update mode' (default: False)")
|
|
229
|
+
# fmt: on
|
|
230
|
+
|
|
231
|
+
# Parse arguments
|
|
232
|
+
args = parser.parse_args()
|
|
233
|
+
|
|
234
|
+
# Check whether options have been supplied, and print help otherwise
|
|
235
|
+
if args.help:
|
|
236
|
+
parser.print_help(sys.stderr)
|
|
237
|
+
sys.exit(1)
|
|
238
|
+
|
|
239
|
+
# Set logging level and log arguments
|
|
240
|
+
if args.logging is not None:
|
|
241
|
+
handler = logging.FileHandler(filename=args.logging, mode="w")
|
|
242
|
+
handler.setFormatter(
|
|
243
|
+
logging.Formatter(
|
|
244
|
+
fmt=_LOGGING_FMT,
|
|
245
|
+
datefmt="%Y%m%d_%H%M%S",
|
|
246
|
+
style="{",
|
|
247
|
+
)
|
|
248
|
+
)
|
|
249
|
+
logger.addHandler(handler)
|
|
250
|
+
|
|
251
|
+
# Set logging level
|
|
252
|
+
levels = [logging.WARNING, logging.INFO, logging.DEBUG]
|
|
253
|
+
verbosity = min(args.verbosity, len(levels) - 1)
|
|
254
|
+
logger.setLevel(levels[verbosity])
|
|
255
|
+
logger.debug("%s", args)
|
|
256
|
+
|
|
257
|
+
# Check arguments
|
|
258
|
+
assert args.i > 0
|
|
259
|
+
assert args.a > 0
|
|
260
|
+
assert args.s > 0
|
|
261
|
+
|
|
262
|
+
# Open relevant I/O pipes
|
|
263
|
+
cin = sys.stdin
|
|
264
|
+
if args.n is not None:
|
|
265
|
+
cin = open(args.n, "r")
|
|
266
|
+
|
|
267
|
+
cout = sys.stdout
|
|
268
|
+
if args.F is not None:
|
|
269
|
+
cout = open(args.F, "w")
|
|
270
|
+
|
|
271
|
+
# Mainloop
|
|
272
|
+
try:
|
|
273
|
+
# Set initial frequency difference
|
|
274
|
+
df = args.f
|
|
275
|
+
if not args.d:
|
|
276
|
+
df = args.f * FCORR_BTOD # convert 2^-34 -> 0.1ppb
|
|
277
|
+
df = df * 1e-10 # convert 0.1ppb -> 1
|
|
278
|
+
|
|
279
|
+
# Restarts read if mainloop terminates, e.g. no data on read
|
|
280
|
+
while True:
|
|
281
|
+
line = cin.readline()
|
|
282
|
+
if line == "":
|
|
283
|
+
logger.debug("No new data, waiting...")
|
|
284
|
+
time.sleep(0.5)
|
|
285
|
+
continue
|
|
286
|
+
|
|
287
|
+
# Derive frequency shift from costream
|
|
288
|
+
epoch, dt = parse(line, format=args.V)
|
|
289
|
+
if args.e:
|
|
290
|
+
print(line, end="", file=sys.stderr)
|
|
291
|
+
df1 = evaluate_freqshift(
|
|
292
|
+
epoch,
|
|
293
|
+
dt,
|
|
294
|
+
ignore=args.i,
|
|
295
|
+
average=args.a,
|
|
296
|
+
separation=args.s,
|
|
297
|
+
cap=args.c,
|
|
298
|
+
)
|
|
299
|
+
if df1 is None:
|
|
300
|
+
logger.debug("No changes to frequency submitted.")
|
|
301
|
+
continue
|
|
302
|
+
|
|
303
|
+
# If 'update mode' is enabled on freqcd, we simply pass the correction
|
|
304
|
+
# directly. Otherwise, we manually calculate the new absolute frequency.
|
|
305
|
+
# Note: This is effectively the inverse of freqcd 'update mode'.
|
|
306
|
+
if args.u:
|
|
307
|
+
df = df1
|
|
308
|
+
else:
|
|
309
|
+
df = (1 + df) * (1 + df1) - 1
|
|
310
|
+
|
|
311
|
+
# Convert and write new frequency
|
|
312
|
+
_df = df * 1e10 # convert 1 -> 0.1ppb
|
|
313
|
+
if not args.d:
|
|
314
|
+
_df = _df * FCORR_DTOB # convert 0.1ppb -> 2^-34
|
|
315
|
+
_df = round(_df)
|
|
316
|
+
logger.debug("Wrote '%d' to output", _df)
|
|
317
|
+
cout.write(f"{_df}\n")
|
|
318
|
+
|
|
319
|
+
# Alternatively, use 'open(pipe, "wb", 0)'
|
|
320
|
+
cout.flush() # necessary to avoid trapping df in buffer
|
|
321
|
+
|
|
322
|
+
except KeyboardInterrupt:
|
|
323
|
+
logger.debug("User interrupted.")
|
|
324
|
+
|
|
325
|
+
finally:
|
|
326
|
+
if cin is not sys.stdin:
|
|
327
|
+
cin.close()
|
|
328
|
+
if cout is not sys.stdout:
|
|
329
|
+
cout.close()
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
if __name__ == "__main__":
|
|
333
|
+
main()
|
fpfind/lib/_logging.py
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import sys
|
|
3
|
+
import traceback
|
|
4
|
+
from types import SimpleNamespace
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class LoggingOverrideFormatter(logging.Formatter):
|
|
8
|
+
"""Supports injection of overrides during logging.
|
|
9
|
+
|
|
10
|
+
The following attributes are injectable: '_funcname', '_filename',
|
|
11
|
+
'_lineno', 'details'. The first three are for higher-level stack trace
|
|
12
|
+
information, adapted from [1].
|
|
13
|
+
|
|
14
|
+
Note that multiline logging is generally discouraged. For legacy and
|
|
15
|
+
convenience reasons, multiline logging is enabled by passing
|
|
16
|
+
'human_readable=True' to the Formatter, then furnishing each logger call
|
|
17
|
+
with the 'details' argument, either as a list or a dict. This relies on
|
|
18
|
+
the presence of the '| ' delimiter to separate debugging information from
|
|
19
|
+
text. This delimiter can be redefined.
|
|
20
|
+
|
|
21
|
+
Examples:
|
|
22
|
+
|
|
23
|
+
# Usual logging setup
|
|
24
|
+
>>> logger = logging.getLogger(__name__)
|
|
25
|
+
>>> handler = logging.StreamHandler(stream=sys.stderr)
|
|
26
|
+
>>> handler.setFormatter(LoggingOverrideFormatter())
|
|
27
|
+
>>> logger.addHandler(handler)
|
|
28
|
+
|
|
29
|
+
# Injection of stack trace information
|
|
30
|
+
>>> caller = inspect.getframeinfo(inspect.stack()[1][0])
|
|
31
|
+
>>> extras = {
|
|
32
|
+
... "_funcname": f"[{f.__name__}]",
|
|
33
|
+
... "_filename": os.path.basename(caller.filename),
|
|
34
|
+
... "_lineno": caller.lineno,
|
|
35
|
+
... }
|
|
36
|
+
>>> logger.warning("callme", stacklevel=2, extra=extras)
|
|
37
|
+
|
|
38
|
+
# Append details (machine-parseable format)
|
|
39
|
+
>>> handler.setFormatter(
|
|
40
|
+
... LoggingOverrideFormatter(
|
|
41
|
+
... fmt="{asctime}\t{levelname:<7s}\t| {message}", style="{",
|
|
42
|
+
... )
|
|
43
|
+
... )
|
|
44
|
+
>>> logger.warning(
|
|
45
|
+
... "call!", extras={"details": {
|
|
46
|
+
... "value1": 2,
|
|
47
|
+
... }
|
|
48
|
+
... )
|
|
49
|
+
2024-01-23 07:59:24,743 WARNING | call! {"value1": 2}
|
|
50
|
+
|
|
51
|
+
# Append details (human-readable format)
|
|
52
|
+
>>> handler.setFormatter(
|
|
53
|
+
... LoggingOverrideFormatter(
|
|
54
|
+
... fmt="{asctime}\t{levelname:<7s}\t| {message}", style="{",
|
|
55
|
+
... human_readable=True, delimiter="| ",
|
|
56
|
+
... )
|
|
57
|
+
... )
|
|
58
|
+
>>> logger.warning(
|
|
59
|
+
... "call!", extras={"details": [
|
|
60
|
+
... "value1 is 2ns",
|
|
61
|
+
... "value2 has been read",
|
|
62
|
+
... ]
|
|
63
|
+
... )
|
|
64
|
+
2024-01-23 07:59:24,743 WARNING | call!
|
|
65
|
+
| value1 is 2ns
|
|
66
|
+
| value2 has been read
|
|
67
|
+
|
|
68
|
+
References:
|
|
69
|
+
[1]: <https://stackoverflow.com/a/71228329>
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
def __init__(self, *args, human_readable=False, delimiter="| ", **kwargs):
|
|
73
|
+
# For multiline logging
|
|
74
|
+
self.human_readable = human_readable
|
|
75
|
+
self.delim = delimiter
|
|
76
|
+
super().__init__(*args, **kwargs)
|
|
77
|
+
|
|
78
|
+
def format(self, record):
|
|
79
|
+
# Override attributes with debugging-relevant information
|
|
80
|
+
if hasattr(record, "_funcname"):
|
|
81
|
+
record.funcName = record._funcname # type: ignore
|
|
82
|
+
if hasattr(record, "_filename"):
|
|
83
|
+
record.filename = record._filename # type: ignore
|
|
84
|
+
if hasattr(record, "_lineno"):
|
|
85
|
+
record.lineno = record._lineno # type: ignore
|
|
86
|
+
message = super().format(record)
|
|
87
|
+
|
|
88
|
+
# Append additional debugging info
|
|
89
|
+
details = getattr(record, "details", None)
|
|
90
|
+
if details is None:
|
|
91
|
+
pass # ignore
|
|
92
|
+
elif not self.human_readable:
|
|
93
|
+
message = f"{message}\t{details}" # append details to the back
|
|
94
|
+
else:
|
|
95
|
+
# Enable multiline logging for humans to read
|
|
96
|
+
if isinstance(details, dict): # parse into array
|
|
97
|
+
details = [f"{k}: {v}" for k, v in details.items()]
|
|
98
|
+
if isinstance(details, (list, tuple)) and len(details) > 0:
|
|
99
|
+
pre, _, text = message.partition(self.delim)
|
|
100
|
+
pad = " " * (len(text) - len(text.lstrip(" ")))
|
|
101
|
+
# preserve tabs, alternatively use 'str.expandtabs'
|
|
102
|
+
_pre = "".join([c if c.isspace() else " " for c in pre])
|
|
103
|
+
text = text.lstrip(" ")
|
|
104
|
+
|
|
105
|
+
# Collect all messages before concatenating
|
|
106
|
+
messages = [message]
|
|
107
|
+
if text == "": # replace first line if empty
|
|
108
|
+
text, details = details[0], details[1:]
|
|
109
|
+
messages = [f"{pre}{self.delim}{pad} {text}"]
|
|
110
|
+
messages.extend(
|
|
111
|
+
[f"{_pre}{self.delim}{pad} {text}" for text in details]
|
|
112
|
+
)
|
|
113
|
+
message = "\n".join(messages)
|
|
114
|
+
|
|
115
|
+
return message
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def get_logger(name):
|
|
119
|
+
logger = logging.getLogger(name)
|
|
120
|
+
logger.setLevel(logging.WARNING) # default logging level
|
|
121
|
+
if not logger.handlers:
|
|
122
|
+
handler = logging.StreamHandler(stream=sys.stderr)
|
|
123
|
+
handler.setFormatter(DEFAULT_FORMATTER)
|
|
124
|
+
logger.addHandler(handler)
|
|
125
|
+
logger.propagate = False
|
|
126
|
+
|
|
127
|
+
_indentation = 0
|
|
128
|
+
|
|
129
|
+
def log(n=0):
|
|
130
|
+
nonlocal _indentation
|
|
131
|
+
_indentation = n
|
|
132
|
+
return namespace
|
|
133
|
+
|
|
134
|
+
def create_logfunc(f):
|
|
135
|
+
def logfunc(message, *details):
|
|
136
|
+
nonlocal _indentation
|
|
137
|
+
msg = " " * _indentation + message
|
|
138
|
+
caller = traceback.extract_stack(limit=2)[0]
|
|
139
|
+
extras = {
|
|
140
|
+
"_funcname": caller.name,
|
|
141
|
+
"_filename": caller.filename,
|
|
142
|
+
"_lineno": caller.lineno,
|
|
143
|
+
"details": details,
|
|
144
|
+
}
|
|
145
|
+
f(msg, extra=extras)
|
|
146
|
+
_indentation = 0
|
|
147
|
+
|
|
148
|
+
return logfunc
|
|
149
|
+
|
|
150
|
+
namespace = SimpleNamespace(
|
|
151
|
+
debug=create_logfunc(logger.debug),
|
|
152
|
+
info=create_logfunc(logger.info),
|
|
153
|
+
warning=create_logfunc(logger.warning),
|
|
154
|
+
error=create_logfunc(logger.error),
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
return logger, log
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def set_logfile(logger, path):
|
|
161
|
+
handler = logging.FileHandler(filename=path, mode="w")
|
|
162
|
+
handler.setFormatter(DEFAULT_FORMATTER)
|
|
163
|
+
logger.addHandler(handler)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def set_verbosity(logger, verbosity):
|
|
167
|
+
levels = [logging.WARNING, logging.INFO, logging.DEBUG]
|
|
168
|
+
verbosity = min(verbosity, len(levels) - 1)
|
|
169
|
+
logger.setLevel(levels[verbosity])
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
DEFAULT_FORMATTER = LoggingOverrideFormatter(
|
|
173
|
+
fmt="{asctime}\t{levelname:<7s}\t{funcName}:{lineno}\t| {message}",
|
|
174
|
+
datefmt="%Y%m%d_%H%M%S",
|
|
175
|
+
style="{",
|
|
176
|
+
human_readable=True,
|
|
177
|
+
)
|
fpfind/lib/constants.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import enum
|
|
2
|
+
import warnings
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TSRES(enum.Enum):
|
|
8
|
+
"""Stores timestamp resolution information.
|
|
9
|
+
|
|
10
|
+
Values assigned correspond to the number of units within a
|
|
11
|
+
span of 1 nanosecond.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
NS2 = 0.5 # S-Fifteen TDC1 timestamp
|
|
15
|
+
NS1 = 1
|
|
16
|
+
PS125 = 8 # CQT Red timestamp
|
|
17
|
+
PS4 = 256 # S-Fifteen TDC2 timestamp
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class FrequencyCompensation(enum.Enum):
|
|
21
|
+
FORCE = enum.auto() # never disable frequency compensation
|
|
22
|
+
ENABLE = enum.auto() # disables when no frequency detected
|
|
23
|
+
DISABLE = enum.auto()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
EPOCH_LENGTH = 1 << 29 # from filespec
|
|
27
|
+
FCORR_AMAXBITS = -13 # from 'freqcd.c'
|
|
28
|
+
NTP_MAXDELAY_NS = 200e6 # for very *very* asymmetric channels
|
|
29
|
+
|
|
30
|
+
# Derived constants
|
|
31
|
+
EPOCH_DURATION = EPOCH_LENGTH * 1e-9 # in seconds
|
|
32
|
+
MAX_FCORR = 2**FCORR_AMAXBITS
|
|
33
|
+
|
|
34
|
+
# Maximum timing resolution [ns] during timing doubling
|
|
35
|
+
MAX_TIMING_RESOLUTION_NS = 1e5
|
|
36
|
+
|
|
37
|
+
# Compilations of numpy that do not include support for 128-bit floats will not
|
|
38
|
+
# expose 'np.float128'. We map such instances directly into a 64-bit float instead.
|
|
39
|
+
# Note that some variants implicitly map 'np.float128' to 'np.float64' as well.
|
|
40
|
+
#
|
|
41
|
+
# The availability of this alias depends on the available build flags for numpy,
|
|
42
|
+
# and is platform-dependent. This is usually extended-precision (80-bits) padded
|
|
43
|
+
# to 128-bits for efficiency.
|
|
44
|
+
NP_PRECISEFLOAT = np.float64
|
|
45
|
+
if hasattr(np, "float128"):
|
|
46
|
+
NP_PRECISEFLOAT = np.float128
|
|
47
|
+
else:
|
|
48
|
+
warnings.warn(
|
|
49
|
+
"Extended-precision floats unsupported in current numpy build on this "
|
|
50
|
+
"platform: falling back to 64-bit floats instead.\n\n"
|
|
51
|
+
"Hint: To avoid precision loss when using time taggers with <125ps "
|
|
52
|
+
"precision, read timestamps as 64-bit integers instead, e.g. "
|
|
53
|
+
"'read_a1(..., fractional=False)'."
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class PeakFindingFailed(ValueError):
|
|
58
|
+
def __init__(
|
|
59
|
+
self,
|
|
60
|
+
message,
|
|
61
|
+
significance=None,
|
|
62
|
+
resolution=None,
|
|
63
|
+
dt1=None,
|
|
64
|
+
dt2=None,
|
|
65
|
+
dt=None,
|
|
66
|
+
df=None,
|
|
67
|
+
):
|
|
68
|
+
self.message = message
|
|
69
|
+
self.s = significance
|
|
70
|
+
self.r = resolution
|
|
71
|
+
self.dt1 = dt1
|
|
72
|
+
self.dt2 = dt2
|
|
73
|
+
self.dt = dt
|
|
74
|
+
self.df = df
|
|
75
|
+
super().__init__(message)
|
|
76
|
+
|
|
77
|
+
def __str__(self):
|
|
78
|
+
text = self.message
|
|
79
|
+
suppl = []
|
|
80
|
+
if self.s is not None:
|
|
81
|
+
suppl.append(f"S={self.s:8.3f}")
|
|
82
|
+
if self.r is not None:
|
|
83
|
+
suppl.append(f"r={self.r:5.0f}")
|
|
84
|
+
if self.dt1 is not None:
|
|
85
|
+
suppl.append(f"dt1={self.dt1:11.0f}")
|
|
86
|
+
if self.dt2 is not None:
|
|
87
|
+
suppl.append(f"dt2={self.dt2:11.0f}")
|
|
88
|
+
if self.dt is not None:
|
|
89
|
+
suppl.append(f"dt={self.dt:11.0f}")
|
|
90
|
+
if self.df is not None:
|
|
91
|
+
suppl.append(f"df={self.df * 1e6:.4f}ppm")
|
|
92
|
+
if suppl:
|
|
93
|
+
text = f"{text} ({', '.join(suppl)})"
|
|
94
|
+
return text
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class PeakFindingTerminated(RuntimeError):
|
|
98
|
+
pass
|