QuLab 2.10.10__cp313-cp313-macosx_10_13_universal2.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.
Files changed (107) hide show
  1. qulab/__init__.py +33 -0
  2. qulab/__main__.py +4 -0
  3. qulab/cli/__init__.py +0 -0
  4. qulab/cli/commands.py +30 -0
  5. qulab/cli/config.py +170 -0
  6. qulab/cli/decorators.py +28 -0
  7. qulab/dicttree.py +523 -0
  8. qulab/executor/__init__.py +5 -0
  9. qulab/executor/analyze.py +188 -0
  10. qulab/executor/cli.py +434 -0
  11. qulab/executor/load.py +563 -0
  12. qulab/executor/registry.py +185 -0
  13. qulab/executor/schedule.py +543 -0
  14. qulab/executor/storage.py +615 -0
  15. qulab/executor/template.py +259 -0
  16. qulab/executor/utils.py +194 -0
  17. qulab/expression.py +827 -0
  18. qulab/fun.cpython-313-darwin.so +0 -0
  19. qulab/monitor/__init__.py +1 -0
  20. qulab/monitor/__main__.py +8 -0
  21. qulab/monitor/config.py +41 -0
  22. qulab/monitor/dataset.py +77 -0
  23. qulab/monitor/event_queue.py +54 -0
  24. qulab/monitor/mainwindow.py +234 -0
  25. qulab/monitor/monitor.py +115 -0
  26. qulab/monitor/ploter.py +123 -0
  27. qulab/monitor/qt_compat.py +16 -0
  28. qulab/monitor/toolbar.py +265 -0
  29. qulab/scan/__init__.py +2 -0
  30. qulab/scan/curd.py +221 -0
  31. qulab/scan/models.py +554 -0
  32. qulab/scan/optimize.py +76 -0
  33. qulab/scan/query.py +387 -0
  34. qulab/scan/record.py +603 -0
  35. qulab/scan/scan.py +1166 -0
  36. qulab/scan/server.py +450 -0
  37. qulab/scan/space.py +213 -0
  38. qulab/scan/utils.py +234 -0
  39. qulab/storage/__init__.py +0 -0
  40. qulab/storage/__main__.py +51 -0
  41. qulab/storage/backend/__init__.py +0 -0
  42. qulab/storage/backend/redis.py +204 -0
  43. qulab/storage/base_dataset.py +352 -0
  44. qulab/storage/chunk.py +60 -0
  45. qulab/storage/dataset.py +127 -0
  46. qulab/storage/file.py +273 -0
  47. qulab/storage/models/__init__.py +22 -0
  48. qulab/storage/models/base.py +4 -0
  49. qulab/storage/models/config.py +28 -0
  50. qulab/storage/models/file.py +89 -0
  51. qulab/storage/models/ipy.py +58 -0
  52. qulab/storage/models/models.py +88 -0
  53. qulab/storage/models/record.py +161 -0
  54. qulab/storage/models/report.py +22 -0
  55. qulab/storage/models/tag.py +93 -0
  56. qulab/storage/storage.py +95 -0
  57. qulab/sys/__init__.py +2 -0
  58. qulab/sys/chat.py +688 -0
  59. qulab/sys/device/__init__.py +3 -0
  60. qulab/sys/device/basedevice.py +255 -0
  61. qulab/sys/device/loader.py +86 -0
  62. qulab/sys/device/utils.py +79 -0
  63. qulab/sys/drivers/FakeInstrument.py +68 -0
  64. qulab/sys/drivers/__init__.py +0 -0
  65. qulab/sys/ipy_events.py +125 -0
  66. qulab/sys/net/__init__.py +0 -0
  67. qulab/sys/net/bencoder.py +205 -0
  68. qulab/sys/net/cli.py +169 -0
  69. qulab/sys/net/dhcp.py +543 -0
  70. qulab/sys/net/dhcpd.py +176 -0
  71. qulab/sys/net/kad.py +1142 -0
  72. qulab/sys/net/kcp.py +192 -0
  73. qulab/sys/net/nginx.py +194 -0
  74. qulab/sys/progress.py +190 -0
  75. qulab/sys/rpc/__init__.py +0 -0
  76. qulab/sys/rpc/client.py +0 -0
  77. qulab/sys/rpc/exceptions.py +96 -0
  78. qulab/sys/rpc/msgpack.py +1052 -0
  79. qulab/sys/rpc/msgpack.pyi +41 -0
  80. qulab/sys/rpc/router.py +35 -0
  81. qulab/sys/rpc/rpc.py +412 -0
  82. qulab/sys/rpc/serialize.py +139 -0
  83. qulab/sys/rpc/server.py +29 -0
  84. qulab/sys/rpc/socket.py +29 -0
  85. qulab/sys/rpc/utils.py +25 -0
  86. qulab/sys/rpc/worker.py +0 -0
  87. qulab/sys/rpc/zmq_socket.py +227 -0
  88. qulab/tools/__init__.py +0 -0
  89. qulab/tools/connection_helper.py +39 -0
  90. qulab/typing.py +2 -0
  91. qulab/utils.py +95 -0
  92. qulab/version.py +1 -0
  93. qulab/visualization/__init__.py +188 -0
  94. qulab/visualization/__main__.py +71 -0
  95. qulab/visualization/_autoplot.py +464 -0
  96. qulab/visualization/plot_circ.py +319 -0
  97. qulab/visualization/plot_layout.py +408 -0
  98. qulab/visualization/plot_seq.py +242 -0
  99. qulab/visualization/qdat.py +152 -0
  100. qulab/visualization/rot3d.py +23 -0
  101. qulab/visualization/widgets.py +86 -0
  102. qulab-2.10.10.dist-info/METADATA +110 -0
  103. qulab-2.10.10.dist-info/RECORD +107 -0
  104. qulab-2.10.10.dist-info/WHEEL +5 -0
  105. qulab-2.10.10.dist-info/entry_points.txt +2 -0
  106. qulab-2.10.10.dist-info/licenses/LICENSE +21 -0
  107. qulab-2.10.10.dist-info/top_level.txt +1 -0
