dsh-labscript 0.2.0 → 0.2.1
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.
- package/CHANGELOG.md +30 -19
- package/LICENSE +21 -21
- package/README.md +161 -154
- package/cordis.patch.yml +6 -6
- package/docs/INSTALL.zh.md +64 -54
- package/docs/README.zh.md +57 -57
- package/examples/config.example.yml +33 -33
- package/examples/postprocess_demo.py +19 -19
- package/examples/protocols/demo_scan.yaml +18 -18
- package/lib/client.js +193 -193
- package/lib/index.js +138 -138
- package/lib/protocols.js +112 -112
- package/lib/runtime.js +350 -326
- package/lib/skill.js +60 -60
- package/lib/store.mjs +136 -136
- package/lib/tools.js +456 -432
- package/package.json +69 -69
- package/python/bridge.py +1506 -1445
- package/python/tests/run_tests.py +309 -239
package/python/bridge.py
CHANGED
|
@@ -1,1445 +1,1506 @@
|
|
|
1
|
-
"""dsh-labscript bridge: one JSON request on stdin, one JSON envelope on stdout.
|
|
2
|
-
|
|
3
|
-
commands: status, set, engage, wait, abort, data, restore,
|
|
4
|
-
protocol_plan, protocol_start, protocol_finalize, progress
|
|
5
|
-
|
|
6
|
-
The node plugin spawns this script with one python interpreter configured by the
|
|
7
|
-
user; it must be able to import numpy and h5py (the labscript environment has
|
|
8
|
-
both). The labscript backend additionally imports runmanager.remote lazily.
|
|
9
|
-
"""
|
|
10
|
-
import ast
|
|
11
|
-
import glob
|
|
12
|
-
import json
|
|
13
|
-
import os
|
|
14
|
-
import sys
|
|
15
|
-
import time
|
|
16
|
-
import datetime
|
|
17
|
-
import importlib
|
|
18
|
-
import traceback
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
class BridgeError(Exception):
|
|
22
|
-
def __init__(self, code, message, detail=None):
|
|
23
|
-
Exception.__init__(self, message)
|
|
24
|
-
self.code = code
|
|
25
|
-
self.message = message
|
|
26
|
-
self.detail = detail or {}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
def now_iso():
|
|
30
|
-
return datetime.datetime.now().astimezone().isoformat(timespec="seconds")
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
def expand(path):
|
|
34
|
-
if not path:
|
|
35
|
-
return path
|
|
36
|
-
return os.path.expanduser(str(path))
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def jsonable(value):
|
|
40
|
-
if hasattr(value, "tolist"):
|
|
41
|
-
return jsonable(value.tolist())
|
|
42
|
-
if hasattr(value, "item"):
|
|
43
|
-
return jsonable(value.item())
|
|
44
|
-
if isinstance(value, dict):
|
|
45
|
-
return dict((str(k), jsonable(v)) for k, v in value.items())
|
|
46
|
-
if isinstance(value, (list, tuple)):
|
|
47
|
-
return [jsonable(v) for v in value]
|
|
48
|
-
if isinstance(value, float):
|
|
49
|
-
if value != value or value in (float("inf"), float("-inf")):
|
|
50
|
-
return None
|
|
51
|
-
return value
|
|
52
|
-
if isinstance(value, (str, int, bool)) or value is None:
|
|
53
|
-
return value
|
|
54
|
-
return str(value)
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
_H5_LOCK_LOADED = False
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
def ensure_h5_lock():
|
|
62
|
-
"""Import labscript_utils.h5_lock before h5py (labscript requirement)."""
|
|
63
|
-
global _H5_LOCK_LOADED
|
|
64
|
-
if _H5_LOCK_LOADED:
|
|
65
|
-
return True
|
|
66
|
-
try:
|
|
67
|
-
import labscript_utils.h5_lock # noqa: F401
|
|
68
|
-
_H5_LOCK_LOADED = True
|
|
69
|
-
return True
|
|
70
|
-
except Exception:
|
|
71
|
-
return False
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
def shot_ready(path):
|
|
75
|
-
"""True when a shot h5 is complete enough to read (best effort, never raises)."""
|
|
76
|
-
ensure_h5_lock()
|
|
77
|
-
try:
|
|
78
|
-
import h5py
|
|
79
|
-
with h5py.File(path, "r", locking=False) as handle:
|
|
80
|
-
return "globals" in handle
|
|
81
|
-
except Exception:
|
|
82
|
-
return False
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
def shot_executed(path):
|
|
86
|
-
"""True/False when a shot shows acquisition data; None when unknown.
|
|
87
|
-
|
|
88
|
-
labscript compiles shot h5 files before BLACS executes them. A compiled but
|
|
89
|
-
not executed shot has no images/results yet; this is how a stalled queue
|
|
90
|
-
(for example BLACS paused) can be told apart from a normal run in progress.
|
|
91
|
-
"""
|
|
92
|
-
ensure_h5_lock()
|
|
93
|
-
try:
|
|
94
|
-
import h5py
|
|
95
|
-
with h5py.File(path, "r", locking=False) as handle:
|
|
96
|
-
if "images" in handle:
|
|
97
|
-
cameras = handle["images"]
|
|
98
|
-
if isinstance(cameras, h5py.Group):
|
|
99
|
-
for camera in cameras:
|
|
100
|
-
camera_group = cameras[camera]
|
|
101
|
-
if isinstance(camera_group, h5py.Group):
|
|
102
|
-
for label in camera_group:
|
|
103
|
-
label_group = camera_group[label]
|
|
104
|
-
if isinstance(label_group, h5py.Group) and len(label_group) > 0:
|
|
105
|
-
return True
|
|
106
|
-
if "results" in handle:
|
|
107
|
-
results = handle["results"]
|
|
108
|
-
if isinstance(results, h5py.Group):
|
|
109
|
-
for name in results:
|
|
110
|
-
group = results[name]
|
|
111
|
-
if isinstance(group, h5py.Group) and (len(group) > 0 or len(group.attrs) > 0):
|
|
112
|
-
return True
|
|
113
|
-
return False
|
|
114
|
-
except Exception:
|
|
115
|
-
return None
|
|
116
|
-
|
|
117
|
-
# ---------------------------------------------------------------- scan expr
|
|
118
|
-
|
|
119
|
-
_ALLOWED_CALLS = {"arange", "linspace", "array", "logspace"}
|
|
120
|
-
_ALLOWED_NODES = (
|
|
121
|
-
ast.Expression, ast.Call, ast.Attribute, ast.Name, ast.Load, ast.Constant,
|
|
122
|
-
ast.List, ast.Tuple, ast.UnaryOp, ast.USub, ast.UAdd, ast.BinOp, ast.Add,
|
|
123
|
-
ast.Sub, ast.Mult, ast.Div, ast.Mod, ast.Pow,
|
|
124
|
-
)
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
def _num_arg(node):
|
|
128
|
-
value = ast.literal_eval(node)
|
|
129
|
-
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
|
130
|
-
raise BridgeError("BAD_SCAN_EXPR", "scan expression argument is not numeric")
|
|
131
|
-
return value
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
def eval_scan_expr(expr):
|
|
135
|
-
"""Evaluate the small scan-expression subset used by protocols (np.arange...)."""
|
|
136
|
-
import numpy as np
|
|
137
|
-
if isinstance(expr, bool):
|
|
138
|
-
raise BridgeError("BAD_SCAN_EXPR", "scan expression cannot be a bool")
|
|
139
|
-
if isinstance(expr, (int, float)):
|
|
140
|
-
return np.atleast_1d(np.asarray(expr, dtype=float))
|
|
141
|
-
if isinstance(expr, (list, tuple)):
|
|
142
|
-
return np.asarray([float(v) for v in expr], dtype=float)
|
|
143
|
-
text = str(expr).strip()
|
|
144
|
-
if text == "":
|
|
145
|
-
raise BridgeError("BAD_SCAN_EXPR", "scan expression is empty")
|
|
146
|
-
try:
|
|
147
|
-
tree = ast.parse(text, mode="eval")
|
|
148
|
-
except SyntaxError as exc:
|
|
149
|
-
raise BridgeError("BAD_SCAN_EXPR", "cannot parse scan expression: " + str(exc))
|
|
150
|
-
for node in ast.walk(tree):
|
|
151
|
-
if not isinstance(node, _ALLOWED_NODES):
|
|
152
|
-
raise BridgeError("BAD_SCAN_EXPR", "scan expression node not allowed: " + type(node).__name__)
|
|
153
|
-
if isinstance(node, ast.Call):
|
|
154
|
-
if not isinstance(node.func, ast.Attribute):
|
|
155
|
-
raise BridgeError("BAD_SCAN_EXPR", "only np.* calls are allowed")
|
|
156
|
-
if not isinstance(node.func.value, ast.Name) or node.func.value.id != "np":
|
|
157
|
-
raise BridgeError("BAD_SCAN_EXPR", "only np.* calls are allowed")
|
|
158
|
-
if node.func.attr not in _ALLOWED_CALLS:
|
|
159
|
-
raise BridgeError("BAD_SCAN_EXPR", "np." + node.func.attr + " is not allowed")
|
|
160
|
-
for arg in node.args:
|
|
161
|
-
if isinstance(arg, ast.Constant):
|
|
162
|
-
_num_arg(arg)
|
|
163
|
-
elif isinstance(arg, (ast.List, ast.Tuple)):
|
|
164
|
-
for item in arg.elts:
|
|
165
|
-
if isinstance(item, ast.Constant):
|
|
166
|
-
_num_arg(item)
|
|
167
|
-
elif not isinstance(item, (ast.UnaryOp, ast.BinOp)):
|
|
168
|
-
raise BridgeError("BAD_SCAN_EXPR", "unsupported scan expression argument")
|
|
169
|
-
elif not isinstance(arg, (ast.UnaryOp, ast.BinOp)):
|
|
170
|
-
raise BridgeError("BAD_SCAN_EXPR", "unsupported scan expression argument")
|
|
171
|
-
env = {"np": np, "__builtins__": {}}
|
|
172
|
-
try:
|
|
173
|
-
value = eval(compile(tree, "<scan>", "eval"), env)
|
|
174
|
-
except Exception as exc:
|
|
175
|
-
raise BridgeError("BAD_SCAN_EXPR", "scan expression evaluation failed: " + str(exc))
|
|
176
|
-
return np.atleast_1d(np.asarray(value, dtype=float))
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
def eval_panel_value(raw):
|
|
180
|
-
"""Evaluate a raw panel string the way runmanager does (best effort)."""
|
|
181
|
-
import numpy as np
|
|
182
|
-
text = str(raw).strip()
|
|
183
|
-
if text == "":
|
|
184
|
-
return ""
|
|
185
|
-
if "np." in text or text.startswith("["):
|
|
186
|
-
try:
|
|
187
|
-
arr = eval_scan_expr(text)
|
|
188
|
-
return arr if arr.size > 1 else arr[0]
|
|
189
|
-
except BridgeError:
|
|
190
|
-
pass
|
|
191
|
-
try:
|
|
192
|
-
return ast.literal_eval(text)
|
|
193
|
-
except Exception:
|
|
194
|
-
return str(raw)
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
def raw_repr(value):
|
|
198
|
-
if isinstance(value, bool):
|
|
199
|
-
return "True" if value else "False"
|
|
200
|
-
if isinstance(value, (int, float)):
|
|
201
|
-
return repr(value)
|
|
202
|
-
if isinstance(value, str):
|
|
203
|
-
return repr(value)
|
|
204
|
-
if isinstance(value, (list, tuple)):
|
|
205
|
-
return repr(list(value))
|
|
206
|
-
return repr(value)
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
def values_to_raw(values):
|
|
210
|
-
"""Convert JSON values to runmanager raw expressions (arrays included)."""
|
|
211
|
-
out = {}
|
|
212
|
-
for key, value in values.items():
|
|
213
|
-
if isinstance(value, (list, tuple)):
|
|
214
|
-
out[str(key)] = scan_expr_from_value(list(value))
|
|
215
|
-
else:
|
|
216
|
-
out[str(key)] = raw_repr(value)
|
|
217
|
-
return out
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
def has_array_value(values):
|
|
221
|
-
return any(isinstance(value, (list, tuple)) for value in values.values())
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
def to_raw_values(values):
|
|
225
|
-
"""Stringify evaluated globals into runmanager raw expressions."""
|
|
226
|
-
out = {}
|
|
227
|
-
for key, value in values.items():
|
|
228
|
-
if isinstance(value, str):
|
|
229
|
-
out[str(key)] = value
|
|
230
|
-
elif isinstance(value, (list, tuple)):
|
|
231
|
-
out[str(key)] = "np.array([" + ",".join(repr(float(item)) for item in value) + "])"
|
|
232
|
-
else:
|
|
233
|
-
out[str(key)] = raw_repr(value)
|
|
234
|
-
return out
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
def as_number(value):
|
|
238
|
-
if isinstance(value, bool):
|
|
239
|
-
return None
|
|
240
|
-
if isinstance(value, (int, float)):
|
|
241
|
-
return float(value)
|
|
242
|
-
return None
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
""
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
return
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
return dict((k,
|
|
398
|
-
|
|
399
|
-
def
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
for
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
self._opener
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
except
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
raise BridgeError(
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
def
|
|
615
|
-
folder = history_dir(config)
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
if not
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
path
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
limits[str(name)] = dict(
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
if meta is None:
|
|
676
|
-
continue
|
|
677
|
-
|
|
678
|
-
if
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
if
|
|
682
|
-
findings.append({"kind": "
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
"
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
if
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
if
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
if
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
"
|
|
975
|
-
"
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
"
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
"
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
else:
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
return
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
def
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
"
|
|
1258
|
-
"
|
|
1259
|
-
"
|
|
1260
|
-
"
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
"
|
|
1272
|
-
"
|
|
1273
|
-
"
|
|
1274
|
-
"
|
|
1275
|
-
"
|
|
1276
|
-
"
|
|
1277
|
-
"
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
for
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1
|
+
"""dsh-labscript bridge: one JSON request on stdin, one JSON envelope on stdout.
|
|
2
|
+
|
|
3
|
+
commands: status, set, engage, wait, abort, data, restore,
|
|
4
|
+
protocol_plan, protocol_start, protocol_finalize, progress
|
|
5
|
+
|
|
6
|
+
The node plugin spawns this script with one python interpreter configured by the
|
|
7
|
+
user; it must be able to import numpy and h5py (the labscript environment has
|
|
8
|
+
both). The labscript backend additionally imports runmanager.remote lazily.
|
|
9
|
+
"""
|
|
10
|
+
import ast
|
|
11
|
+
import glob
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import sys
|
|
15
|
+
import time
|
|
16
|
+
import datetime
|
|
17
|
+
import importlib
|
|
18
|
+
import traceback
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class BridgeError(Exception):
|
|
22
|
+
def __init__(self, code, message, detail=None):
|
|
23
|
+
Exception.__init__(self, message)
|
|
24
|
+
self.code = code
|
|
25
|
+
self.message = message
|
|
26
|
+
self.detail = detail or {}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def now_iso():
|
|
30
|
+
return datetime.datetime.now().astimezone().isoformat(timespec="seconds")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def expand(path):
|
|
34
|
+
if not path:
|
|
35
|
+
return path
|
|
36
|
+
return os.path.expanduser(str(path))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def jsonable(value):
|
|
40
|
+
if hasattr(value, "tolist"):
|
|
41
|
+
return jsonable(value.tolist())
|
|
42
|
+
if hasattr(value, "item"):
|
|
43
|
+
return jsonable(value.item())
|
|
44
|
+
if isinstance(value, dict):
|
|
45
|
+
return dict((str(k), jsonable(v)) for k, v in value.items())
|
|
46
|
+
if isinstance(value, (list, tuple)):
|
|
47
|
+
return [jsonable(v) for v in value]
|
|
48
|
+
if isinstance(value, float):
|
|
49
|
+
if value != value or value in (float("inf"), float("-inf")):
|
|
50
|
+
return None
|
|
51
|
+
return value
|
|
52
|
+
if isinstance(value, (str, int, bool)) or value is None:
|
|
53
|
+
return value
|
|
54
|
+
return str(value)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
_H5_LOCK_LOADED = False
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def ensure_h5_lock():
|
|
62
|
+
"""Import labscript_utils.h5_lock before h5py (labscript requirement)."""
|
|
63
|
+
global _H5_LOCK_LOADED
|
|
64
|
+
if _H5_LOCK_LOADED:
|
|
65
|
+
return True
|
|
66
|
+
try:
|
|
67
|
+
import labscript_utils.h5_lock # noqa: F401
|
|
68
|
+
_H5_LOCK_LOADED = True
|
|
69
|
+
return True
|
|
70
|
+
except Exception:
|
|
71
|
+
return False
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def shot_ready(path):
|
|
75
|
+
"""True when a shot h5 is complete enough to read (best effort, never raises)."""
|
|
76
|
+
ensure_h5_lock()
|
|
77
|
+
try:
|
|
78
|
+
import h5py
|
|
79
|
+
with h5py.File(path, "r", locking=False) as handle:
|
|
80
|
+
return "globals" in handle
|
|
81
|
+
except Exception:
|
|
82
|
+
return False
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def shot_executed(path):
|
|
86
|
+
"""True/False when a shot shows acquisition data; None when unknown.
|
|
87
|
+
|
|
88
|
+
labscript compiles shot h5 files before BLACS executes them. A compiled but
|
|
89
|
+
not executed shot has no images/results yet; this is how a stalled queue
|
|
90
|
+
(for example BLACS paused) can be told apart from a normal run in progress.
|
|
91
|
+
"""
|
|
92
|
+
ensure_h5_lock()
|
|
93
|
+
try:
|
|
94
|
+
import h5py
|
|
95
|
+
with h5py.File(path, "r", locking=False) as handle:
|
|
96
|
+
if "images" in handle:
|
|
97
|
+
cameras = handle["images"]
|
|
98
|
+
if isinstance(cameras, h5py.Group):
|
|
99
|
+
for camera in cameras:
|
|
100
|
+
camera_group = cameras[camera]
|
|
101
|
+
if isinstance(camera_group, h5py.Group):
|
|
102
|
+
for label in camera_group:
|
|
103
|
+
label_group = camera_group[label]
|
|
104
|
+
if isinstance(label_group, h5py.Group) and len(label_group) > 0:
|
|
105
|
+
return True
|
|
106
|
+
if "results" in handle:
|
|
107
|
+
results = handle["results"]
|
|
108
|
+
if isinstance(results, h5py.Group):
|
|
109
|
+
for name in results:
|
|
110
|
+
group = results[name]
|
|
111
|
+
if isinstance(group, h5py.Group) and (len(group) > 0 or len(group.attrs) > 0):
|
|
112
|
+
return True
|
|
113
|
+
return False
|
|
114
|
+
except Exception:
|
|
115
|
+
return None
|
|
116
|
+
|
|
117
|
+
# ---------------------------------------------------------------- scan expr
|
|
118
|
+
|
|
119
|
+
_ALLOWED_CALLS = {"arange", "linspace", "array", "logspace"}
|
|
120
|
+
_ALLOWED_NODES = (
|
|
121
|
+
ast.Expression, ast.Call, ast.Attribute, ast.Name, ast.Load, ast.Constant,
|
|
122
|
+
ast.List, ast.Tuple, ast.UnaryOp, ast.USub, ast.UAdd, ast.BinOp, ast.Add,
|
|
123
|
+
ast.Sub, ast.Mult, ast.Div, ast.Mod, ast.Pow,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _num_arg(node):
|
|
128
|
+
value = ast.literal_eval(node)
|
|
129
|
+
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
|
130
|
+
raise BridgeError("BAD_SCAN_EXPR", "scan expression argument is not numeric")
|
|
131
|
+
return value
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def eval_scan_expr(expr):
|
|
135
|
+
"""Evaluate the small scan-expression subset used by protocols (np.arange...)."""
|
|
136
|
+
import numpy as np
|
|
137
|
+
if isinstance(expr, bool):
|
|
138
|
+
raise BridgeError("BAD_SCAN_EXPR", "scan expression cannot be a bool")
|
|
139
|
+
if isinstance(expr, (int, float)):
|
|
140
|
+
return np.atleast_1d(np.asarray(expr, dtype=float))
|
|
141
|
+
if isinstance(expr, (list, tuple)):
|
|
142
|
+
return np.asarray([float(v) for v in expr], dtype=float)
|
|
143
|
+
text = str(expr).strip()
|
|
144
|
+
if text == "":
|
|
145
|
+
raise BridgeError("BAD_SCAN_EXPR", "scan expression is empty")
|
|
146
|
+
try:
|
|
147
|
+
tree = ast.parse(text, mode="eval")
|
|
148
|
+
except SyntaxError as exc:
|
|
149
|
+
raise BridgeError("BAD_SCAN_EXPR", "cannot parse scan expression: " + str(exc))
|
|
150
|
+
for node in ast.walk(tree):
|
|
151
|
+
if not isinstance(node, _ALLOWED_NODES):
|
|
152
|
+
raise BridgeError("BAD_SCAN_EXPR", "scan expression node not allowed: " + type(node).__name__)
|
|
153
|
+
if isinstance(node, ast.Call):
|
|
154
|
+
if not isinstance(node.func, ast.Attribute):
|
|
155
|
+
raise BridgeError("BAD_SCAN_EXPR", "only np.* calls are allowed")
|
|
156
|
+
if not isinstance(node.func.value, ast.Name) or node.func.value.id != "np":
|
|
157
|
+
raise BridgeError("BAD_SCAN_EXPR", "only np.* calls are allowed")
|
|
158
|
+
if node.func.attr not in _ALLOWED_CALLS:
|
|
159
|
+
raise BridgeError("BAD_SCAN_EXPR", "np." + node.func.attr + " is not allowed")
|
|
160
|
+
for arg in node.args:
|
|
161
|
+
if isinstance(arg, ast.Constant):
|
|
162
|
+
_num_arg(arg)
|
|
163
|
+
elif isinstance(arg, (ast.List, ast.Tuple)):
|
|
164
|
+
for item in arg.elts:
|
|
165
|
+
if isinstance(item, ast.Constant):
|
|
166
|
+
_num_arg(item)
|
|
167
|
+
elif not isinstance(item, (ast.UnaryOp, ast.BinOp)):
|
|
168
|
+
raise BridgeError("BAD_SCAN_EXPR", "unsupported scan expression argument")
|
|
169
|
+
elif not isinstance(arg, (ast.UnaryOp, ast.BinOp)):
|
|
170
|
+
raise BridgeError("BAD_SCAN_EXPR", "unsupported scan expression argument")
|
|
171
|
+
env = {"np": np, "__builtins__": {}}
|
|
172
|
+
try:
|
|
173
|
+
value = eval(compile(tree, "<scan>", "eval"), env)
|
|
174
|
+
except Exception as exc:
|
|
175
|
+
raise BridgeError("BAD_SCAN_EXPR", "scan expression evaluation failed: " + str(exc))
|
|
176
|
+
return np.atleast_1d(np.asarray(value, dtype=float))
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def eval_panel_value(raw):
|
|
180
|
+
"""Evaluate a raw panel string the way runmanager does (best effort)."""
|
|
181
|
+
import numpy as np
|
|
182
|
+
text = str(raw).strip()
|
|
183
|
+
if text == "":
|
|
184
|
+
return ""
|
|
185
|
+
if "np." in text or text.startswith("["):
|
|
186
|
+
try:
|
|
187
|
+
arr = eval_scan_expr(text)
|
|
188
|
+
return arr if arr.size > 1 else arr[0]
|
|
189
|
+
except BridgeError:
|
|
190
|
+
pass
|
|
191
|
+
try:
|
|
192
|
+
return ast.literal_eval(text)
|
|
193
|
+
except Exception:
|
|
194
|
+
return str(raw)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def raw_repr(value):
|
|
198
|
+
if isinstance(value, bool):
|
|
199
|
+
return "True" if value else "False"
|
|
200
|
+
if isinstance(value, (int, float)):
|
|
201
|
+
return repr(value)
|
|
202
|
+
if isinstance(value, str):
|
|
203
|
+
return repr(value)
|
|
204
|
+
if isinstance(value, (list, tuple)):
|
|
205
|
+
return repr(list(value))
|
|
206
|
+
return repr(value)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def values_to_raw(values):
|
|
210
|
+
"""Convert JSON values to runmanager raw expressions (arrays included)."""
|
|
211
|
+
out = {}
|
|
212
|
+
for key, value in values.items():
|
|
213
|
+
if isinstance(value, (list, tuple)):
|
|
214
|
+
out[str(key)] = scan_expr_from_value(list(value))
|
|
215
|
+
else:
|
|
216
|
+
out[str(key)] = raw_repr(value)
|
|
217
|
+
return out
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def has_array_value(values):
|
|
221
|
+
return any(isinstance(value, (list, tuple)) for value in values.values())
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def to_raw_values(values):
|
|
225
|
+
"""Stringify evaluated globals into runmanager raw expressions."""
|
|
226
|
+
out = {}
|
|
227
|
+
for key, value in values.items():
|
|
228
|
+
if isinstance(value, str):
|
|
229
|
+
out[str(key)] = value
|
|
230
|
+
elif isinstance(value, (list, tuple)):
|
|
231
|
+
out[str(key)] = "np.array([" + ",".join(repr(float(item)) for item in value) + "])"
|
|
232
|
+
else:
|
|
233
|
+
out[str(key)] = raw_repr(value)
|
|
234
|
+
return out
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def as_number(value):
|
|
238
|
+
if isinstance(value, bool):
|
|
239
|
+
return None
|
|
240
|
+
if isinstance(value, (int, float)):
|
|
241
|
+
return float(value)
|
|
242
|
+
return None
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def as_limit_value(value):
|
|
246
|
+
"""Numeric value for hard-limit checks; booleans count as 0/1."""
|
|
247
|
+
if isinstance(value, bool):
|
|
248
|
+
return 1.0 if value else 0.0
|
|
249
|
+
return as_number(value)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
# ---------------------------------------------------------------- backends
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def map_path(config, path):
|
|
256
|
+
if not path:
|
|
257
|
+
return path
|
|
258
|
+
text = str(path)
|
|
259
|
+
for rule in config.get("pathMap") or []:
|
|
260
|
+
remote = rule.get("remotePrefix") or ""
|
|
261
|
+
local = rule.get("localPrefix") or ""
|
|
262
|
+
if remote and local and text.startswith(remote):
|
|
263
|
+
return local + text[len(remote):]
|
|
264
|
+
return text
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
class LabscriptBackend(object):
|
|
268
|
+
def __init__(self, config):
|
|
269
|
+
self.config = config
|
|
270
|
+
self._client = None
|
|
271
|
+
|
|
272
|
+
def client(self):
|
|
273
|
+
if self._client is not None:
|
|
274
|
+
return self._client
|
|
275
|
+
try:
|
|
276
|
+
from labscript_utils import h5_lock # noqa: F401 (must precede h5py)
|
|
277
|
+
except Exception:
|
|
278
|
+
pass
|
|
279
|
+
try:
|
|
280
|
+
from runmanager.remote import Client
|
|
281
|
+
except Exception as exc:
|
|
282
|
+
raise BridgeError("BACKEND_UNAVAILABLE", "labscript python environment not importable: " + str(exc))
|
|
283
|
+
rm = self.config.get("runmanager") or {}
|
|
284
|
+
host = rm.get("host") or "127.0.0.1"
|
|
285
|
+
port = int(rm.get("port") or 42523)
|
|
286
|
+
timeout = float(rm.get("timeoutS") or 10)
|
|
287
|
+
try:
|
|
288
|
+
client = Client(host=host, port=port, timeout=timeout)
|
|
289
|
+
client.say_hello()
|
|
290
|
+
except Exception as exc:
|
|
291
|
+
raise BridgeError("BACKEND_UNAVAILABLE", "runmanager not reachable at " + host + ":" + str(port) + ": " + str(exc))
|
|
292
|
+
self._client = client
|
|
293
|
+
return client
|
|
294
|
+
|
|
295
|
+
def version(self):
|
|
296
|
+
try:
|
|
297
|
+
return str(self.client().get_version())
|
|
298
|
+
except Exception:
|
|
299
|
+
return None
|
|
300
|
+
|
|
301
|
+
def get_globals(self, raw=False):
|
|
302
|
+
return dict(self.client().get_globals(raw=raw))
|
|
303
|
+
|
|
304
|
+
def set_globals(self, values, raw=False):
|
|
305
|
+
return dict(self.client().set_globals(values, raw=raw))
|
|
306
|
+
|
|
307
|
+
def n_shots(self):
|
|
308
|
+
return int(self.client().n_shots())
|
|
309
|
+
|
|
310
|
+
def error_in_globals(self):
|
|
311
|
+
return bool(self.client().error_in_globals())
|
|
312
|
+
|
|
313
|
+
def run_shots(self):
|
|
314
|
+
try:
|
|
315
|
+
return bool(self.client().get_run_shots())
|
|
316
|
+
except Exception:
|
|
317
|
+
return None
|
|
318
|
+
|
|
319
|
+
def labscript_file(self):
|
|
320
|
+
try:
|
|
321
|
+
return self.client().get_labscript_file()
|
|
322
|
+
except Exception:
|
|
323
|
+
return None
|
|
324
|
+
|
|
325
|
+
def shot_output_folder(self):
|
|
326
|
+
try:
|
|
327
|
+
return self.client().get_shot_output_folder()
|
|
328
|
+
except Exception:
|
|
329
|
+
return None
|
|
330
|
+
|
|
331
|
+
def engage(self):
|
|
332
|
+
# runmanager advances the sequence index on engage: capture the folder first.
|
|
333
|
+
run_folder = self.shot_output_folder()
|
|
334
|
+
n_shots = self.n_shots()
|
|
335
|
+
try:
|
|
336
|
+
self.client().engage()
|
|
337
|
+
except Exception as exc:
|
|
338
|
+
raise BridgeError("ENGAGE_REJECTED", "runmanager engage failed: " + type(exc).__name__ + ": " + str(exc))
|
|
339
|
+
return {"accepted": True, "n_shots": n_shots, "run_folder": run_folder, "mode": "runmanager"}
|
|
340
|
+
|
|
341
|
+
def abort(self):
|
|
342
|
+
return self.client().abort()
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
MOCK_DEFAULT_PANEL = {
|
|
346
|
+
"do_acquire": True,
|
|
347
|
+
"duration_test_us": 1.0,
|
|
348
|
+
"amplitude": 0.5,
|
|
349
|
+
"dummy_iterator": 0,
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
class MockBackend(object):
|
|
354
|
+
"""In-memory simulator used for tests, examples and hardware-free development."""
|
|
355
|
+
|
|
356
|
+
def __init__(self, config):
|
|
357
|
+
self.config = config
|
|
358
|
+
mock = config.get("mock") or {}
|
|
359
|
+
self.state_dir = os.path.abspath(expand(mock.get("stateDir") or "~/.dsh-labscript/mock"))
|
|
360
|
+
os.makedirs(self.state_dir, exist_ok=True)
|
|
361
|
+
self.panel_path = os.path.join(self.state_dir, "panel.json")
|
|
362
|
+
self.state_path = os.path.join(self.state_dir, "state.json")
|
|
363
|
+
self.runs_dir = os.path.join(self.state_dir, "runs")
|
|
364
|
+
os.makedirs(self.runs_dir, exist_ok=True)
|
|
365
|
+
|
|
366
|
+
# ---- state helpers
|
|
367
|
+
def read_panel(self):
|
|
368
|
+
if os.path.isfile(self.panel_path):
|
|
369
|
+
with open(self.panel_path, "r", encoding="utf-8") as handle:
|
|
370
|
+
data = json.load(handle)
|
|
371
|
+
else:
|
|
372
|
+
data = dict((k, raw_repr(v)) for k, v in MOCK_DEFAULT_PANEL.items())
|
|
373
|
+
return data
|
|
374
|
+
|
|
375
|
+
def write_panel(self, raw_panel):
|
|
376
|
+
with open(self.panel_path, "w", encoding="utf-8") as handle:
|
|
377
|
+
json.dump(raw_panel, handle, indent=1)
|
|
378
|
+
|
|
379
|
+
def next_run_folder(self):
|
|
380
|
+
state = {}
|
|
381
|
+
if os.path.isfile(self.state_path):
|
|
382
|
+
with open(self.state_path, "r", encoding="utf-8") as handle:
|
|
383
|
+
state = json.load(handle)
|
|
384
|
+
index = int(state.get("last_run") or 0) + 1
|
|
385
|
+
state["last_run"] = index
|
|
386
|
+
with open(self.state_path, "w", encoding="utf-8") as handle:
|
|
387
|
+
json.dump(state, handle)
|
|
388
|
+
folder = os.path.join(self.runs_dir, "run_" + ("%06d" % index))
|
|
389
|
+
os.makedirs(folder, exist_ok=True)
|
|
390
|
+
return folder
|
|
391
|
+
|
|
392
|
+
# ---- backend interface
|
|
393
|
+
def get_globals(self, raw=False):
|
|
394
|
+
panel = self.read_panel()
|
|
395
|
+
if raw:
|
|
396
|
+
return dict(panel)
|
|
397
|
+
return dict((k, jsonable(eval_panel_value(v))) for k, v in panel.items())
|
|
398
|
+
|
|
399
|
+
def set_globals(self, values, raw=False):
|
|
400
|
+
panel = self.read_panel()
|
|
401
|
+
for key, value in values.items():
|
|
402
|
+
panel[str(key)] = str(value) if raw else raw_repr(value)
|
|
403
|
+
self.write_panel(panel)
|
|
404
|
+
return dict((k, panel[k]) for k in values)
|
|
405
|
+
|
|
406
|
+
def n_shots(self):
|
|
407
|
+
import numpy as np
|
|
408
|
+
total = 1
|
|
409
|
+
for value in self.get_globals(raw=False).values():
|
|
410
|
+
arr = np.asarray(value) if hasattr(value, "__len__") and not isinstance(value, str) else None
|
|
411
|
+
if arr is None or arr.ndim == 0:
|
|
412
|
+
continue
|
|
413
|
+
total *= max(1, int(arr.size))
|
|
414
|
+
return int(total)
|
|
415
|
+
|
|
416
|
+
def error_in_globals(self):
|
|
417
|
+
return False
|
|
418
|
+
|
|
419
|
+
def run_shots(self):
|
|
420
|
+
return True
|
|
421
|
+
|
|
422
|
+
def labscript_file(self):
|
|
423
|
+
return "mock_sequence.py"
|
|
424
|
+
|
|
425
|
+
def shot_output_folder(self):
|
|
426
|
+
return self.runs_dir
|
|
427
|
+
|
|
428
|
+
def engage(self):
|
|
429
|
+
import h5py
|
|
430
|
+
import numpy as np
|
|
431
|
+
values = self.get_globals(raw=False)
|
|
432
|
+
arrays = {}
|
|
433
|
+
for key, value in values.items():
|
|
434
|
+
if isinstance(value, str):
|
|
435
|
+
continue
|
|
436
|
+
arr = np.asarray(value)
|
|
437
|
+
if arr.ndim >= 1 and arr.size > 1:
|
|
438
|
+
arrays[key] = arr
|
|
439
|
+
total = 1
|
|
440
|
+
for arr in arrays.values():
|
|
441
|
+
total *= int(arr.size)
|
|
442
|
+
folder = self.next_run_folder()
|
|
443
|
+
for index in range(total):
|
|
444
|
+
attrs = {}
|
|
445
|
+
for key, value in values.items():
|
|
446
|
+
if key in arrays:
|
|
447
|
+
arr = arrays[key]
|
|
448
|
+
attrs[key] = arr.flat[index % arr.size]
|
|
449
|
+
elif isinstance(value, str):
|
|
450
|
+
attrs[key] = value
|
|
451
|
+
else:
|
|
452
|
+
attrs[key] = value
|
|
453
|
+
shot_path = os.path.join(folder, "shot_" + ("%04d" % index) + ".h5")
|
|
454
|
+
with h5py.File(shot_path, "w") as handle:
|
|
455
|
+
group = handle.create_group("globals")
|
|
456
|
+
for key, value in attrs.items():
|
|
457
|
+
group.attrs[key] = value
|
|
458
|
+
results = handle.create_group("results/demo")
|
|
459
|
+
results.attrs["value"] = float(index)
|
|
460
|
+
results.create_dataset("mean", data=float(index))
|
|
461
|
+
return {"accepted": True, "n_shots": total, "run_folder": folder, "mode": "mock-labscript"}
|
|
462
|
+
|
|
463
|
+
def abort(self):
|
|
464
|
+
return {"aborted": True, "mode": "mock-labscript"}
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
class HttpSimBackend(object):
|
|
468
|
+
"""HTTP client for an optional local simulator service (development backend).
|
|
469
|
+
|
|
470
|
+
The simulator must speak the documented JSON/HTTP contract on a loopback address.
|
|
471
|
+
It is a development tool, not a physics backend for publication.
|
|
472
|
+
"""
|
|
473
|
+
|
|
474
|
+
def __init__(self, config):
|
|
475
|
+
simulator = config.get("simulator") or {}
|
|
476
|
+
url = str(simulator.get("url") or "http://127.0.0.1:18761").rstrip("/")
|
|
477
|
+
if not url.startswith("http://127.0.0.1:"):
|
|
478
|
+
raise BridgeError("BAD_CONFIG", "simulator.url must be http://127.0.0.1:<port>, got: " + url)
|
|
479
|
+
self.base = url
|
|
480
|
+
self.timeout = float(simulator.get("timeoutS") or 10)
|
|
481
|
+
self._opener = None
|
|
482
|
+
|
|
483
|
+
def opener(self):
|
|
484
|
+
import urllib.request
|
|
485
|
+
if self._opener is not None:
|
|
486
|
+
return self._opener
|
|
487
|
+
|
|
488
|
+
class NoRedirect(urllib.request.HTTPRedirectHandler):
|
|
489
|
+
def redirect_request(self, req, fp, code, msg, headers, newurl):
|
|
490
|
+
return None
|
|
491
|
+
|
|
492
|
+
self._opener = urllib.request.build_opener(NoRedirect, urllib.request.ProxyHandler({}))
|
|
493
|
+
return self._opener
|
|
494
|
+
|
|
495
|
+
def request(self, method, path, body=None):
|
|
496
|
+
import urllib.request
|
|
497
|
+
import urllib.error
|
|
498
|
+
data = None
|
|
499
|
+
headers = {}
|
|
500
|
+
if body is not None:
|
|
501
|
+
data = json.dumps(jsonable(body)).encode("utf-8")
|
|
502
|
+
headers["Content-Type"] = "application/json"
|
|
503
|
+
req = urllib.request.Request(self.base + path, data=data, headers=headers, method=method)
|
|
504
|
+
try:
|
|
505
|
+
with self.opener().open(req, timeout=self.timeout) as response:
|
|
506
|
+
raw = response.read().decode("utf-8")
|
|
507
|
+
except urllib.error.HTTPError as exc:
|
|
508
|
+
try:
|
|
509
|
+
payload = json.loads(exc.read().decode("utf-8"))
|
|
510
|
+
except Exception:
|
|
511
|
+
raise BridgeError("BACKEND_UNAVAILABLE", "http-sim HTTP " + str(exc.code) + " at " + path)
|
|
512
|
+
error = payload.get("error") or {}
|
|
513
|
+
raise BridgeError(str(error.get("code") or "BACKEND_UNAVAILABLE"), str(error.get("message") or "http-sim error"))
|
|
514
|
+
except Exception as exc:
|
|
515
|
+
raise BridgeError("BACKEND_UNAVAILABLE", "http-sim not reachable at " + self.base + ": " + str(exc))
|
|
516
|
+
try:
|
|
517
|
+
envelope = json.loads(raw)
|
|
518
|
+
except Exception:
|
|
519
|
+
raise BridgeError("BACKEND_UNAVAILABLE", "http-sim response is not JSON")
|
|
520
|
+
if envelope.get("ok") is not True:
|
|
521
|
+
error = envelope.get("error") or {}
|
|
522
|
+
raise BridgeError(str(error.get("code") or "BACKEND_UNAVAILABLE"), str(error.get("message") or "http-sim error"))
|
|
523
|
+
return envelope.get("data") or {}
|
|
524
|
+
|
|
525
|
+
def snapshot(self):
|
|
526
|
+
return self.request("GET", "/v1/rm/snapshot")
|
|
527
|
+
|
|
528
|
+
def get_globals(self, raw=False):
|
|
529
|
+
snap = self.snapshot()
|
|
530
|
+
key = "raw_globals" if raw else "globals"
|
|
531
|
+
return dict(snap.get(key) or {})
|
|
532
|
+
|
|
533
|
+
def set_globals(self, values, raw=False):
|
|
534
|
+
body = dict((k, v.tolist() if hasattr(v, "tolist") else v) for k, v in values.items())
|
|
535
|
+
data = self.request("POST", "/v1/rm/set_globals", {"values": body, "raw": bool(raw)})
|
|
536
|
+
return dict(data.get("written") or {})
|
|
537
|
+
|
|
538
|
+
def n_shots(self):
|
|
539
|
+
return int(self.snapshot().get("n_shots") or 0)
|
|
540
|
+
|
|
541
|
+
def error_in_globals(self):
|
|
542
|
+
return bool(self.snapshot().get("error_in_globals"))
|
|
543
|
+
|
|
544
|
+
def run_shots(self):
|
|
545
|
+
return True
|
|
546
|
+
|
|
547
|
+
def labscript_file(self):
|
|
548
|
+
return self.snapshot().get("labscript_file")
|
|
549
|
+
|
|
550
|
+
def shot_output_folder(self):
|
|
551
|
+
return self.snapshot().get("shot_output_folder")
|
|
552
|
+
|
|
553
|
+
def engage(self):
|
|
554
|
+
data = self.request("POST", "/v1/rm/engage", {})
|
|
555
|
+
if data.get("accepted") is not True:
|
|
556
|
+
raise BridgeError("ENGAGE_REJECTED", "http-sim did not accept engage: " + str(data.get("detail") or ""))
|
|
557
|
+
return {"accepted": True, "n_shots": data.get("n_shots"), "run_folder": data.get("run_folder"), "mode": "http-sim"}
|
|
558
|
+
|
|
559
|
+
def abort(self):
|
|
560
|
+
data = self.request("POST", "/v1/blacs/stop", {})
|
|
561
|
+
return {"aborted": True, "via": "http-sim/blacs/stop", "result": data}
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
def make_backend(config):
|
|
565
|
+
backend = (config.get("backend") or "mock-labscript").lower()
|
|
566
|
+
if backend == "mock-labscript":
|
|
567
|
+
return MockBackend(config)
|
|
568
|
+
if backend == "http-sim":
|
|
569
|
+
return HttpSimBackend(config)
|
|
570
|
+
if backend == "labscript":
|
|
571
|
+
return LabscriptBackend(config)
|
|
572
|
+
raise BridgeError("BAD_CONFIG", "unknown backend: " + backend)
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
# ---------------------------------------------------------------- snapshots / logs
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
def history_dir(config):
|
|
579
|
+
mock = config.get("mock") or {}
|
|
580
|
+
base = config.get("panelHistoryDir") or os.path.join(expand("~/.dsh-labscript"), "panel_history")
|
|
581
|
+
if (config.get("backend") or "").lower() == "mock-labscript" and mock.get("historyDir"):
|
|
582
|
+
base = mock["historyDir"]
|
|
583
|
+
path = os.path.abspath(expand(base))
|
|
584
|
+
os.makedirs(path, exist_ok=True)
|
|
585
|
+
return path
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
def snapshot_panel(backend, config, tool, args, touched=None):
|
|
589
|
+
panel = backend.get_globals(raw=True)
|
|
590
|
+
folder = history_dir(config)
|
|
591
|
+
stamp = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
|
|
592
|
+
name = stamp + "-" + tool + "-" + os.urandom(3).hex() + ".json"
|
|
593
|
+
path = os.path.join(folder, name)
|
|
594
|
+
with open(path, "w", encoding="utf-8") as handle:
|
|
595
|
+
json.dump({
|
|
596
|
+
"ts": now_iso(),
|
|
597
|
+
"tool": tool,
|
|
598
|
+
"args": jsonable(args),
|
|
599
|
+
"backend": config.get("backend"),
|
|
600
|
+
"raw_globals": jsonable(panel),
|
|
601
|
+
"globals": jsonable(backend.get_globals(raw=False)),
|
|
602
|
+
"touched": sorted(str(key) for key in touched) if touched is not None else None,
|
|
603
|
+
}, handle, indent=1)
|
|
604
|
+
prune_keep = 200
|
|
605
|
+
files = sorted(glob.glob(os.path.join(folder, "*.json")))
|
|
606
|
+
for old in files[:-prune_keep]:
|
|
607
|
+
try:
|
|
608
|
+
os.remove(old)
|
|
609
|
+
except OSError:
|
|
610
|
+
pass
|
|
611
|
+
return {"file": path, "name": name, "dir": folder}
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
def append_event(config, event):
|
|
615
|
+
folder = history_dir(config)
|
|
616
|
+
path = os.path.join(folder, "events.jsonl")
|
|
617
|
+
with open(path, "a", encoding="utf-8") as handle:
|
|
618
|
+
handle.write(json.dumps(jsonable(event), ensure_ascii=False) + "\n")
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
def load_snapshot(config, name):
|
|
622
|
+
folder = history_dir(config)
|
|
623
|
+
if not name or name == "latest":
|
|
624
|
+
files = sorted(glob.glob(os.path.join(folder, "*.json")))
|
|
625
|
+
if not files:
|
|
626
|
+
raise BridgeError("NO_SNAPSHOT", "no panel snapshot found in " + folder)
|
|
627
|
+
path = files[-1]
|
|
628
|
+
else:
|
|
629
|
+
path = name if os.path.isabs(name) else os.path.join(folder, name)
|
|
630
|
+
if not os.path.isfile(path):
|
|
631
|
+
raise BridgeError("NO_SNAPSHOT", "snapshot not found: " + str(path))
|
|
632
|
+
with open(path, "r", encoding="utf-8") as handle:
|
|
633
|
+
data = json.load(handle)
|
|
634
|
+
return path, data
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
# ---------------------------------------------------------------- hard limits
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
def load_limits(config):
|
|
641
|
+
path = config.get("hardLimitsFile")
|
|
642
|
+
if not path:
|
|
643
|
+
return None
|
|
644
|
+
path = os.path.abspath(expand(path))
|
|
645
|
+
if not os.path.isfile(path):
|
|
646
|
+
raise BridgeError("HARD_LIMITS_UNREADABLE", "hardLimitsFile does not exist: " + path)
|
|
647
|
+
try:
|
|
648
|
+
with open(path, "r", encoding="utf-8") as handle:
|
|
649
|
+
doc = json.load(handle)
|
|
650
|
+
except Exception as exc:
|
|
651
|
+
raise BridgeError("HARD_LIMITS_UNREADABLE", "cannot read hardLimitsFile: " + str(exc))
|
|
652
|
+
limits = {}
|
|
653
|
+
for name, meta in (doc.get("limits") or {}).items():
|
|
654
|
+
if isinstance(meta, dict):
|
|
655
|
+
limits[str(name)] = dict(meta)
|
|
656
|
+
for channel, meta in (doc.get("channels") or {}).items():
|
|
657
|
+
if not isinstance(meta, dict):
|
|
658
|
+
continue
|
|
659
|
+
expanded = dict((k, v) for k, v in meta.items() if k != "globals")
|
|
660
|
+
expanded["channel"] = channel
|
|
661
|
+
for name in meta.get("globals") or []:
|
|
662
|
+
limits[str(name)] = dict(expanded)
|
|
663
|
+
return {"doc": doc, "limits": limits}
|
|
664
|
+
|
|
665
|
+
|
|
666
|
+
def check_limits(config, set_values, scan_values, expected_n_shots):
|
|
667
|
+
loaded = load_limits(config)
|
|
668
|
+
if loaded is None:
|
|
669
|
+
return {"checked": False, "warning": "hardLimitsFile not configured"}
|
|
670
|
+
findings = []
|
|
671
|
+
limits = loaded["limits"]
|
|
672
|
+
for key, raw in (set_values or {}).items():
|
|
673
|
+
meta = limits.get(key)
|
|
674
|
+
number = as_limit_value(raw)
|
|
675
|
+
if meta is None or number is None:
|
|
676
|
+
continue
|
|
677
|
+
minimum = as_number(meta.get("min"))
|
|
678
|
+
if minimum is not None and number < minimum:
|
|
679
|
+
findings.append({"kind": "under_min", "key": key, "value": number, "min": minimum})
|
|
680
|
+
maximum = as_number(meta.get("max"))
|
|
681
|
+
if maximum is not None and number > maximum:
|
|
682
|
+
findings.append({"kind": "over_max", "key": key, "value": number, "max": maximum})
|
|
683
|
+
for key, expr in (scan_values or {}).items():
|
|
684
|
+
meta = limits.get(key)
|
|
685
|
+
if meta is None:
|
|
686
|
+
continue
|
|
687
|
+
minimum = as_number(meta.get("min"))
|
|
688
|
+
maximum = as_number(meta.get("max"))
|
|
689
|
+
if minimum is None and maximum is None:
|
|
690
|
+
continue
|
|
691
|
+
arr = eval_scan_expr(expr)
|
|
692
|
+
if not arr.size:
|
|
693
|
+
continue
|
|
694
|
+
if minimum is not None and float(arr.min()) < minimum:
|
|
695
|
+
findings.append({"kind": "under_min_scan", "key": key, "min_in_scan": float(arr.min()), "min": minimum})
|
|
696
|
+
if maximum is not None and float(arr.max()) > maximum:
|
|
697
|
+
findings.append({"kind": "over_max_scan", "key": key, "max_in_scan": float(arr.max()), "max": maximum})
|
|
698
|
+
doc = loaded["doc"]
|
|
699
|
+
max_shots = None
|
|
700
|
+
run_limits = doc.get("run_limits") or {}
|
|
701
|
+
if isinstance(run_limits, dict):
|
|
702
|
+
max_shots = run_limits.get("maxTotalShots") or run_limits.get("max_total_shots")
|
|
703
|
+
if isinstance(max_shots, int) and expected_n_shots is not None and expected_n_shots > max_shots:
|
|
704
|
+
findings.append({"kind": "over_max_total_shots", "expected": expected_n_shots, "max": max_shots})
|
|
705
|
+
if findings:
|
|
706
|
+
raise BridgeError("HARD_LIMIT_EXCEEDED", "hard limit exceeded", {"findings": findings})
|
|
707
|
+
return {"checked": True, "findings": []}
|
|
708
|
+
|
|
709
|
+
|
|
710
|
+
# ---------------------------------------------------------------- postprocess
|
|
711
|
+
|
|
712
|
+
|
|
713
|
+
def run_postprocess(config, plan, run_folder, scan_global, reps):
|
|
714
|
+
name = plan.get("postprocess")
|
|
715
|
+
if not name:
|
|
716
|
+
return None
|
|
717
|
+
spec = config.get("postprocess") or {}
|
|
718
|
+
entry = spec.get("entry")
|
|
719
|
+
if not entry:
|
|
720
|
+
raise BridgeError("POSTPROCESS_NOT_CONFIGURED", "protocol asks for postprocess but config.postprocess.entry is not set")
|
|
721
|
+
for extra in spec.get("pythonPaths") or []:
|
|
722
|
+
path = os.path.abspath(expand(extra))
|
|
723
|
+
if path not in sys.path:
|
|
724
|
+
sys.path.insert(0, path)
|
|
725
|
+
if ":" in entry:
|
|
726
|
+
module_name, func_name = entry.split(":", 1)
|
|
727
|
+
else:
|
|
728
|
+
module_name, func_name = entry.rsplit(".", 1)
|
|
729
|
+
try:
|
|
730
|
+
module = importlib.import_module(module_name)
|
|
731
|
+
func = getattr(module, func_name)
|
|
732
|
+
except Exception as exc:
|
|
733
|
+
raise BridgeError("POSTPROCESS_IMPORT_FAILED", "cannot import postprocess " + entry + ": " + str(exc))
|
|
734
|
+
try:
|
|
735
|
+
result = func(run_folder=run_folder, scan_global=scan_global, reps=reps, name=name)
|
|
736
|
+
except TypeError:
|
|
737
|
+
try:
|
|
738
|
+
result = func(run_folder, scan_global, reps, name)
|
|
739
|
+
except Exception as exc:
|
|
740
|
+
raise BridgeError("POSTPROCESS_FAILED", "postprocess raised: " + str(exc))
|
|
741
|
+
except Exception as exc:
|
|
742
|
+
raise BridgeError("POSTPROCESS_FAILED", "postprocess raised: " + str(exc))
|
|
743
|
+
if not isinstance(result, dict):
|
|
744
|
+
raise BridgeError("POSTPROCESS_BAD_RESULT", "postprocess must return a dict, got " + type(result).__name__)
|
|
745
|
+
return jsonable(result)
|
|
746
|
+
|
|
747
|
+
|
|
748
|
+
def resolve_dotted(obj, dotted):
|
|
749
|
+
current = obj
|
|
750
|
+
for part in str(dotted).split("."):
|
|
751
|
+
if not isinstance(current, dict):
|
|
752
|
+
return None
|
|
753
|
+
current = current.get(part)
|
|
754
|
+
return current
|
|
755
|
+
|
|
756
|
+
|
|
757
|
+
def apply_finish(backend, config, plan, result, run_folder):
|
|
758
|
+
out = {}
|
|
759
|
+
finish = plan.get("finish") or {}
|
|
760
|
+
panel = finish.get("panel") or {}
|
|
761
|
+
writes = {}
|
|
762
|
+
for key, dotted in panel.items():
|
|
763
|
+
value = as_number(resolve_dotted(result, dotted))
|
|
764
|
+
if value is not None:
|
|
765
|
+
writes[str(key)] = value
|
|
766
|
+
if writes:
|
|
767
|
+
out["panel"] = jsonable(backend.set_globals(writes, raw=False))
|
|
768
|
+
history = finish.get("history") or {}
|
|
769
|
+
rows = []
|
|
770
|
+
log_path = config.get("calibrationLog")
|
|
771
|
+
if history and log_path:
|
|
772
|
+
log_path = os.path.abspath(expand(log_path))
|
|
773
|
+
os.makedirs(os.path.dirname(log_path), exist_ok=True)
|
|
774
|
+
with open(log_path, "a", encoding="utf-8") as handle:
|
|
775
|
+
for key, dotted in history.items():
|
|
776
|
+
value = as_number(resolve_dotted(result, dotted))
|
|
777
|
+
if value is None:
|
|
778
|
+
continue
|
|
779
|
+
row = {
|
|
780
|
+
"ts": now_iso(),
|
|
781
|
+
"protocol": plan.get("name") or plan.get("labscript"),
|
|
782
|
+
"run_folder": run_folder,
|
|
783
|
+
"key": str(key),
|
|
784
|
+
"value": value,
|
|
785
|
+
"postprocess": plan.get("postprocess"),
|
|
786
|
+
}
|
|
787
|
+
handle.write(json.dumps(row, ensure_ascii=False) + "\n")
|
|
788
|
+
rows.append(row)
|
|
789
|
+
if rows:
|
|
790
|
+
out["history"] = rows
|
|
791
|
+
return out
|
|
792
|
+
|
|
793
|
+
|
|
794
|
+
def restore_panel(backend, config, snapshot_file, protected, only_touched=True):
|
|
795
|
+
_, data = load_snapshot(config, snapshot_file) if snapshot_file else load_snapshot(config, "latest")
|
|
796
|
+
protected = protected or set()
|
|
797
|
+
touched = data.get("touched")
|
|
798
|
+
use_raw = isinstance(backend, LabscriptBackend)
|
|
799
|
+
if use_raw:
|
|
800
|
+
values = to_raw_values(data.get("raw_globals") or {})
|
|
801
|
+
else:
|
|
802
|
+
values = data.get("globals")
|
|
803
|
+
if not values:
|
|
804
|
+
values = to_raw_values(data.get("raw_globals") or {})
|
|
805
|
+
use_raw = True
|
|
806
|
+
if not values:
|
|
807
|
+
return {"restored": False, "reason": "snapshot has no values"}
|
|
808
|
+
if only_touched and touched:
|
|
809
|
+
wanted = set(str(key) for key in touched)
|
|
810
|
+
values = dict((k, v) for k, v in values.items() if k in wanted)
|
|
811
|
+
if protected:
|
|
812
|
+
values = dict((k, v) for k, v in values.items() if k not in protected)
|
|
813
|
+
if not values:
|
|
814
|
+
return {"restored": False, "reason": "nothing to restore after filtering touched/protected keys"}
|
|
815
|
+
written = backend.set_globals(values, raw=use_raw)
|
|
816
|
+
readback = backend.get_globals(raw=True)
|
|
817
|
+
return {
|
|
818
|
+
"restored": True,
|
|
819
|
+
"selective": bool(only_touched and touched),
|
|
820
|
+
"keys": sorted(str(key) for key in values),
|
|
821
|
+
"from": data.get("ts"),
|
|
822
|
+
"written": jsonable(written),
|
|
823
|
+
"readback": jsonable(readback),
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
|
|
827
|
+
# ---------------------------------------------------------------- protocol plan
|
|
828
|
+
|
|
829
|
+
|
|
830
|
+
def normalize_plan(raw):
|
|
831
|
+
if not isinstance(raw, dict):
|
|
832
|
+
raise BridgeError("BAD_PROTOCOL", "protocol must be a mapping")
|
|
833
|
+
plan = {}
|
|
834
|
+
plan["name"] = raw.get("name") or raw.get("protocol") or ""
|
|
835
|
+
plan["labscript"] = raw.get("labscript")
|
|
836
|
+
plan["joint_script"] = raw.get("joint_script")
|
|
837
|
+
plan["set"] = dict(raw.get("set") or {})
|
|
838
|
+
plan["scan"] = dict(raw.get("scan") or {})
|
|
839
|
+
plan["reps"] = raw.get("reps", 1)
|
|
840
|
+
plan["engage"] = bool(raw.get("engage", True))
|
|
841
|
+
plan["postprocess"] = raw.get("postprocess") or raw.get("analysis")
|
|
842
|
+
finish = raw.get("finish") or {}
|
|
843
|
+
plan["finish"] = {
|
|
844
|
+
"restore": bool(finish.get("restore", True)),
|
|
845
|
+
"panel": dict(finish.get("panel") or {}),
|
|
846
|
+
"history": dict(finish.get("history") or {}),
|
|
847
|
+
}
|
|
848
|
+
if not plan["labscript"] and not plan["joint_script"]:
|
|
849
|
+
raise BridgeError("BAD_PROTOCOL", "protocol needs labscript or joint_script")
|
|
850
|
+
reps = plan["reps"]
|
|
851
|
+
if isinstance(reps, bool) or not isinstance(reps, int) or reps <= 0:
|
|
852
|
+
raise BridgeError("BAD_PROTOCOL", "reps must be a positive integer")
|
|
853
|
+
return plan
|
|
854
|
+
|
|
855
|
+
|
|
856
|
+
def scan_expr_from_value(value):
|
|
857
|
+
"""Protocol YAML scan value -> runmanager raw expression string."""
|
|
858
|
+
import numpy as np
|
|
859
|
+
if isinstance(value, bool):
|
|
860
|
+
raise BridgeError("BAD_PROTOCOL", "scan value cannot be a bool")
|
|
861
|
+
if isinstance(value, (int, float)):
|
|
862
|
+
return repr(float(value))
|
|
863
|
+
if isinstance(value, str):
|
|
864
|
+
arr = eval_scan_expr(value)
|
|
865
|
+
if arr.size < 1:
|
|
866
|
+
raise BridgeError("BAD_PROTOCOL", "scan expression is empty: " + value)
|
|
867
|
+
return value
|
|
868
|
+
if isinstance(value, (list, tuple)):
|
|
869
|
+
if not value:
|
|
870
|
+
raise BridgeError("BAD_PROTOCOL", "scan list is empty")
|
|
871
|
+
numbers = [float(item) for item in value]
|
|
872
|
+
if len(numbers) == 1:
|
|
873
|
+
return repr(numbers[0])
|
|
874
|
+
steps = [numbers[index + 1] - numbers[index] for index in range(len(numbers) - 1)]
|
|
875
|
+
mean = sum(steps) / len(steps)
|
|
876
|
+
tolerance = 1e-9 * max(1.0, abs(mean))
|
|
877
|
+
if all(abs(step - mean) <= tolerance for step in steps):
|
|
878
|
+
return "np.linspace(" + repr(numbers[0]) + ", " + repr(numbers[-1]) + ", " + str(len(numbers)) + ")"
|
|
879
|
+
return "np.array([" + ",".join(repr(number) for number in numbers) + "])"
|
|
880
|
+
raise BridgeError("BAD_PROTOCOL", "unsupported scan value: " + repr(value))
|
|
881
|
+
|
|
882
|
+
|
|
883
|
+
def resolve_plan(config, plan):
|
|
884
|
+
resolved = dict(plan)
|
|
885
|
+
if not resolved.get("labscript") and resolved.get("joint_script"):
|
|
886
|
+
path = config.get("scriptsFile")
|
|
887
|
+
if not path:
|
|
888
|
+
raise BridgeError("UNKNOWN_SCRIPT", "protocol uses joint_script but config.scriptsFile is not set")
|
|
889
|
+
path = os.path.abspath(expand(path))
|
|
890
|
+
if not os.path.isfile(path):
|
|
891
|
+
raise BridgeError("UNKNOWN_SCRIPT", "scriptsFile not found: " + path)
|
|
892
|
+
with open(path, "r", encoding="utf-8") as handle:
|
|
893
|
+
registry = json.load(handle)
|
|
894
|
+
entry = registry.get(resolved["joint_script"])
|
|
895
|
+
if not entry:
|
|
896
|
+
raise BridgeError("UNKNOWN_SCRIPT", "joint_script not registered: " + str(resolved["joint_script"]))
|
|
897
|
+
resolved["labscript"] = entry.get("labscript")
|
|
898
|
+
scan_exprs = {}
|
|
899
|
+
scan_raw = {}
|
|
900
|
+
n_points = 1
|
|
901
|
+
for key, value in resolved["scan"].items():
|
|
902
|
+
expr = scan_expr_from_value(value)
|
|
903
|
+
arr = eval_scan_expr(expr)
|
|
904
|
+
if arr.size < 1:
|
|
905
|
+
raise BridgeError("BAD_PROTOCOL", "scan axis is empty: " + key)
|
|
906
|
+
scan_exprs[key] = arr
|
|
907
|
+
scan_raw[key] = expr
|
|
908
|
+
n_points *= int(arr.size)
|
|
909
|
+
resolved["scan_exprs"] = scan_exprs
|
|
910
|
+
resolved["scan_raw"] = scan_raw
|
|
911
|
+
resolved["expected_n_shots"] = int(resolved["reps"]) * n_points
|
|
912
|
+
return resolved
|
|
913
|
+
|
|
914
|
+
|
|
915
|
+
# ---------------------------------------------------------------- commands
|
|
916
|
+
|
|
917
|
+
|
|
918
|
+
def cmd_status(config, args):
|
|
919
|
+
backend = make_backend(config)
|
|
920
|
+
keys = args.get("keys")
|
|
921
|
+
raw = bool(args.get("raw"))
|
|
922
|
+
values = backend.get_globals(raw=False)
|
|
923
|
+
raw_values = backend.get_globals(raw=True)
|
|
924
|
+
if keys:
|
|
925
|
+
values = dict((k, values.get(k)) for k in keys)
|
|
926
|
+
raw_values = dict((k, raw_values.get(k)) for k in keys)
|
|
927
|
+
out = {
|
|
928
|
+
"backend": config.get("backend"),
|
|
929
|
+
"version": backend.version() if hasattr(backend, "version") else None,
|
|
930
|
+
"labscript": backend.labscript_file(),
|
|
931
|
+
"shot_output_folder": backend.shot_output_folder(),
|
|
932
|
+
"globals": jsonable(values),
|
|
933
|
+
"n_shots": backend.n_shots(),
|
|
934
|
+
"error_in_globals": backend.error_in_globals(),
|
|
935
|
+
"run_shots": backend.run_shots(),
|
|
936
|
+
"panel_history": panel_history_info(config),
|
|
937
|
+
}
|
|
938
|
+
if raw:
|
|
939
|
+
out["raw_globals"] = jsonable(raw_values)
|
|
940
|
+
return out
|
|
941
|
+
|
|
942
|
+
|
|
943
|
+
def panel_history_info(config):
|
|
944
|
+
folder = history_dir(config)
|
|
945
|
+
files = sorted(glob.glob(os.path.join(folder, "*.json")))
|
|
946
|
+
latest = os.path.basename(files[-1]) if files else None
|
|
947
|
+
return {"dir": folder, "latest": latest, "count": len(files)}
|
|
948
|
+
|
|
949
|
+
|
|
950
|
+
def cmd_set(config, args):
|
|
951
|
+
backend = make_backend(config)
|
|
952
|
+
values = dict(args.get("values") or {})
|
|
953
|
+
reps = args.get("reps")
|
|
954
|
+
raw = bool(args.get("raw"))
|
|
955
|
+
if reps is not None:
|
|
956
|
+
if isinstance(reps, bool) or not isinstance(reps, int) or reps <= 0:
|
|
957
|
+
raise BridgeError("BAD_ARGS", "reps must be a positive integer")
|
|
958
|
+
reps_global = ((config.get("reps") or {}).get("global") or "dummy_iterator")
|
|
959
|
+
values[reps_global] = "np.arange(" + str(reps) + ")" if raw else list(range(reps))
|
|
960
|
+
raw = True
|
|
961
|
+
if not values:
|
|
962
|
+
raise BridgeError("BAD_ARGS", "values must not be empty")
|
|
963
|
+
if raw:
|
|
964
|
+
values = dict((k, v) if isinstance(v, str) else (k, values_to_raw({k: v})[k]) for k, v in values.items()) if False else dict((k, v if isinstance(v, str) else values_to_raw({k: v})[k]) for k, v in values.items())
|
|
965
|
+
elif has_array_value(values):
|
|
966
|
+
values = values_to_raw(values)
|
|
967
|
+
raw = True
|
|
968
|
+
check_limits(config, dict((k, v) for k, v in values.items() if not raw), dict((k, v) for k, v in values.items()) if raw else {}, None)
|
|
969
|
+
snap = snapshot_panel(backend, config, "set", {"keys": sorted(values.keys()), "raw": raw, "note": args.get("note")}, touched=values.keys())
|
|
970
|
+
try:
|
|
971
|
+
written = backend.set_globals(values, raw=raw)
|
|
972
|
+
except BridgeError as exc:
|
|
973
|
+
detail = dict(exc.detail or {})
|
|
974
|
+
detail["snapshot"] = snap
|
|
975
|
+
detail["hint"] = "set_globals failed after the panel snapshot; the panel may be partially written. Use labs_restore to undo."
|
|
976
|
+
raise BridgeError(exc.code, exc.message, detail)
|
|
977
|
+
except Exception as exc:
|
|
978
|
+
raise BridgeError("SET_FAILED", "set_globals failed: " + type(exc).__name__ + ": " + str(exc), {
|
|
979
|
+
"snapshot": snap,
|
|
980
|
+
"hint": "the panel may be partially written; use labs_restore to undo",
|
|
981
|
+
})
|
|
982
|
+
readback = backend.get_globals(raw=False)
|
|
983
|
+
raw_readback = backend.get_globals(raw=True)
|
|
984
|
+
missing = [k for k in values if k not in readback]
|
|
985
|
+
out = {
|
|
986
|
+
"written": jsonable(written),
|
|
987
|
+
"readback": dict((k, jsonable(readback.get(k))) for k in values),
|
|
988
|
+
"raw_readback": dict((k, jsonable(raw_readback.get(k))) for k in values),
|
|
989
|
+
"missing": missing,
|
|
990
|
+
"n_shots": backend.n_shots(),
|
|
991
|
+
"error_in_globals": backend.error_in_globals(),
|
|
992
|
+
"snapshot": snap,
|
|
993
|
+
}
|
|
994
|
+
append_event(config, {"ts": now_iso(), "tool": "set", "backend": config.get("backend"), "note": args.get("note"), "keys": sorted(values.keys()), "snapshot": snap["name"]})
|
|
995
|
+
return out
|
|
996
|
+
|
|
997
|
+
|
|
998
|
+
def cmd_engage(config, args):
|
|
999
|
+
backend = make_backend(config)
|
|
1000
|
+
if backend.error_in_globals():
|
|
1001
|
+
raise BridgeError("GLOBALS_ERROR", "panel globals are in an error state; refusing to engage")
|
|
1002
|
+
run_shots = backend.run_shots()
|
|
1003
|
+
if run_shots is False:
|
|
1004
|
+
raise BridgeError("RUN_SHOTS_OFF", "the runmanager 'Run shot(s)' checkbox is off; engage would only compile and never execute")
|
|
1005
|
+
n_shots = backend.n_shots()
|
|
1006
|
+
result = backend.engage()
|
|
1007
|
+
run_folder = None
|
|
1008
|
+
if isinstance(result, dict):
|
|
1009
|
+
run_folder = result.get("run_folder")
|
|
1010
|
+
else:
|
|
1011
|
+
run_folder = getattr(result, "run_folder", None)
|
|
1012
|
+
out = {
|
|
1013
|
+
"accepted": True,
|
|
1014
|
+
"run_folder": run_folder,
|
|
1015
|
+
"mapped_run_folder": map_path(config, run_folder),
|
|
1016
|
+
"n_shots_at_submit": n_shots,
|
|
1017
|
+
"expected_n_shots": int(args.get("expected_n_shots") or n_shots),
|
|
1018
|
+
"labscript": backend.labscript_file(),
|
|
1019
|
+
"submitted_at": now_iso(),
|
|
1020
|
+
}
|
|
1021
|
+
append_event(config, {"ts": out["submitted_at"], "tool": "engage", "backend": config.get("backend"), "note": args.get("note"), "run_folder": run_folder, "expected_n_shots": out["expected_n_shots"]})
|
|
1022
|
+
return out
|
|
1023
|
+
|
|
1024
|
+
|
|
1025
|
+
def wait_for_run(config, run_folder, expected, timeout_s):
|
|
1026
|
+
if not run_folder:
|
|
1027
|
+
raise BridgeError("BAD_ARGS", "run_folder is required")
|
|
1028
|
+
folder = map_path(config, run_folder)
|
|
1029
|
+
engage_cfg = config.get("engage") or {}
|
|
1030
|
+
timeout = float(timeout_s or engage_cfg.get("timeoutS") or 900)
|
|
1031
|
+
poll = float(engage_cfg.get("pollS") or 1.0)
|
|
1032
|
+
settle = float(engage_cfg.get("settleS") or 2.0)
|
|
1033
|
+
stall = float(engage_cfg.get("stallS") or 20.0)
|
|
1034
|
+
start = time.time()
|
|
1035
|
+
stable_since = None
|
|
1036
|
+
compiled_since = None
|
|
1037
|
+
last_count = -1
|
|
1038
|
+
last_size = -1
|
|
1039
|
+
last_executed = None
|
|
1040
|
+
|
|
1041
|
+
def result(complete, timed_out, diagnosis, count, executed, elapsed, hint=None):
|
|
1042
|
+
payload = {
|
|
1043
|
+
"run_folder": run_folder,
|
|
1044
|
+
"mapped_run_folder": folder,
|
|
1045
|
+
"folder_exists": os.path.isdir(folder),
|
|
1046
|
+
"expected_n_shots": expected,
|
|
1047
|
+
"n_h5": count,
|
|
1048
|
+
"complete": complete,
|
|
1049
|
+
"timed_out": timed_out,
|
|
1050
|
+
"diagnosis": diagnosis,
|
|
1051
|
+
"executed": executed,
|
|
1052
|
+
"elapsed": round(elapsed, 1),
|
|
1053
|
+
}
|
|
1054
|
+
if hint:
|
|
1055
|
+
payload["hint"] = hint
|
|
1056
|
+
return payload
|
|
1057
|
+
|
|
1058
|
+
while True:
|
|
1059
|
+
shots = sorted(glob.glob(os.path.join(folder, "**", "*.h5"), recursive=True))
|
|
1060
|
+
count = len(shots)
|
|
1061
|
+
total = sum(os.path.getsize(p) for p in shots) if shots else 0
|
|
1062
|
+
ready = False
|
|
1063
|
+
if count >= (expected or 0) and count == last_count and total == last_size and shots:
|
|
1064
|
+
ready = shot_ready(shots[-1])
|
|
1065
|
+
executed = None
|
|
1066
|
+
if ready:
|
|
1067
|
+
executed = shot_executed(shots[-1])
|
|
1068
|
+
if ready and executed is True:
|
|
1069
|
+
compiled_since = None
|
|
1070
|
+
if stable_since is None:
|
|
1071
|
+
stable_since = time.time()
|
|
1072
|
+
elif time.time() - stable_since >= settle:
|
|
1073
|
+
return result(True, False, "complete", count, True, time.time() - start)
|
|
1074
|
+
else:
|
|
1075
|
+
stable_since = None
|
|
1076
|
+
if ready and executed is False:
|
|
1077
|
+
if compiled_since is None:
|
|
1078
|
+
compiled_since = time.time()
|
|
1079
|
+
elif time.time() - compiled_since >= stall:
|
|
1080
|
+
return result(
|
|
1081
|
+
False, False, "compiled_not_executed", count, False, time.time() - start,
|
|
1082
|
+
"shots were compiled but show no acquisition data after " + str(int(stall)) +
|
|
1083
|
+
" s: BLACS may be paused / the queue may not be running, or the run is still being programmed. Check BLACS and the panel, then labs_abort if needed.")
|
|
1084
|
+
else:
|
|
1085
|
+
compiled_since = None
|
|
1086
|
+
last_count = count
|
|
1087
|
+
last_size = total
|
|
1088
|
+
last_executed = executed
|
|
1089
|
+
if time.time() - start >= timeout:
|
|
1090
|
+
if count == 0:
|
|
1091
|
+
diagnosis = "no_shots"
|
|
1092
|
+
hint = "no shot h5 appeared: check that the run was accepted, that BLACS is running, and that pathMap points at the run folder."
|
|
1093
|
+
elif count < (expected or 0):
|
|
1094
|
+
diagnosis = "compiling"
|
|
1095
|
+
hint = "only " + str(count) + " of " + str(expected) + " shots appeared before the timeout; the run may still be compiling or blocked."
|
|
1096
|
+
elif executed is False:
|
|
1097
|
+
diagnosis = "compiled_not_executed"
|
|
1098
|
+
hint = "shots were compiled but no acquisition data was found: BLACS may be paused or not executing the queue."
|
|
1099
|
+
else:
|
|
1100
|
+
diagnosis = "partial"
|
|
1101
|
+
hint = "shots appeared but the run was not confirmed complete before the timeout."
|
|
1102
|
+
return result(False, True, diagnosis, count, executed, time.time() - start, hint)
|
|
1103
|
+
time.sleep(poll)
|
|
1104
|
+
|
|
1105
|
+
|
|
1106
|
+
def cmd_wait(config, args):
|
|
1107
|
+
return wait_for_run(config, args.get("run_folder"), int(args.get("expected_n_shots") or 0), args.get("timeout_s"))
|
|
1108
|
+
|
|
1109
|
+
|
|
1110
|
+
def cmd_abort(config, args):
|
|
1111
|
+
backend = make_backend(config)
|
|
1112
|
+
backend_name = (config.get("backend") or "").lower()
|
|
1113
|
+
if backend_name == "labscript":
|
|
1114
|
+
note = ("labscript abort stops compiling/submitting new shots. Shots that are already compiled and queued "
|
|
1115
|
+
"in BLACS, or currently running, are not stopped by it. This is not a hardware interlock.")
|
|
1116
|
+
elif backend_name == "http-sim":
|
|
1117
|
+
note = "http-sim abort stops the simulated queue (pause + clear)."
|
|
1118
|
+
else:
|
|
1119
|
+
note = "mock backend abort is a no-op."
|
|
1120
|
+
result = backend.abort()
|
|
1121
|
+
append_event(config, {"ts": now_iso(), "tool": "abort", "backend": config.get("backend"), "note": note, "result": jsonable(result)})
|
|
1122
|
+
return {"aborted": True, "note": note, "result": jsonable(result)}
|
|
1123
|
+
|
|
1124
|
+
|
|
1125
|
+
def cmd_progress(config, args):
|
|
1126
|
+
run_folder = args.get("run_folder")
|
|
1127
|
+
expected = int(args.get("expected_n_shots") or 0)
|
|
1128
|
+
folder = map_path(config, run_folder)
|
|
1129
|
+
shots = sorted(glob.glob(os.path.join(folder, "**", "*.h5"), recursive=True)) if folder else []
|
|
1130
|
+
return {"run_folder": run_folder, "n_h5": len(shots), "expected_n_shots": expected, "complete": bool(expected and len(shots) >= expected)}
|
|
1131
|
+
|
|
1132
|
+
|
|
1133
|
+
|
|
1134
|
+
def script_name_check(script_path, keys):
|
|
1135
|
+
"""Check that every protocol key appears as an identifier in the labscript source.
|
|
1136
|
+
|
|
1137
|
+
Heuristic by design: tokenize strips comments/strings; dynamic globals lookups
|
|
1138
|
+
are not detected. Returns checked=False when the source cannot be read.
|
|
1139
|
+
"""
|
|
1140
|
+
if not script_path:
|
|
1141
|
+
return {"checked": False, "reason": "no labscript file", "script": None, "missing": [], "suggestions": {}}
|
|
1142
|
+
path = os.path.abspath(expand(script_path))
|
|
1143
|
+
if not os.path.isfile(path):
|
|
1144
|
+
return {"checked": False, "reason": "labscript file not readable: " + path, "script": script_path, "missing": [], "suggestions": {}}
|
|
1145
|
+
try:
|
|
1146
|
+
with open(path, "r", encoding="utf-8", errors="replace") as handle:
|
|
1147
|
+
source = handle.read()
|
|
1148
|
+
import io
|
|
1149
|
+
import tokenize
|
|
1150
|
+
names = set()
|
|
1151
|
+
for token in tokenize.generate_tokens(io.StringIO(source).readline):
|
|
1152
|
+
if token.type == tokenize.NAME:
|
|
1153
|
+
names.add(token.string)
|
|
1154
|
+
except Exception as exc:
|
|
1155
|
+
return {"checked": False, "reason": "cannot parse labscript: " + str(exc), "script": script_path, "missing": [], "suggestions": {}}
|
|
1156
|
+
import difflib
|
|
1157
|
+
missing = []
|
|
1158
|
+
suggestions = {}
|
|
1159
|
+
for key in keys:
|
|
1160
|
+
if key in names:
|
|
1161
|
+
continue
|
|
1162
|
+
missing.append(key)
|
|
1163
|
+
suggestions[key] = difflib.get_close_matches(str(key), sorted(names), n=3, cutoff=0.6)
|
|
1164
|
+
return {"checked": True, "script": script_path, "missing": missing, "suggestions": suggestions}
|
|
1165
|
+
|
|
1166
|
+
|
|
1167
|
+
def check_protocol_names(config, plan):
|
|
1168
|
+
backend = make_backend(config)
|
|
1169
|
+
loaded = backend.labscript_file()
|
|
1170
|
+
if not loaded:
|
|
1171
|
+
return {"checked": False, "reason": "runmanager did not report a labscript file", "script": None, "missing": [], "suggestions": {}}
|
|
1172
|
+
reps_global = ((config.get("reps") or {}).get("global") or "dummy_iterator")
|
|
1173
|
+
keys = sorted((set(plan.get("set") or {}) | set(plan.get("scan") or {})) - {reps_global})
|
|
1174
|
+
info = script_name_check(map_path(config, loaded), keys)
|
|
1175
|
+
info["script"] = loaded
|
|
1176
|
+
return info
|
|
1177
|
+
|
|
1178
|
+
def cmd_data(config, args):
|
|
1179
|
+
ensure_h5_lock()
|
|
1180
|
+
import h5py
|
|
1181
|
+
run_folder = args.get("run_folder")
|
|
1182
|
+
if not run_folder:
|
|
1183
|
+
raise BridgeError("BAD_ARGS", "run_folder is required")
|
|
1184
|
+
folder = map_path(config, run_folder)
|
|
1185
|
+
shots = sorted(glob.glob(os.path.join(folder, "**", "*.h5"), recursive=True))
|
|
1186
|
+
out = {"run_folder": run_folder, "mapped_run_folder": folder, "n_h5": len(shots)}
|
|
1187
|
+
if not shots:
|
|
1188
|
+
return out
|
|
1189
|
+
scan = args.get("scan")
|
|
1190
|
+
with h5py.File(shots[0], "r", locking=False) as handle:
|
|
1191
|
+
globals_attrs = dict(handle["globals"].attrs) if "globals" in handle else {}
|
|
1192
|
+
if args.get("globals", True):
|
|
1193
|
+
out["globals"] = jsonable(globals_attrs)
|
|
1194
|
+
if args.get("results"):
|
|
1195
|
+
layout = {}
|
|
1196
|
+
if "results" in handle:
|
|
1197
|
+
for group_name in handle["results"]:
|
|
1198
|
+
group = handle["results"][group_name]
|
|
1199
|
+
if isinstance(group, h5py.Group):
|
|
1200
|
+
layout[group_name] = sorted(list(group.keys()) + list(group.attrs.keys()))
|
|
1201
|
+
out["results_layout"] = layout
|
|
1202
|
+
if scan:
|
|
1203
|
+
rows = []
|
|
1204
|
+
for path in shots:
|
|
1205
|
+
with h5py.File(path, "r", locking=False) as handle:
|
|
1206
|
+
attrs = handle["globals"].attrs if "globals" in handle else {}
|
|
1207
|
+
row = {"shot": os.path.basename(path)}
|
|
1208
|
+
if scan in attrs:
|
|
1209
|
+
row[scan] = jsonable(attrs[scan])
|
|
1210
|
+
if "results" in handle:
|
|
1211
|
+
for group_name in handle["results"]:
|
|
1212
|
+
group = handle["results"][group_name]
|
|
1213
|
+
if not isinstance(group, h5py.Group):
|
|
1214
|
+
continue
|
|
1215
|
+
for key in list(group.attrs.keys()):
|
|
1216
|
+
value = as_number(group.attrs[key])
|
|
1217
|
+
if value is not None:
|
|
1218
|
+
row[group_name + "." + key] = value
|
|
1219
|
+
for key in group.keys():
|
|
1220
|
+
item = group[key]
|
|
1221
|
+
if isinstance(item, h5py.Dataset) and item.size <= 16:
|
|
1222
|
+
try:
|
|
1223
|
+
value = item[()]
|
|
1224
|
+
if hasattr(value, "item"):
|
|
1225
|
+
value = value.item()
|
|
1226
|
+
except Exception:
|
|
1227
|
+
continue
|
|
1228
|
+
number = as_number(value)
|
|
1229
|
+
if number is not None:
|
|
1230
|
+
row[group_name + "." + key] = number
|
|
1231
|
+
rows.append(row)
|
|
1232
|
+
out["scan_table"] = {"scan": scan, "n_points": len(rows), "rows": rows}
|
|
1233
|
+
return out
|
|
1234
|
+
|
|
1235
|
+
|
|
1236
|
+
def cmd_restore(config, args):
|
|
1237
|
+
backend = make_backend(config)
|
|
1238
|
+
snap = args.get("snapshot")
|
|
1239
|
+
full = bool(args.get("full"))
|
|
1240
|
+
path, data = load_snapshot(config, snap)
|
|
1241
|
+
touched = data.get("touched")
|
|
1242
|
+
use_raw = isinstance(backend, LabscriptBackend)
|
|
1243
|
+
if use_raw:
|
|
1244
|
+
values = to_raw_values(data.get("raw_globals") or {})
|
|
1245
|
+
else:
|
|
1246
|
+
values = data.get("globals") or to_raw_values(data.get("raw_globals") or {})
|
|
1247
|
+
if not data.get("globals"):
|
|
1248
|
+
use_raw = True
|
|
1249
|
+
if not full and touched:
|
|
1250
|
+
wanted = set(str(key) for key in touched)
|
|
1251
|
+
values = dict((k, v) for k, v in values.items() if k in wanted)
|
|
1252
|
+
written = backend.set_globals(values, raw=use_raw)
|
|
1253
|
+
readback = backend.get_globals(raw=True)
|
|
1254
|
+
append_event(config, {"ts": now_iso(), "tool": "restore", "backend": config.get("backend"), "snapshot": os.path.basename(path)})
|
|
1255
|
+
return {
|
|
1256
|
+
"restored_from": {"file": path, "ts": data.get("ts"), "tool": data.get("tool")},
|
|
1257
|
+
"selective": bool(not full and touched),
|
|
1258
|
+
"keys": sorted(str(key) for key in values),
|
|
1259
|
+
"written": jsonable(written),
|
|
1260
|
+
"readback": jsonable(readback),
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
|
|
1264
|
+
def cmd_protocol_plan(config, args):
|
|
1265
|
+
plan = resolve_plan(config, normalize_plan(args.get("protocol") or {}))
|
|
1266
|
+
limits = check_limits(config, plan["set"], dict((k, v) for k, v in plan["scan_raw"].items()), plan["expected_n_shots"])
|
|
1267
|
+
public = {
|
|
1268
|
+
"name": plan.get("name"),
|
|
1269
|
+
"labscript": plan.get("labscript"),
|
|
1270
|
+
"joint_script": plan.get("joint_script"),
|
|
1271
|
+
"set": jsonable(plan["set"]),
|
|
1272
|
+
"scan": dict((k, {"expr": plan["scan_raw"][k], "n": int(plan["scan_exprs"][k].size)}) for k in plan["scan"]),
|
|
1273
|
+
"reps": plan["reps"],
|
|
1274
|
+
"engage": plan["engage"],
|
|
1275
|
+
"postprocess": plan.get("postprocess"),
|
|
1276
|
+
"finish": jsonable(plan["finish"]),
|
|
1277
|
+
"expected_n_shots": plan["expected_n_shots"],
|
|
1278
|
+
}
|
|
1279
|
+
warnings = []
|
|
1280
|
+
if not plan.get("postprocess"):
|
|
1281
|
+
warnings.append("protocol has no postprocess")
|
|
1282
|
+
name_check = check_protocol_names(config, plan)
|
|
1283
|
+
return {"plan": public, "hard_limits": limits, "name_check": name_check, "warnings": warnings}
|
|
1284
|
+
|
|
1285
|
+
|
|
1286
|
+
def cmd_protocol_start(config, args):
|
|
1287
|
+
plan = resolve_plan(config, normalize_plan(args.get("protocol") or {}))
|
|
1288
|
+
backend = make_backend(config)
|
|
1289
|
+
check_limits(config, plan["set"], dict((k, v) for k, v in plan["scan_raw"].items()), plan["expected_n_shots"])
|
|
1290
|
+
reps_global = ((config.get("reps") or {}).get("global") or "dummy_iterator")
|
|
1291
|
+
touched = set(plan["set"]) | set(plan["scan"]) | {reps_global}
|
|
1292
|
+
snap = snapshot_panel(backend, config, "protocol", {"name": plan.get("name"), "labscript": plan.get("labscript"), "note": args.get("note")}, touched=touched)
|
|
1293
|
+
steps = {}
|
|
1294
|
+
try:
|
|
1295
|
+
if plan["set"]:
|
|
1296
|
+
steps["set"] = jsonable(backend.set_globals(plan["set"], raw=False))
|
|
1297
|
+
scan_values = dict((k, expr) for k, expr in plan["scan_raw"].items())
|
|
1298
|
+
scan_values[reps_global] = "np.arange(" + str(plan["reps"]) + ")"
|
|
1299
|
+
steps["scan"] = jsonable(backend.set_globals(scan_values, raw=True))
|
|
1300
|
+
except BridgeError as exc:
|
|
1301
|
+
detail = dict(exc.detail or {})
|
|
1302
|
+
detail["snapshot"] = snap
|
|
1303
|
+
detail["hint"] = "protocol panel write failed; the panel may be partially written. Use labs_restore to undo."
|
|
1304
|
+
raise BridgeError(exc.code, exc.message, detail)
|
|
1305
|
+
except Exception as exc:
|
|
1306
|
+
raise BridgeError("PROTOCOL_WRITE_FAILED", "protocol panel write failed: " + type(exc).__name__ + ": " + str(exc), {
|
|
1307
|
+
"snapshot": snap,
|
|
1308
|
+
"hint": "the panel may be partially written; use labs_restore to undo",
|
|
1309
|
+
})
|
|
1310
|
+
panel_n_shots = backend.n_shots()
|
|
1311
|
+
if panel_n_shots != plan["expected_n_shots"]:
|
|
1312
|
+
raise BridgeError("N_SHOTS_MISMATCH", "panel n_shots does not match protocol plan", {"panel_n_shots": panel_n_shots, "expected": plan["expected_n_shots"]})
|
|
1313
|
+
if not plan["engage"]:
|
|
1314
|
+
return {"plan": public_plan(plan), "snapshot": snap, "steps": steps, "engage": None, "n_shots_at_submit": panel_n_shots}
|
|
1315
|
+
result = backend.engage()
|
|
1316
|
+
run_folder = result.get("run_folder") if isinstance(result, dict) else getattr(result, "run_folder", None)
|
|
1317
|
+
out = {
|
|
1318
|
+
"plan": public_plan(plan),
|
|
1319
|
+
"snapshot": snap,
|
|
1320
|
+
"steps": steps,
|
|
1321
|
+
"engage": {"accepted": True, "run_folder": run_folder, "mapped_run_folder": map_path(config, run_folder), "n_shots_at_submit": panel_n_shots},
|
|
1322
|
+
"n_shots_at_submit": panel_n_shots,
|
|
1323
|
+
"expected_n_shots": plan["expected_n_shots"],
|
|
1324
|
+
"submitted_at": now_iso(),
|
|
1325
|
+
}
|
|
1326
|
+
append_event(config, {"ts": out["submitted_at"], "tool": "protocol", "backend": config.get("backend"), "note": args.get("note"), "protocol": plan.get("name"), "run_folder": run_folder, "expected_n_shots": plan["expected_n_shots"]})
|
|
1327
|
+
return out
|
|
1328
|
+
|
|
1329
|
+
|
|
1330
|
+
def public_plan(plan):
|
|
1331
|
+
return {
|
|
1332
|
+
"name": plan.get("name"),
|
|
1333
|
+
"labscript": plan.get("labscript"),
|
|
1334
|
+
"joint_script": plan.get("joint_script"),
|
|
1335
|
+
"set": jsonable(plan["set"]),
|
|
1336
|
+
"scan": dict((k, {"expr": plan["scan"][k], "n": int(plan["scan_exprs"][k].size)}) for k in plan["scan"]),
|
|
1337
|
+
"reps": plan["reps"],
|
|
1338
|
+
"engage": plan["engage"],
|
|
1339
|
+
"postprocess": plan.get("postprocess"),
|
|
1340
|
+
"finish": jsonable(plan["finish"]),
|
|
1341
|
+
"expected_n_shots": plan["expected_n_shots"],
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
|
|
1345
|
+
def cmd_protocol_finalize(config, args):
|
|
1346
|
+
plan = resolve_plan(config, normalize_plan(args.get("protocol") or {}))
|
|
1347
|
+
run_folder = args.get("run_folder")
|
|
1348
|
+
if not run_folder:
|
|
1349
|
+
raise BridgeError("BAD_ARGS", "run_folder is required")
|
|
1350
|
+
backend = make_backend(config)
|
|
1351
|
+
out = {"run_folder": run_folder}
|
|
1352
|
+
result = None
|
|
1353
|
+
if plan.get("postprocess"):
|
|
1354
|
+
scan_global = next(iter(plan["scan"]), None) if plan["scan"] else None
|
|
1355
|
+
result = run_postprocess(config, plan, map_path(config, run_folder), scan_global, plan["reps"])
|
|
1356
|
+
out["postprocess"] = result
|
|
1357
|
+
if plan.get("finish"):
|
|
1358
|
+
out["finish"] = apply_finish(backend, config, plan, result or {}, run_folder)
|
|
1359
|
+
if plan["finish"].get("restore"):
|
|
1360
|
+
protected = set((plan["finish"].get("panel") or {}).keys())
|
|
1361
|
+
out["restore"] = restore_panel(backend, config, (args.get("snapshot") or {}).get("file"), protected)
|
|
1362
|
+
append_event(config, {"ts": now_iso(), "tool": "protocol_finalize", "backend": config.get("backend"), "note": args.get("note"), "protocol": plan.get("name"), "run_folder": run_folder, "postprocess": bool(result), "restore": (out.get("restore") or {}).get("restored")})
|
|
1363
|
+
return out
|
|
1364
|
+
|
|
1365
|
+
|
|
1366
|
+
def _add_check(checks, name, status, detail, hint=""):
|
|
1367
|
+
checks.append({"name": name, "status": status, "detail": str(detail), "hint": hint})
|
|
1368
|
+
|
|
1369
|
+
|
|
1370
|
+
def cmd_doctor(config, args):
|
|
1371
|
+
"""Read-only environment/config diagnostic (no panel writes, no engage)."""
|
|
1372
|
+
checks = []
|
|
1373
|
+
_add_check(checks, "python", "ok", sys.version.split()[0])
|
|
1374
|
+
try:
|
|
1375
|
+
import numpy as np
|
|
1376
|
+
_add_check(checks, "numpy", "ok", np.__version__)
|
|
1377
|
+
except Exception as exc:
|
|
1378
|
+
_add_check(checks, "numpy", "fail", str(exc), "install numpy (the labscript-suite environment has it)")
|
|
1379
|
+
try:
|
|
1380
|
+
import h5py
|
|
1381
|
+
_add_check(checks, "h5py", "ok", h5py.__version__)
|
|
1382
|
+
except Exception as exc:
|
|
1383
|
+
_add_check(checks, "h5py", "fail", str(exc), "install h5py")
|
|
1384
|
+
|
|
1385
|
+
backend = (config.get("backend") or "mock-labscript").lower()
|
|
1386
|
+
_add_check(checks, "backend", "ok", backend)
|
|
1387
|
+
if backend == "labscript":
|
|
1388
|
+
missing = []
|
|
1389
|
+
for module in ("labscript", "runmanager", "labscript_utils", "zprocess"):
|
|
1390
|
+
try:
|
|
1391
|
+
importlib.import_module(module)
|
|
1392
|
+
except Exception as exc:
|
|
1393
|
+
missing.append(module + ": " + str(exc))
|
|
1394
|
+
if missing:
|
|
1395
|
+
_add_check(checks, "labscript-suite", "fail", "; ".join(missing), "use the labscript-suite python environment")
|
|
1396
|
+
else:
|
|
1397
|
+
_add_check(checks, "labscript-suite", "ok", "labscript / runmanager / labscript_utils / zprocess importable")
|
|
1398
|
+
try:
|
|
1399
|
+
probe = LabscriptBackend(config)
|
|
1400
|
+
client = probe.client()
|
|
1401
|
+
version = client.get_version() if hasattr(client, "get_version") else "unknown"
|
|
1402
|
+
_add_check(checks, "runmanager", "ok", str(version))
|
|
1403
|
+
try:
|
|
1404
|
+
_add_check(checks, "panel", "ok", "n_shots=" + str(probe.n_shots()) + ", run_shots=" + str(probe.run_shots()) + ", error_in_globals=" + str(probe.error_in_globals()))
|
|
1405
|
+
except Exception as exc:
|
|
1406
|
+
_add_check(checks, "panel", "warn", str(exc), "runmanager connected but the panel read failed")
|
|
1407
|
+
try:
|
|
1408
|
+
_add_check(checks, "shot-output", "ok", str(probe.shot_output_folder()))
|
|
1409
|
+
except Exception as exc:
|
|
1410
|
+
_add_check(checks, "shot-output", "warn", str(exc), "cannot resolve the shot output folder")
|
|
1411
|
+
except Exception as exc:
|
|
1412
|
+
_add_check(checks, "runmanager", "fail", str(exc), "check runmanager host/port and the zprocess shared secret in labconfig")
|
|
1413
|
+
else:
|
|
1414
|
+
try:
|
|
1415
|
+
probe = make_backend(config)
|
|
1416
|
+
status = cmd_status(config, {})
|
|
1417
|
+
_add_check(checks, "backend-status", "ok", "n_shots=" + str(status.get("n_shots")) + ", labscript=" + str(status.get("labscript")))
|
|
1418
|
+
except Exception as exc:
|
|
1419
|
+
_add_check(checks, "backend-status", "fail", str(exc), "check the mock/simulator configuration")
|
|
1420
|
+
|
|
1421
|
+
protocols_dir = config.get("protocolsDir")
|
|
1422
|
+
if protocols_dir:
|
|
1423
|
+
path = os.path.abspath(expand(protocols_dir))
|
|
1424
|
+
_add_check(checks, "protocols-dir", "ok" if os.path.isdir(path) else "warn", path, "" if os.path.isdir(path) else "directory does not exist")
|
|
1425
|
+
scripts_file = config.get("scriptsFile")
|
|
1426
|
+
if scripts_file and os.path.isfile(os.path.abspath(expand(scripts_file))):
|
|
1427
|
+
try:
|
|
1428
|
+
with open(os.path.abspath(expand(scripts_file)), "r", encoding="utf-8") as handle:
|
|
1429
|
+
json.load(handle)
|
|
1430
|
+
_add_check(checks, "scripts-file", "ok", scripts_file)
|
|
1431
|
+
except Exception as exc:
|
|
1432
|
+
_add_check(checks, "scripts-file", "fail", str(exc), "scriptsFile must be JSON")
|
|
1433
|
+
hard_limits = config.get("hardLimitsFile")
|
|
1434
|
+
if hard_limits:
|
|
1435
|
+
try:
|
|
1436
|
+
with open(os.path.abspath(expand(hard_limits)), "r", encoding="utf-8") as handle:
|
|
1437
|
+
json.load(handle)
|
|
1438
|
+
_add_check(checks, "hard-limits-file", "ok", hard_limits)
|
|
1439
|
+
except Exception as exc:
|
|
1440
|
+
_add_check(checks, "hard-limits-file", "warn", str(exc), "hard limits are not enforced if this file cannot be read")
|
|
1441
|
+
try:
|
|
1442
|
+
base = history_dir(config)
|
|
1443
|
+
_add_check(checks, "state-dir", "ok", base)
|
|
1444
|
+
except Exception as exc:
|
|
1445
|
+
_add_check(checks, "state-dir", "fail", str(exc), "cannot create the panel history directory")
|
|
1446
|
+
|
|
1447
|
+
spec = config.get("postprocess") or {}
|
|
1448
|
+
entry = spec.get("entry")
|
|
1449
|
+
if entry:
|
|
1450
|
+
for extra in spec.get("pythonPaths") or []:
|
|
1451
|
+
extra_path = os.path.abspath(expand(extra))
|
|
1452
|
+
if extra_path not in sys.path:
|
|
1453
|
+
sys.path.insert(0, extra_path)
|
|
1454
|
+
try:
|
|
1455
|
+
module_name, _, function_name = entry.partition(":")
|
|
1456
|
+
module = importlib.import_module(module_name)
|
|
1457
|
+
if not hasattr(module, function_name):
|
|
1458
|
+
raise AttributeError("module has no attribute " + function_name)
|
|
1459
|
+
_add_check(checks, "postprocess", "ok", entry)
|
|
1460
|
+
except Exception as exc:
|
|
1461
|
+
_add_check(checks, "postprocess", "fail", str(exc), "postprocess.entry must be module:function and importable from pythonPaths")
|
|
1462
|
+
return {"checks": checks, "backend": backend, "python": sys.version.split()[0]}
|
|
1463
|
+
|
|
1464
|
+
|
|
1465
|
+
HANDLERS = {
|
|
1466
|
+
"status": cmd_status,
|
|
1467
|
+
"set": cmd_set,
|
|
1468
|
+
"engage": cmd_engage,
|
|
1469
|
+
"wait": cmd_wait,
|
|
1470
|
+
"abort": cmd_abort,
|
|
1471
|
+
"progress": cmd_progress,
|
|
1472
|
+
"data": cmd_data,
|
|
1473
|
+
"restore": cmd_restore,
|
|
1474
|
+
"protocol_plan": cmd_protocol_plan,
|
|
1475
|
+
"protocol_start": cmd_protocol_start,
|
|
1476
|
+
"protocol_finalize": cmd_protocol_finalize,
|
|
1477
|
+
"doctor": cmd_doctor,
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
|
|
1481
|
+
def main():
|
|
1482
|
+
try:
|
|
1483
|
+
request = json.load(sys.stdin)
|
|
1484
|
+
except Exception as exc:
|
|
1485
|
+
sys.stdout.write(json.dumps({"ok": False, "error": {"code": "BAD_REQUEST", "message": "invalid JSON on stdin: " + str(exc)}}))
|
|
1486
|
+
return 0
|
|
1487
|
+
command = request.get("cmd") or ""
|
|
1488
|
+
handler = HANDLERS.get(command)
|
|
1489
|
+
if handler is None:
|
|
1490
|
+
sys.stdout.write(json.dumps({"ok": False, "error": {"code": "UNKNOWN_COMMAND", "message": "unknown bridge command: " + str(command)}}))
|
|
1491
|
+
return 0
|
|
1492
|
+
try:
|
|
1493
|
+
data = handler(request.get("config") or {}, request.get("args") or {})
|
|
1494
|
+
sys.stdout.write(json.dumps({"ok": True, "data": jsonable(data)}, ensure_ascii=True))
|
|
1495
|
+
return 0
|
|
1496
|
+
except BridgeError as exc:
|
|
1497
|
+
sys.stdout.write(json.dumps({"ok": False, "error": {"code": exc.code, "message": exc.message, "detail": jsonable(exc.detail)}}, ensure_ascii=True))
|
|
1498
|
+
return 0
|
|
1499
|
+
except Exception as exc:
|
|
1500
|
+
traceback.print_exc(file=sys.stderr)
|
|
1501
|
+
sys.stdout.write(json.dumps({"ok": False, "error": {"code": "INTERNAL", "message": type(exc).__name__ + ": " + str(exc)}}, ensure_ascii=True))
|
|
1502
|
+
return 1
|
|
1503
|
+
|
|
1504
|
+
|
|
1505
|
+
if __name__ == "__main__":
|
|
1506
|
+
sys.exit(main())
|