dsh-labscript 0.0.0-stage → 0.2.0
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 +19 -0
- package/LICENSE +21 -0
- package/README.md +154 -3
- package/cordis.patch.yml +6 -0
- package/docs/INSTALL.zh.md +54 -0
- package/docs/README.zh.md +57 -0
- package/examples/config.example.yml +33 -0
- package/examples/postprocess_demo.py +19 -0
- package/examples/protocols/demo_scan.yaml +18 -0
- package/lib/client.js +193 -0
- package/lib/index.js +138 -0
- package/lib/protocols.js +112 -0
- package/lib/runtime.js +326 -0
- package/lib/skill.js +60 -0
- package/lib/store.mjs +136 -0
- package/lib/tools.js +432 -0
- package/package.json +69 -6
- package/python/bridge.py +1445 -0
- package/python/tests/run_tests.py +239 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"""Self-contained tests for the python bridge core (mock backend only).
|
|
2
|
+
|
|
3
|
+
Run: python python/tests/run_tests.py
|
|
4
|
+
No pytest, no labscript, no hardware: numpy and h5py are enough.
|
|
5
|
+
"""
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import shutil
|
|
9
|
+
import sys
|
|
10
|
+
import tempfile
|
|
11
|
+
|
|
12
|
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
13
|
+
sys.path.insert(0, os.path.dirname(HERE))
|
|
14
|
+
EXAMPLES = os.path.join(os.path.dirname(os.path.dirname(HERE)), "examples")
|
|
15
|
+
|
|
16
|
+
import bridge
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def make_config(tmpdir):
|
|
20
|
+
return {
|
|
21
|
+
"backend": "mock-labscript",
|
|
22
|
+
"mock": {"stateDir": os.path.join(tmpdir, "mock")},
|
|
23
|
+
"panelHistoryDir": os.path.join(tmpdir, "history"),
|
|
24
|
+
"calibrationLog": os.path.join(tmpdir, "calibration.jsonl"),
|
|
25
|
+
"engage": {"timeoutS": 10, "pollS": 0.05, "settleS": 0.05},
|
|
26
|
+
"postprocess": {"entry": "postprocess_demo:analyze", "pythonPaths": [EXAMPLES]},
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def protocol_raw():
|
|
31
|
+
return {
|
|
32
|
+
"name": "demo_scan",
|
|
33
|
+
"labscript": "/tmp/mock_sequence.py",
|
|
34
|
+
"set": {"amplitude": 0.5},
|
|
35
|
+
"scan": {"duration_test_us": "np.linspace(0, 2, 3)"},
|
|
36
|
+
"reps": 2,
|
|
37
|
+
"postprocess": "demo.peaks",
|
|
38
|
+
"finish": {
|
|
39
|
+
"restore": True,
|
|
40
|
+
"panel": {"duration_test_us": "fit.center"},
|
|
41
|
+
"history": {"demo.peaks.center_us": "fit.center"},
|
|
42
|
+
},
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_scan_expressions():
|
|
47
|
+
assert bridge.eval_scan_expr("np.arange(0, 1, 0.25)").size == 4
|
|
48
|
+
assert bridge.eval_scan_expr("np.linspace(0, 2, 5)").size == 5
|
|
49
|
+
assert bridge.eval_scan_expr([0, 1, 2]).size == 3
|
|
50
|
+
assert bridge.eval_scan_expr(3).size == 1
|
|
51
|
+
for bad in ["os.system('x')", "__import__('os')", "np.ones(3)" ]:
|
|
52
|
+
try:
|
|
53
|
+
bridge.eval_scan_expr(bad)
|
|
54
|
+
except bridge.BridgeError as exc:
|
|
55
|
+
assert exc.code == "BAD_SCAN_EXPR", exc.message
|
|
56
|
+
else:
|
|
57
|
+
raise AssertionError("expected rejection for " + bad)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_mock_status_and_set():
|
|
61
|
+
tmp = tempfile.mkdtemp(prefix="dsh-labscript-py-")
|
|
62
|
+
try:
|
|
63
|
+
cfg = make_config(tmp)
|
|
64
|
+
status = bridge.cmd_status(cfg, {})
|
|
65
|
+
assert status["n_shots"] == 1
|
|
66
|
+
out = bridge.cmd_set(cfg, {"values": {"amplitude": 0.7}, "reps": 2, "note": "test"})
|
|
67
|
+
assert out["n_shots"] == 2
|
|
68
|
+
assert out["missing"] == []
|
|
69
|
+
assert out["snapshot"]["name"].endswith(".json")
|
|
70
|
+
assert out["readback"]["amplitude"] == 0.7
|
|
71
|
+
raw = bridge.cmd_status(cfg, {"raw": True})
|
|
72
|
+
assert raw["raw_globals"]["amplitude"] == "0.7"
|
|
73
|
+
restored = bridge.cmd_restore(cfg, {"snapshot": "latest"})
|
|
74
|
+
assert restored["restored_from"]["file"]
|
|
75
|
+
assert bridge.cmd_status(cfg, {})["n_shots"] == 1
|
|
76
|
+
finally:
|
|
77
|
+
shutil.rmtree(tmp, ignore_errors=True)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_protocol_end_to_end():
|
|
81
|
+
tmp = tempfile.mkdtemp(prefix="dsh-labscript-protocol-")
|
|
82
|
+
try:
|
|
83
|
+
cfg = make_config(tmp)
|
|
84
|
+
planned = bridge.cmd_protocol_plan(cfg, {"protocol": protocol_raw()})
|
|
85
|
+
assert planned["plan"]["expected_n_shots"] == 6
|
|
86
|
+
started = bridge.cmd_protocol_start(cfg, {"protocol": protocol_raw(), "note": "cal demo"})
|
|
87
|
+
assert started["n_shots_at_submit"] == 6
|
|
88
|
+
run_folder = started["engage"]["run_folder"]
|
|
89
|
+
waited = bridge.wait_for_run(cfg, run_folder, 6, 10)
|
|
90
|
+
assert waited["complete"] is True, waited
|
|
91
|
+
assert waited["n_h5"] == 6
|
|
92
|
+
final = bridge.cmd_protocol_finalize(cfg, {
|
|
93
|
+
"protocol": protocol_raw(),
|
|
94
|
+
"run_folder": run_folder,
|
|
95
|
+
"snapshot": started["snapshot"],
|
|
96
|
+
})
|
|
97
|
+
assert final["postprocess"]["fit"]["center"] == 2.5
|
|
98
|
+
assert final["restore"]["restored"] is True
|
|
99
|
+
panel = bridge.cmd_status(cfg, {})["globals"]
|
|
100
|
+
assert panel["duration_test_us"] == 2.5
|
|
101
|
+
assert panel["amplitude"] == 0.5
|
|
102
|
+
with open(cfg["calibrationLog"], "r", encoding="utf-8") as handle:
|
|
103
|
+
rows = [json.loads(line) for line in handle if line.strip()]
|
|
104
|
+
assert len(rows) == 1
|
|
105
|
+
assert rows[0]["key"] == "demo.peaks.center_us"
|
|
106
|
+
assert rows[0]["value"] == 2.5
|
|
107
|
+
events = os.path.join(cfg["panelHistoryDir"], "events.jsonl")
|
|
108
|
+
assert os.path.isfile(events)
|
|
109
|
+
assert len(open(events, encoding="utf-8").readlines()) >= 2
|
|
110
|
+
finally:
|
|
111
|
+
shutil.rmtree(tmp, ignore_errors=True)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def test_hard_limits():
|
|
115
|
+
tmp = tempfile.mkdtemp(prefix="dsh-labscript-limits-")
|
|
116
|
+
try:
|
|
117
|
+
cfg = make_config(tmp)
|
|
118
|
+
limits_path = os.path.join(tmp, "limits.json")
|
|
119
|
+
with open(limits_path, "w", encoding="utf-8") as handle:
|
|
120
|
+
json.dump({"limits": {"amplitude": {"max": 0.4}}, "run_limits": {"max_total_shots": 4}}, handle)
|
|
121
|
+
cfg["hardLimitsFile"] = limits_path
|
|
122
|
+
raw = protocol_raw()
|
|
123
|
+
try:
|
|
124
|
+
bridge.cmd_protocol_plan(cfg, {"protocol": raw})
|
|
125
|
+
except bridge.BridgeError as exc:
|
|
126
|
+
assert exc.code == "HARD_LIMIT_EXCEEDED", exc.message
|
|
127
|
+
findings = exc.detail["findings"]
|
|
128
|
+
assert any(item["kind"] == "over_max" for item in findings)
|
|
129
|
+
else:
|
|
130
|
+
raise AssertionError("expected hard limit rejection")
|
|
131
|
+
raw["set"] = {}
|
|
132
|
+
raw["scan"] = {"duration_test_us": "np.linspace(0, 2, 3)"}
|
|
133
|
+
raw["reps"] = 3
|
|
134
|
+
try:
|
|
135
|
+
bridge.cmd_protocol_plan(cfg, {"protocol": raw})
|
|
136
|
+
except bridge.BridgeError as exc:
|
|
137
|
+
findings = exc.detail["findings"]
|
|
138
|
+
assert any(item["kind"] == "over_max_total_shots" for item in findings)
|
|
139
|
+
else:
|
|
140
|
+
raise AssertionError("expected shot limit rejection")
|
|
141
|
+
finally:
|
|
142
|
+
shutil.rmtree(tmp, ignore_errors=True)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def test_array_values_through_set():
|
|
146
|
+
tmp = tempfile.mkdtemp(prefix="dsh-labscript-array-")
|
|
147
|
+
try:
|
|
148
|
+
cfg = make_config(tmp)
|
|
149
|
+
out = bridge.cmd_set(cfg, {"values": {"duration_test_us": [0.0, 0.5, 1.0]}, "note": "array set"})
|
|
150
|
+
assert out["n_shots"] == 3, out
|
|
151
|
+
raw = out["raw_readback"]["duration_test_us"]
|
|
152
|
+
assert raw.startswith("np.linspace(") or raw.startswith("np.array(["), raw
|
|
153
|
+
raw = bridge.cmd_status(cfg, {"raw": True})
|
|
154
|
+
assert raw["globals"] is not None
|
|
155
|
+
finally:
|
|
156
|
+
shutil.rmtree(tmp, ignore_errors=True)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def test_data_reading():
|
|
160
|
+
tmp = tempfile.mkdtemp(prefix="dsh-labscript-data-")
|
|
161
|
+
try:
|
|
162
|
+
cfg = make_config(tmp)
|
|
163
|
+
started = bridge.cmd_protocol_start(cfg, {"protocol": protocol_raw()})
|
|
164
|
+
run_folder = started["engage"]["run_folder"]
|
|
165
|
+
data = bridge.cmd_data(cfg, {"run_folder": run_folder, "scan": "duration_test_us"})
|
|
166
|
+
assert data["n_h5"] == 6
|
|
167
|
+
assert data["scan_table"]["n_points"] == 6
|
|
168
|
+
assert data["globals"]["amplitude"] == 0.5
|
|
169
|
+
keys = data["scan_table"]["rows"][0].keys()
|
|
170
|
+
assert any(str(key).startswith("demo.") for key in keys), list(keys)
|
|
171
|
+
finally:
|
|
172
|
+
shutil.rmtree(tmp, ignore_errors=True)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def test_selective_restore():
|
|
176
|
+
tmp = tempfile.mkdtemp(prefix="dsh-labscript-selective-")
|
|
177
|
+
try:
|
|
178
|
+
cfg = make_config(tmp)
|
|
179
|
+
bridge.cmd_set(cfg, {"values": {"amplitude": 0.61}})
|
|
180
|
+
second = bridge.cmd_set(cfg, {"values": {"do_acquire": False}})
|
|
181
|
+
bridge.cmd_set(cfg, {"values": {"amplitude": 0.66}})
|
|
182
|
+
restored = bridge.cmd_restore(cfg, {"snapshot": second["snapshot"]["name"]})
|
|
183
|
+
panel = bridge.cmd_status(cfg, {})["globals"]
|
|
184
|
+
assert restored["selective"] is True, restored
|
|
185
|
+
assert panel["amplitude"] == 0.66, panel
|
|
186
|
+
assert panel["do_acquire"] is True, panel
|
|
187
|
+
finally:
|
|
188
|
+
shutil.rmtree(tmp, ignore_errors=True)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def test_wait_diagnosis_compiled_not_executed():
|
|
192
|
+
import h5py
|
|
193
|
+
tmp = tempfile.mkdtemp(prefix="dsh-labscript-diag-")
|
|
194
|
+
try:
|
|
195
|
+
cfg = make_config(tmp)
|
|
196
|
+
cfg["engage"] = {"timeoutS": 5, "pollS": 0.05, "settleS": 0.1, "stallS": 0.3}
|
|
197
|
+
run_dir = os.path.join(tmp, "compiled_run")
|
|
198
|
+
os.makedirs(run_dir, exist_ok=True)
|
|
199
|
+
with h5py.File(os.path.join(run_dir, "shot_0000.h5"), "w") as handle:
|
|
200
|
+
group = handle.create_group("globals")
|
|
201
|
+
group.attrs["dummy_iterator"] = 0
|
|
202
|
+
result = bridge.wait_for_run(cfg, run_dir, 1, 4)
|
|
203
|
+
assert result["complete"] is False, result
|
|
204
|
+
assert result["diagnosis"] == "compiled_not_executed", result
|
|
205
|
+
assert result["executed"] is False, result
|
|
206
|
+
finally:
|
|
207
|
+
shutil.rmtree(tmp, ignore_errors=True)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def test_doctor_mock():
|
|
211
|
+
tmp = tempfile.mkdtemp(prefix="dsh-labscript-doctor-")
|
|
212
|
+
try:
|
|
213
|
+
cfg = make_config(tmp)
|
|
214
|
+
out = bridge.cmd_doctor(cfg, {})
|
|
215
|
+
names = dict((check["name"], check["status"]) for check in out["checks"])
|
|
216
|
+
assert names.get("python") == "ok", names
|
|
217
|
+
assert names.get("numpy") == "ok", names
|
|
218
|
+
assert names.get("h5py") == "ok", names
|
|
219
|
+
assert names.get("backend") == "ok", names
|
|
220
|
+
finally:
|
|
221
|
+
shutil.rmtree(tmp, ignore_errors=True)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def main():
|
|
225
|
+
tests = [value for name, value in sorted(globals().items()) if name.startswith("test_")]
|
|
226
|
+
failures = 0
|
|
227
|
+
for test in tests:
|
|
228
|
+
try:
|
|
229
|
+
test()
|
|
230
|
+
print("ok " + test.__name__)
|
|
231
|
+
except Exception as exc: # noqa: BLE001
|
|
232
|
+
failures += 1
|
|
233
|
+
print("FAIL " + test.__name__ + ": " + repr(exc))
|
|
234
|
+
print(str(len(tests)) + " tests, " + str(failures) + " failures")
|
|
235
|
+
return 1 if failures else 0
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
if __name__ == "__main__":
|
|
239
|
+
sys.exit(main())
|