qulab/dicttree.py ADDED
@@ -0,0 +1,523 @@
1
+ import copy
2
+ import math
3
+ import operator
4
+ import pickle
5
+ import sys
6
+ from typing import Any, Generator
7
+
8
+
9
+ class Singleton(type):
10
+ _instances = {}
11
+
12
+ def __call__(cls, *args, **kwargs):
13
+ if cls not in cls._instances:
14
+ cls._instances[cls] = super(Singleton,
15
+ cls).__call__(*args, **kwargs)
16
+ return cls._instances[cls]
17
+
18
+
19
+ class _SELF(metaclass=Singleton):
20
+ __slots__ = ()
21
+
22
+ def __repr__(self):
23
+ return "self"
24
+
25
+
26
+ class _NOTSET(metaclass=Singleton):
27
+ __slots__ = ()
28
+
29
+ def __repr__(self):
30
+ return 'N/A'
31
+
32
+
33
+ class _UNKNOW(metaclass=Singleton):
34
+ __slots__ = ()
35
+
36
+ def __repr__(self) -> str:
37
+ return "Unknow"
38
+
39
+
40
+ class _DELETE(metaclass=Singleton):
41
+ __slots__ = ()
42
+
43
+ def __repr__(self):
44
+ return 'Delete'
45
+
46
+
47
+ SELF = _SELF()
48
+ NOTSET = _NOTSET()
49
+ UNKNOW = _UNKNOW()
50
+ DELETE = _DELETE()
51
+
52
+
53
+ def flattenDictIter(d: dict,
54
+ prefix: list = []
55
+ ) -> Generator[tuple[str, Any], None, None]:
56
+ for k in d:
57
+ if isinstance(d[k], dict):
58
+ yield from flattenDictIter(d[k], prefix=[*prefix, k])
59
+ else:
60
+ yield '.'.join(prefix + [k]), d[k]
61
+
62
+
63
+ def flattenDict(d: dict[str, Any]) -> dict[str, Any]:
64
+ return {k: v for k, v in flattenDictIter(d)}
65
+
66
+
67
+ def foldDict(d: dict[str, Any]) -> dict[str, Any]:
68
+ ret = {}
69
+
70
+ for k, v in d.items():
71
+ keys = k.split('.')
72
+ d = ret
73
+ parent = None
74
+
75
+ for key in keys[:-1]:
76
+ if not isinstance(d, dict):
77
+ parent[0][parent[1]] = {SELF: d}
78
+ d = parent[0][parent[1]]
79
+ if key not in d:
80
+ d[key] = dict()
81
+ parent = d, key
82
+ d = d[key]
83
+ if not isinstance(d, dict):
84
+ parent[0][parent[1]] = {SELF: d}
85
+ d = parent[0][parent[1]]
86
+ if keys[-1] in d and isinstance(d[keys[-1]], dict):
87
+ d[keys[-1]][SELF] = v
88
+ else:
89
+ d[keys[-1]] = v
90
+ return ret
91
+
92
+
93
+ class Update():
94
+ __slots__ = ('o', 'n')
95
+
96
+ def __init__(self, o, n):
97
+ self.o = o
98
+ self.n = n
99
+
100
+ def __repr__(self):
101
+ return f"Update: {self.o!r} ==> {self.n!r}"
102
+
103
+ def __eq__(self, o: object) -> bool:
104
+ return isinstance(o, Update) and self.n == o.n
105
+
106
+
107
+ class Create():
108
+ __slots__ = ('n', 'replace')
109
+
110
+ def __init__(self, n, replace=False):
111
+ """
112
+ Create a new node
113
+
114
+ :param n: the new node
115
+ :param replace: if True, replace the node if it already exists
116
+ """
117
+ self.n = n
118
+ self.replace = replace
119
+
120
+ def __repr__(self):
121
+ if self.replace:
122
+ return f"Replace: {self.n!r}"
123
+ else:
124
+ return f"Create: {self.n!r}"
125
+
126
+ def __eq__(self, o: object) -> bool:
127
+ return isinstance(
128
+ o, Create) and self.n == o.n and self.replace == o.replace
129
+
130
+
131
+ def _eq(a, b):
132
+ import numpy as np
133
+
134
+ if isinstance(a, np.ndarray):
135
+ return np.array_equal(a, np.asarray(b))
136
+
137
+ try:
138
+ return a == b
139
+ except:
140
+ pass
141
+ if isinstance(a, (list, tuple)):
142
+ return len(a) == len(b) and all(_eq(a[i], b[i]) for i in range(len(a)))
143
+ if isinstance(a, dict):
144
+ return set(a.keys()) == set(b.keys()) and all(
145
+ _eq(a[k], b[k]) for k in a)
146
+
147
+ try:
148
+ return pickle.dumps(a) == pickle.dumps(b)
149
+ except:
150
+ return False
151
+
152
+
153
+ def diff(d1: dict, d2: dict) -> dict:
154
+ """
155
+ Compute the difference between two dictionaries
156
+
157
+ Args:
158
+ d1: the original dictionary
159
+ d2: the new dictionary
160
+
161
+ Returns:
162
+ a dictionary containing the difference between d1 and d2
163
+ """
164
+ ret = {}
165
+ for k in d2:
166
+ if k in d1:
167
+ if isinstance(d2[k], type(d1[k])) and _eq(d1[k], d2[k]):
168
+ pass
169
+ elif isinstance(d1[k], dict) and isinstance(d2[k], dict):
170
+ ret[k] = diff(d1[k], d2[k])
171
+ else:
172
+ ret[k] = Update(d1[k], d2[k])
173
+ else:
174
+ ret[k] = Create(d2[k])
175
+ for k in d1:
176
+ if k not in d2:
177
+ ret[k] = DELETE
178
+ return ret
179
+
180
+
181
+ def patch(source, diff, in_place=False):
182
+ """
183
+ Patch a dictionary with a diff
184
+
185
+ Args:
186
+ source: the original dictionary
187
+ diff: the diff
188
+ in_place: if True, patch the source dictionary in place
189
+
190
+ Returns:
191
+ the patched dictionary
192
+ """
193
+ if in_place:
194
+ ret = source
195
+ else:
196
+ ret = copy.copy(source)
197
+ for k, v in diff.items():
198
+ if isinstance(v, dict):
199
+ ret[k] = patch(source[k], v, in_place=in_place)
200
+ else:
201
+ if isinstance(v, Update):
202
+ ret[k] = v.n
203
+ elif isinstance(v, Create):
204
+ if v.replace or k not in ret:
205
+ ret[k] = v.n
206
+ else:
207
+ update_tree(ret[k], v.n)
208
+ elif v is DELETE:
209
+ del ret[k]
210
+ else:
211
+ raise ValueError(f"Unsupported patch: {v!r}")
212
+ return ret
213
+
214
+
215
+ def merge(diff1, diff2, origin=None):
216
+ """
217
+ Merge two diffs
218
+
219
+ Args:
220
+ diff1: the first diff
221
+ diff2: the second diff
222
+ origin: the original dictionary
223
+
224
+ Returns:
225
+ the merged diff
226
+ """
227
+ if origin is not None:
228
+ updated = patch(patch(origin, diff1), diff2)
229
+ return diff(origin, updated)
230
+
231
+ ret = {}
232
+ for k, v in diff1.items():
233
+ if k in diff2:
234
+ v2 = diff2[k]
235
+ if isinstance(v, dict) and isinstance(v2, dict):
236
+ d = merge(v, v2)
237
+ if d:
238
+ ret[k] = d
239
+ else:
240
+ if isinstance(v, Update) and isinstance(v2, Update):
241
+ ret[k] = Update(v.o, v2.n)
242
+ elif isinstance(v, Create) and isinstance(v2, dict):
243
+ ret[k] = Create(patch(copy.copy(v.n), v2, True))
244
+ elif isinstance(v, Create) and isinstance(v2, Update):
245
+ ret[k] = Create(v2.n)
246
+ elif isinstance(v, Create) and v2 is DELETE:
247
+ pass
248
+ elif v2 is DELETE:
249
+ ret[k] = DELETE
250
+ elif v is DELETE and isinstance(v2, Create):
251
+ if isinstance(v2.n, dict):
252
+ ret[k] = Create(v2.n, replace=True)
253
+ else:
254
+ ret[k] = Update(UNKNOW, v2.n)
255
+ elif isinstance(v2, Create) and v2.replace:
256
+ ret[k] = v2
257
+ else:
258
+ raise ValueError(f"Unsupported merge: {v!r} {v2!r}")
259
+ else:
260
+ ret[k] = v
261
+ for k, v in diff2.items():
262
+ if k not in diff1:
263
+ ret[k] = v
264
+ return ret
265
+
266
+
267
+ def print_diff(d, limit=None, offset=0, file=sys.stdout, ignores=None):
268
+ """
269
+ Print a diff
270
+
271
+ Args:
272
+ d: the diff
273
+ limit: the maximum number of lines to print
274
+ offset: the offset of the first line
275
+ file: the file to print to
276
+ ignores: ignore keys starting with this prefix
277
+ """
278
+ count = 0
279
+ for i, (k, v) in enumerate(flattenDictIter(d)):
280
+ if count >= offset:
281
+ if ignores is not None and k.startswith(ignores):
282
+ continue
283
+ print(f"{k:40}", v, file=file)
284
+ count += 1
285
+ if limit is not None and count >= limit:
286
+ break
287
+
288
+
289
+ def update_tree(result, updates):
290
+ for k, v in updates.items():
291
+ if isinstance(v, dict):
292
+ if k not in result or not isinstance(result[k], dict):
293
+ result[k] = {}
294
+ update_tree(result[k], v)
295
+ else:
296
+ result[k] = v
297
+ return result
298
+
299
+
300
+ def queryref_tree(q: str, keys, dct, prefix=[], chain=None):
301
+ q = q.removeprefix('$')
302
+ if q.startswith('.'):
303
+ while q.startswith('.'):
304
+ keys.pop()
305
+ q = q[1:]
306
+ q = '.'.join(keys + [q])
307
+
308
+ if chain is None:
309
+ chain = [q]
310
+ elif q in chain:
311
+ raise KeyError(f'Circular reference: {chain+[q]}')
312
+
313
+ return query_tree(q, dct, prefix=prefix, eval=True, chain=chain + [q])
314
+
315
+
316
+ def query_tree(q, dct, prefix=[], eval=True, chain=None):
317
+ ret = dct
318
+ keys = q.split('.')
319
+ for i, key in enumerate(keys):
320
+ if key not in ret:
321
+ return (NOTSET, '.'.join(prefix + keys[:i + 1]))
322
+ ret = ret[key]
323
+ if isinstance(ret, str) and eval:
324
+ if ret.startswith('$'):
325
+ return queryref_tree(ret, keys, dct, prefix=prefix, chain=chain)
326
+ elif ret.startswith('&'):
327
+ return eval_expr(ret[1:], Env(dct,
328
+ keys,
329
+ prefix=prefix,
330
+ chain=chain))
331
+ return ret
332
+
333
+
334
+ class Env():
335
+
336
+ def __init__(self, dct=None, keys=None, prefix=[], chain=None):
337
+ self.dct = dct if dct is not None else {}
338
+ self.keys = keys if keys is not None else []
339
+ self.chain = chain
340
+ self.prefix = prefix
341
+
342
+ def get(self, name):
343
+ return queryref_tree(name,
344
+ self.keys,
345
+ self.dct,
346
+ prefix=self.prefix,
347
+ chain=self.chain)
348
+
349
+
350
+ internal_functions = {
351
+ '**': operator.pow,
352
+ '+': operator.add,
353
+ '-': operator.sub,
354
+ '*': operator.mul,
355
+ '/': operator.truediv,
356
+ '//': operator.floordiv,
357
+ '%': operator.mod,
358
+ '&': operator.and_,
359
+ '|': operator.or_,
360
+ '^': operator.xor,
361
+ '~': operator.invert,
362
+ '<<': operator.lshift,
363
+ '>>': operator.rshift,
364
+ 'getitem': operator.getitem,
365
+ 'contains': operator.contains,
366
+ 'not': operator.not_,
367
+ 'getattr': getattr,
368
+ 'abs': abs,
369
+ 'min': min,
370
+ 'max': max,
371
+ 'sum': sum,
372
+ 'len': len,
373
+ 'any': any,
374
+ 'all': all,
375
+ 'ord': ord,
376
+ 'chr': chr,
377
+ 'bin': bin,
378
+ 'oct': oct,
379
+ 'hex': hex,
380
+ 'int': int,
381
+ 'round': round,
382
+ 'real': lambda x: x.real,
383
+ 'imag': lambda x: x.imag,
384
+ 'sqrt': math.sqrt,
385
+ 'sin': math.sin,
386
+ 'cos': math.cos,
387
+ 'tan': math.tan,
388
+ 'asin': math.asin,
389
+ 'acos': math.acos,
390
+ 'atan': math.atan,
391
+ 'atan2': math.atan2,
392
+ 'sinh': math.sinh,
393
+ 'cosh': math.cosh,
394
+ 'tanh': math.tanh,
395
+ 'asinh': math.asinh,
396
+ 'acosh': math.acosh,
397
+ 'atanh': math.atanh,
398
+ 'ceil': math.ceil,
399
+ 'floor': math.floor,
400
+ 'trunc': math.trunc,
401
+ 'log10': math.log10,
402
+ 'log': math.log,
403
+ 'exp': math.exp,
404
+ }
405
+
406
+
407
+ def eval_expr(expression, env=None, functions=None):
408
+ from pyparsing import (Combine, Forward, Literal, MatchFirst, ParseResults,
409
+ Regex, Suppress, Word, ZeroOrMore, alphanums,
410
+ alphas, delimitedList, infixNotation, oneOf,
411
+ opAssoc, pyparsing_common)
412
+ if functions is None:
413
+ functions = internal_functions
414
+
415
+ def lookup(name):
416
+ return env.get(name)
417
+
418
+ def apply(fun, *args):
419
+ return functions[fun](*args)
420
+
421
+ def eval_expr(t):
422
+ if not isinstance(t, ParseResults):
423
+ return t
424
+ if t[0] == '-' and len(t) == 2:
425
+ return -t[1]
426
+ return apply(t[1], t[0], t[2])
427
+
428
+ # Define pyparsing grammar
429
+ expr = Forward()
430
+
431
+ complex_ = (pyparsing_common.number +
432
+ 'j').setParseAction(lambda s, l, t: complex(t[0]))
433
+ number = pyparsing_common.number | complex_
434
+
435
+ const = oneOf('pi e').setParseAction(lambda s, l, t: {
436
+ 'pi': math.pi,
437
+ 'e': math.e
438
+ }.get(t[0]))
439
+
440
+ identifier = Word(alphas + '_', alphanums + '_')
441
+ dollar = Literal('$')
442
+ dot_sequence = Regex(r'\.{1,}')
443
+ attr = Combine(Literal('.') + identifier)
444
+ attr_chain = ZeroOrMore(attr)
445
+ dollar_named_chain = Combine(dollar + identifier + attr_chain)
446
+ dollar_dotN_chain = Combine(dollar + dot_sequence + identifier +
447
+ attr_chain)
448
+ dollar_simple = Combine(dollar + identifier)
449
+
450
+ variable = MatchFirst(
451
+ [dollar_dotN_chain, dollar_named_chain,
452
+ dollar_simple]).setParseAction(lambda s, l, t: lookup(t[0]))
453
+
454
+ bracket = Suppress('(') + expr + Suppress(')')
455
+ bracket.setParseAction(lambda s, l, t: t[0])
456
+
457
+ operand = const | variable | number | bracket
458
+
459
+ func = pyparsing_common.identifier
460
+ func_call = func + Suppress("(") + delimitedList(expr) + Suppress(")")
461
+ func_call.setParseAction(lambda s, l, t: apply(t[0], *t[1:]))
462
+
463
+ term = operand | func_call
464
+
465
+ expr << infixNotation(term, [
466
+ (Literal('**'), 2, opAssoc.RIGHT),
467
+ (Literal('~'), 1, opAssoc.RIGHT),
468
+ (Literal('-'), 1, opAssoc.RIGHT),
469
+ (oneOf('* / // %'), 2, opAssoc.LEFT),
470
+ (oneOf('+ -'), 2, opAssoc.LEFT),
471
+ (oneOf('>> <<'), 2, opAssoc.LEFT),
472
+ (Literal('&'), 2, opAssoc.LEFT),
473
+ (Literal('^'), 2, opAssoc.LEFT),
474
+ (Literal('|'), 2, opAssoc.LEFT),
475
+ ])
476
+
477
+ expr.setParseAction(lambda s, l, t: eval_expr(t[0]))
478
+
479
+ parsed = expr.parseString(expression, parseAll=True)
480
+ return parsed[0]
481
+
482
+
483
+ def sorted_tree(dct, *, keys=None):
484
+ if keys is None or callable(keys):
485
+ key = keys
486
+ elif isinstance(keys, list):
487
+ key = keys[0]
488
+ if len(keys) > 1:
489
+ keys = keys[1:]
490
+ else:
491
+ keys = None
492
+ elif (isinstance(keys, tuple) and len(keys) == 2
493
+ and isinstance(keys[1], dict)):
494
+ key = keys[0]
495
+ keys = keys[1]
496
+ else:
497
+ raise Exception(f"Unsupported keys: {keys!r}")
498
+
499
+ if isinstance(dct, dict):
500
+ if isinstance(keys, dict):
501
+ default = keys.get('default', None)
502
+ return {
503
+ k: sorted_tree(dct[k], keys=keys.get(k, default))
504
+ for k in sorted(dct.keys(), key=key)
505
+ }
506
+ else:
507
+ return {
508
+ k: sorted_tree(dct[k], keys=keys)
509
+ for k in sorted(dct.keys(), key=key)
510
+ }
511
+ elif isinstance(dct, set):
512
+ if isinstance(keys, dict):
513
+ default = keys.get('default', None)
514
+ return set([
515
+ sorted_tree(v, keys=keys.get(v, default))
516
+ for v in sorted(list(dct), key=key)
517
+ ])
518
+ else:
519
+ return set([
520
+ sorted_tree(v, keys=keys) for v in sorted(list(dct), key=key)
521
+ ])
522
+ else:
523
+ return dct
@@ -0,0 +1,5 @@
1
+ from loguru import logger
2
+
3
+ from .schedule import maintain, run
4
+
5
+ # logger.configure(handlers=[])
@@ -0,0 +1,188 @@
1
+ import pickle
2
+ import threading
3
+ import time
4
+
5
+ import pyperclip
6
+ import zmq
7
+
8
+ from .storage import Report
9
+
10
+ # 需要复制到 Notebook 的代码模板
11
+ clip_template = """
12
+ from qulab.executor.analyze import get_report as get_report_{server_port}
13
+
14
+ report, history = get_report_{server_port}("tcp://{server_address}:{server_port}")
15
+ # 在这里插入数据处理逻辑
16
+
17
+ {analysis_code}
18
+ """
19
+
20
+ analysis_code = """
21
+ # report.state = 'OK'
22
+ # report.parameters = {}
23
+ # report.oracle = {}
24
+ # report.other_infomation = {}
25
+
26
+ report.parameters
27
+
28
+ """
29
+
30
+
31
+ # ZeroMQ 服务线程,用于响应 Notebook 的请求
32
+ class ServerThread(threading.Thread):
33
+
34
+ def __init__(self, data, timeout):
35
+ super().__init__()
36
+ self.data = data
37
+ self.result = None
38
+ self.port = 0
39
+ self.timeout = timeout
40
+ self.running = True
41
+ self.finished = threading.Event()
42
+ self.context = zmq.Context()
43
+
44
+ def find_free_port(self):
45
+ with zmq.Socket(self.context, zmq.REQ) as s:
46
+ s.bind_to_random_port("tcp://*")
47
+ self.port = int(
48
+ s.getsockopt(zmq.LAST_ENDPOINT).decode().split(":")[-1])
49
+ s.unbind(s.getsockopt(zmq.LAST_ENDPOINT))
50
+ return self.port
51
+
52
+ def run(self):
53
+ self.port = self.find_free_port()
54
+ socket = self.context.socket(zmq.REP)
55
+ socket.bind(f"tcp://*:{self.port}")
56
+ # 设置 recv 超时 1 秒
57
+ socket.RCVTIMEO = 1000
58
+ start_time = time.time()
59
+ try:
60
+ while self.running and (time.time() - start_time < self.timeout):
61
+ try:
62
+ msg = socket.recv()
63
+ except zmq.Again:
64
+ continue # 超时后继续等待
65
+
66
+ # Notebook 端请求数据
67
+ if msg == b"GET":
68
+ socket.send(pickle.dumps(self.data))
69
+ else:
70
+ # Notebook 端提交了处理结果
71
+ try:
72
+ self.result = pickle.loads(msg)
73
+ except Exception as e:
74
+ # 如果解析失败,也返回默认 ACK
75
+ self.result = None
76
+ socket.send(b"ACK")
77
+ self.running = False
78
+ break
79
+ finally:
80
+ socket.close()
81
+ self.context.term()
82
+ self.finished.set()
83
+
84
+ def stop(self):
85
+ self.running = False
86
+ self.finished.set()
87
+
88
+ def wait_for_result(self):
89
+ self.finished.wait()
90
+ return self.result
91
+
92
+
93
+ # 进入分析流程,启动服务并打印等待提示
94
+ def get_result_or_wait_until_timeout(report: Report, history: Report | None,
95
+ timeout: float) -> Report:
96
+ server = ServerThread((report, history), timeout)
97
+ server.start()
98
+
99
+ parameters = report.parameters
100
+ oracle = report.oracle
101
+ other_infomation = report.other_infomation
102
+ state = report.state
103
+
104
+ # 格式化代码模板
105
+ code = clip_template.format(server_address="127.0.0.1",
106
+ server_port=server.port,
107
+ analysis_code=analysis_code)
108
+
109
+ # 将代码复制到剪切板
110
+ pyperclip.copy(code)
111
+
112
+ # 同时打印到终端,防止误操作导致剪切板内容丢失
113
+ print("请在 Jupyter Notebook 中运行下面这段代码:")
114
+ print("-" * 60)
115
+ print(code)
116
+ print("-" * 60)
117
+ print("等待 Notebook 提交处理结果,或等待超时({} 秒)...".format(timeout))
118
+
119
+ start_time = time.time()
120
+ # 采用循环等待提交结果,间隔 0.5 秒检测一次
121
+ while server.finished.wait(timeout=0.5) is False:
122
+ elapsed = time.time() - start_time
123
+ if elapsed >= timeout:
124
+ # 超时后结束等待
125
+ server.stop()
126
+ break
127
+
128
+ result = server.wait_for_result()
129
+ if result is None:
130
+ return (state, parameters, oracle, other_infomation, code)
131
+ else:
132
+ return result
133
+
134
+
135
+ def manual_analysis(report: Report, history=None, timeout=3600):
136
+ try:
137
+ (state, parameters, oracle, other_infomation,
138
+ code) = get_result_or_wait_until_timeout(report, history, timeout)
139
+ report.parameters = parameters
140
+ report.oracle = oracle
141
+ report.state = state
142
+ report.other_infomation = other_infomation
143
+ except Exception as e:
144
+ pass
145
+ return report
146
+
147
+
148
+ def get_report(address: str) -> Report:
149
+ import IPython
150
+
151
+ ipy = IPython.get_ipython()
152
+ if ipy is None:
153
+ raise RuntimeError("请在 Jupyter Notebook 中运行此函数。")
154
+ ipy.set_next_input(("from qulab.executor.analyze import submit_report\n"
155
+ "# 处理完成后,提交结果\n"
156
+ f"# submit_report(report, \"{address}\")"),
157
+ replace=False)
158
+ context = zmq.Context()
159
+ sock = context.socket(zmq.REQ)
160
+ sock.connect(address)
161
+ # 请求数据
162
+ sock.send(b"GET")
163
+ report, history = pickle.loads(sock.recv())
164
+ return report, history
165
+
166
+
167
+ def submit_report(report: Report, address: str):
168
+ import IPython
169
+
170
+ ipy = IPython.get_ipython()
171
+ if ipy is None:
172
+ raise RuntimeError("请在 Jupyter Notebook 中运行此函数。")
173
+
174
+ code = ipy.user_ns['In'][-2]
175
+
176
+ parameters = report.parameters
177
+ oracle = report.oracle
178
+ other_infomation = report.other_infomation
179
+ state = report.state
180
+
181
+ context = zmq.Context()
182
+ sock = context.socket(zmq.REQ)
183
+ sock.connect(address)
184
+ # 提交处理后的结果
185
+ sock.send(pickle.dumps(
186
+ (state, parameters, oracle, other_infomation, code)))
187
+ ack = sock.recv()
188
+ print("结果已提交。")