dsh-labscript 0.3.1 → 0.3.3
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 +45 -20
- package/README.md +8 -3
- package/docs/README.zh.md +3 -0
- package/examples/postprocess_demo.py +4 -3
- package/lib/runtime.js +2 -4
- package/lib/skill.js +0 -2
- package/lib/store.mjs +1 -1
- package/lib/tools.js +14 -6
- package/package.json +1 -1
- package/python/bridge.py +70 -139
- package/python/tests/run_tests.py +172 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,24 +1,46 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.3.
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
3
|
+
## 0.3.3
|
|
4
|
+
|
|
5
|
+
Makes the tools report what actually happened instead of hiding it, and removes a backend
|
|
6
|
+
nobody could reach. Net change is negative: about 40 lines of reporting added, a dead
|
|
7
|
+
backend removed.
|
|
8
|
+
|
|
9
|
+
- `labs_doctor` no longer imports labscript to check the environment. That import has side
|
|
10
|
+
effects (labscript's double-import denier walks the stack and leaves half-initialised
|
|
11
|
+
modules behind) and it broke the runmanager probe that runs right after it in the same
|
|
12
|
+
process, so a working environment was reported as two `[fail]` lines pointing at an h5py
|
|
13
|
+
path. The check is now `importlib.util.find_spec` (presence only: nothing in this plugin
|
|
14
|
+
imports labscript), error messages keep the exception type instead of only its text, and
|
|
15
|
+
doctor prints `python-executable` so "wrong interpreter" is visible at a glance.
|
|
16
|
+
- `labs_status` prints a panel field passed as if it were a global (`keys: ["n_shots"]`) as
|
|
17
|
+
`<value> (panel field, not a global)` instead of `<missing>`.
|
|
18
|
+
- `finish.panel` and `finish.history` report values they cannot write as `finish.skipped`,
|
|
19
|
+
with the dotted path and the reason (`missing` / `not a number (str)`); `labs_wait` renders
|
|
20
|
+
one `finish skipped:` line. These used to be dropped without a word, which reads exactly
|
|
21
|
+
like "the calibration number did not land".
|
|
22
|
+
- Postprocess receives `scan_globals` (every axis, in plan order) when its signature accepts
|
|
23
|
+
it; `scan_global` is unchanged. Dispatch uses `inspect.signature` instead of retrying on
|
|
24
|
+
`TypeError`, which also stops a `TypeError` raised inside the analysis function from running
|
|
25
|
+
it a second time.
|
|
26
|
+
- Removed the `http-sim` backend: an HTTP client for a local simulator service, added for a
|
|
27
|
+
one-off benchmark in v0.1 and later renamed to hide the internal service name. It had no
|
|
28
|
+
tests, no documentation and no entry on the Plugins page. `backend` is `labscript` or
|
|
29
|
+
`mock-labscript` again.
|
|
30
|
+
- Tests: 38 python + 49 node.
|
|
31
|
+
|
|
32
|
+
## 0.3.2
|
|
11
33
|
|
|
12
34
|
Implements the upstream request written from the field (labscript-suite workflow terms and
|
|
13
35
|
`runmanager.remote` limits). No output field is renamed or removed.
|
|
14
36
|
|
|
15
|
-
- `n_shots` is
|
|
37
|
+
- `n_shots` is documented and rendered as the authoritative shot count. The scan-axes line is
|
|
16
38
|
labelled a heuristic: runmanager expands list/array globals but passes tuples through, and an
|
|
17
39
|
expansion setting of `none` turns an array into a non-axis; neither is visible through
|
|
18
|
-
`runmanager.remote`. A mismatch
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
40
|
+
`runmanager.remote`. A mismatch reads "n_shots is authoritative" instead of printing two numbers
|
|
41
|
+
with no verdict. The bridge's axis guess ignores tuples and dicts, and the mock backend follows the
|
|
42
|
+
same rule (it used to multiply tuples in, pinning the wrong semantics into the tests); the mock
|
|
43
|
+
also keeps Python types, without which the tuple/list distinction was untestable.
|
|
22
44
|
- Skill and tool descriptions no longer tell the agent to "read the runmanager output": compilation
|
|
23
45
|
errors appear only in runmanager's output tab (the compiler subprocess writes its traceback to
|
|
24
46
|
stderr and runmanager redirects it to an in-memory Qt widget), so the instruction is now to say
|
|
@@ -28,16 +50,17 @@ Implements the upstream request written from the field (labscript-suite workflow
|
|
|
28
50
|
- `compiled_not_executed` explains the other official causes: BLACS may keep a shot out of its
|
|
29
51
|
queue (the shot's connection table must be a subset of the lab connection table) or put it back
|
|
30
52
|
at the head after a device programming timeout; all of that is visible only in the BLACS GUI.
|
|
31
|
-
More shot files than expected
|
|
32
|
-
|
|
53
|
+
More shot files than expected points at a changed panel or BLACS Repeat all / Repeat last, which
|
|
54
|
+
re-runs completed shots without creating new files.
|
|
33
55
|
- `labs_status` reports `shuffle` (from `get_shuffle`; "unknown" when that runmanager version has
|
|
34
56
|
no such method). With shuffle on, shot order is not axis order: read each shot's parameters from
|
|
35
57
|
its own h5 globals.
|
|
36
58
|
- runmanager host/port/timeout default to labscript's labconfig when unset, resolved by
|
|
37
59
|
`runmanager.remote.Client` itself (`[servers]/[ports]/[timeouts]`, port 42523, timeout 60 s);
|
|
38
|
-
configured values still win. The Plugins page
|
|
39
|
-
|
|
40
|
-
|
|
60
|
+
configured values still win. The Plugins page leaves host/port blank for that reason, and clearing
|
|
61
|
+
them hands the endpoint back to labconfig. `labs_doctor` prints the effective endpoint and its
|
|
62
|
+
source. The DSH-side bridge budget (`requestTimeoutMs`) defaults to 180 s so it stays above
|
|
63
|
+
runmanager's client timeout instead of racing it.
|
|
41
64
|
- mock backend: `mock.errorInGlobals` makes `error_in_globals` true so the engagement refusal can
|
|
42
65
|
be tested, and `mock.shuffle` sets the reported shuffle flag.
|
|
43
66
|
- Docs: a terminology table maps the plugin's short names to the suite's (shot output folder,
|
|
@@ -45,8 +68,10 @@ Implements the upstream request written from the field (labscript-suite workflow
|
|
|
45
68
|
- Compilation logs stay out of scope: nothing about a compilation failure is written anywhere
|
|
46
69
|
readable (see README), so no `labs_compile_log` was added; a guessed diagnosis would be worse
|
|
47
70
|
than none.
|
|
48
|
-
-
|
|
49
|
-
|
|
71
|
+
- Tool descriptions stay close to their 0.2.8 size (+147 characters): the explanations live in the
|
|
72
|
+
runtime skill (loaded on demand) and in the wait/engage diagnostics (shown only when they matter),
|
|
73
|
+
so they do not sit in context on every turn.
|
|
74
|
+
- Tests: 33 python + 47 node.
|
|
50
75
|
|
|
51
76
|
## 0.2.8
|
|
52
77
|
|
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ files. It does not require any custom BLACS/lyse patch. Everything lab-specific
|
|
|
9
9
|
|
|
10
10
|
## Status
|
|
11
11
|
|
|
12
|
-
v0.
|
|
12
|
+
v0.3.3. The mock-labscript backend, the protocol/postprocess flow and the
|
|
13
13
|
snapshot/diagnosis behaviour are covered by node and python tests. A client
|
|
14
14
|
configuration page (Plugins page) and a read-only `labs_doctor` are included. The real
|
|
15
15
|
labscript backend is implemented against the stock runmanager remote API but has
|
|
@@ -75,6 +75,11 @@ guess panel state).
|
|
|
75
75
|
labs_protocol run demo_scan # set + scan + reps + engage, returns immediately
|
|
76
76
|
labs_wait # waits, then postprocess -> finish -> restore
|
|
77
77
|
|
|
78
|
+
`postprocess.entry` is called as `analyze(run_folder, scan_global, reps, name)`; when the
|
|
79
|
+
function accepts it, `scan_globals=[...]` (every axis, in order) is passed as well, so a 2D
|
|
80
|
+
scan is not limited to its first axis. A `finish` value that is missing or not a number is
|
|
81
|
+
reported as `skipped` and printed by `labs_wait` instead of being dropped silently.
|
|
82
|
+
|
|
78
83
|
## Configuration
|
|
79
84
|
|
|
80
85
|
Add one row to your DSH profile patch (see examples/config.example.yml):
|
|
@@ -140,8 +145,8 @@ is not a physics simulation.
|
|
|
140
145
|
## Development
|
|
141
146
|
|
|
142
147
|
npm install # only runtime dependency: yaml
|
|
143
|
-
npm test # node --test,
|
|
144
|
-
python python/tests/run_tests.py #
|
|
148
|
+
npm test # node --test, 49 tests (fake bridge, no python needed)
|
|
149
|
+
python python/tests/run_tests.py # 38 tests (needs numpy + h5py)
|
|
145
150
|
LABSCRIPT_PYTHON=/path/to/python3 node scripts/demo.mjs # end-to-end mock demo
|
|
146
151
|
|
|
147
152
|
A fresh clone must run `npm install` before `npm test`; the CI workflow does this.
|
package/docs/README.zh.md
CHANGED
|
@@ -49,6 +49,9 @@ hard limits 全部通过配置接入。
|
|
|
49
49
|
|
|
50
50
|
postprocess 是后处理:跑完后调用配置的 Python 函数(entry + pythonPaths),
|
|
51
51
|
返回 dict 供 finish 写回。旧协议里的 analysis: 字段当作 postprocess 别名。
|
|
52
|
+
函数按 `analyze(run_folder, scan_global, reps, name)` 调用;签名接受时还会传
|
|
53
|
+
`scan_globals=[...]`(全部扫描轴、按顺序),二维扫描因此能拿到第二轴。finish 解析到
|
|
54
|
+
缺失或非数字的值不再静默丢弃,会在 labs_wait 输出里以 `finish skipped` 列出。
|
|
52
55
|
|
|
53
56
|
## 边界
|
|
54
57
|
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
"""Example postprocess entry: after a run, return the numbers that finish.panel
|
|
2
2
|
and finish.history write back. Replace the fake model with your lab analysis.
|
|
3
3
|
|
|
4
|
-
Signature: analyze(run_folder=..., scan_global=..., reps=..., name=...)
|
|
5
|
-
|
|
4
|
+
Signature: analyze(run_folder=..., scan_global=..., reps=..., name=..., scan_globals=...)
|
|
5
|
+
scan_global is the first axis; scan_globals is every axis in order (a 2D scan needs both).
|
|
6
|
+
Returns a JSON-serializable dict; finish.panel reads the numbers back out of it.
|
|
6
7
|
"""
|
|
7
8
|
|
|
8
9
|
|
|
9
|
-
def analyze(run_folder, scan_global=None, reps=None, name=None):
|
|
10
|
+
def analyze(run_folder, scan_global=None, reps=None, name=None, scan_globals=None):
|
|
10
11
|
# Placeholder numbers so the write-back path is demonstrable end to end.
|
|
11
12
|
return {
|
|
12
13
|
"fit": {"center": 2.5, "pi_duration_us": 1.25, "r2": 0.99},
|
package/lib/runtime.js
CHANGED
|
@@ -29,7 +29,7 @@ const VERSION = PACKAGE.version
|
|
|
29
29
|
const DEFAULT_ENGAGE_TIMEOUT_S = 900
|
|
30
30
|
const DEFAULT_APPROVAL = { engage: true, protocolRun: true, protocolNames: true }
|
|
31
31
|
const DEFAULT_BACKEND = 'mock-labscript'
|
|
32
|
-
const BACKENDS = ['labscript', 'mock-labscript'
|
|
32
|
+
const BACKENDS = ['labscript', 'mock-labscript']
|
|
33
33
|
|
|
34
34
|
export class LabError extends Error {
|
|
35
35
|
constructor(code, message, detail) {
|
|
@@ -44,7 +44,7 @@ function normalizeConfig(raw) {
|
|
|
44
44
|
const input = raw && typeof raw === 'object' ? raw : {}
|
|
45
45
|
const backend = String(input.backend || DEFAULT_BACKEND).toLowerCase()
|
|
46
46
|
if (!BACKENDS.includes(backend)) {
|
|
47
|
-
throw new LabError('BAD_CONFIG', 'backend must be "labscript"
|
|
47
|
+
throw new LabError('BAD_CONFIG', 'backend must be "labscript" or "mock-labscript", got: ' + backend)
|
|
48
48
|
}
|
|
49
49
|
const approval = Object.assign({}, DEFAULT_APPROVAL, input.approval || {})
|
|
50
50
|
return {
|
|
@@ -67,7 +67,6 @@ function normalizeConfig(raw) {
|
|
|
67
67
|
protocolNames: approval.protocolNames !== false,
|
|
68
68
|
},
|
|
69
69
|
mock: input.mock || {},
|
|
70
|
-
simulator: input.simulator || {},
|
|
71
70
|
dshHome: input.dshHome || '',
|
|
72
71
|
// Must stay above runmanager's own client timeout (labconfig [timeouts]
|
|
73
72
|
// communication_timeout, default 60 s): the bridge is killed when this expires, which
|
|
@@ -89,7 +88,6 @@ function bridgePayload(config) {
|
|
|
89
88
|
engage: Object.assign({ timeoutS: DEFAULT_ENGAGE_TIMEOUT_S }, config.engage || {}),
|
|
90
89
|
panelHistoryDir: config.panelHistoryDir,
|
|
91
90
|
mock: config.mock,
|
|
92
|
-
simulator: config.simulator,
|
|
93
91
|
}
|
|
94
92
|
}
|
|
95
93
|
|
package/lib/skill.js
CHANGED
|
@@ -35,8 +35,6 @@ export const SKILL_CONTENT = [
|
|
|
35
35
|
' n_executed (or the diagnosis) says whether shots actually acquired data.',
|
|
36
36
|
'- shuffle true means shot order is not axis order: read each shot parameters from its',
|
|
37
37
|
' own h5 globals, never from the shot index.',
|
|
38
|
-
'- Never name a global like a Python builtin or a name from "from labscript import *":',
|
|
39
|
-
' runmanager accepts it, labscript fails at compile time, and only the output tab shows it.',
|
|
40
38
|
'- labs_protocol run submits a fixed protocol; labs_wait finalizes it (postprocess,',
|
|
41
39
|
' finish write-back, panel restore).',
|
|
42
40
|
'- backend defaults to mock-labscript; real hardware is only used when the config says',
|
package/lib/store.mjs
CHANGED
|
@@ -25,7 +25,7 @@ export function statePath(home) {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
const UI_KEYS = ['backend', 'python', 'runmanager', 'protocolsDir']
|
|
28
|
-
const BACKENDS = ['labscript', 'mock-labscript'
|
|
28
|
+
const BACKENDS = ['labscript', 'mock-labscript']
|
|
29
29
|
|
|
30
30
|
export function normalizeUiConfig(input) {
|
|
31
31
|
const source = input && typeof input === 'object' ? input : {}
|
package/lib/tools.js
CHANGED
|
@@ -62,7 +62,12 @@ function globalLines(args, value) {
|
|
|
62
62
|
const out = ['globals:']
|
|
63
63
|
for (const key of keys) {
|
|
64
64
|
const shown = raw ? raw[key] : globals[key]
|
|
65
|
-
|
|
65
|
+
// A panel field requested as if it were a global (n_shots is the usual one) should
|
|
66
|
+
// say what it is instead of looking like a global that vanished.
|
|
67
|
+
let text = '<missing>'
|
|
68
|
+
if (shown !== undefined) text = globalText(shown)
|
|
69
|
+
else if (Object.prototype.hasOwnProperty.call(value, key)) text = globalText(value[key]) + ' (panel field, not a global)'
|
|
70
|
+
out.push(' ' + key + ' = ' + text)
|
|
66
71
|
}
|
|
67
72
|
return out
|
|
68
73
|
}
|
|
@@ -255,7 +260,7 @@ export function registerTools(ctx, runtime) {
|
|
|
255
260
|
|
|
256
261
|
ctx.tools.register({
|
|
257
262
|
name: 'labs_status',
|
|
258
|
-
description: 'Read the labscript runmanager panel: loaded labscript, globals (evaluated; raw
|
|
263
|
+
description: 'Read the labscript runmanager panel: loaded labscript, globals (evaluated; raw with raw=true), n_shots, error_in_globals, run_shots, shuffle, shot output folder (run folder in this tool; created by engage), pending run progress, snapshot directory. Read-only; call this before any write. Pass keys=[...] (leaf global names; UI group names are not globals) to print panel values (240 chars each), missing keys show as <missing>; keys=["*"] prints every global. scan axes is a heuristic (runmanager does not expand tuples and expansion=none is invisible), and n_shots is authoritative: when they differ, report n_shots. shuffle true means shot order is not axis order. Paths are already mapped through pathMap; do not map them again.',
|
|
259
264
|
parameters: {
|
|
260
265
|
type: 'object',
|
|
261
266
|
properties: {
|
|
@@ -289,7 +294,7 @@ export function registerTools(ctx, runtime) {
|
|
|
289
294
|
|
|
290
295
|
ctx.tools.register({
|
|
291
296
|
name: 'labs_set',
|
|
292
|
-
description: 'Write runmanager panel globals. One call can do the whole step: values are JSON scalars (bool/number/string); with raw=true a string value is written as an expression (e.g. np.arange(0.5, 6.7, 0.2)) while bool/number values are still converted
|
|
297
|
+
description: 'Write runmanager panel globals. One call can do the whole step: values are JSON scalars (bool/number/string); with raw=true a string value is written as an expression (e.g. np.arange(0.5, 6.7, 0.2)) while bool/number values are still converted (true -> True, 2.72 -> 2.72), so scalars, an expression scan axis and reps=N can be combined. reps=N writes the configured repetition global as np.arange(N). A global name must not shadow a Python builtin or a "from labscript import *" name: runmanager accepts it, labscript fails at compile time (visible only in runmanager output tab). A panel snapshot is taken before the write. Does not run anything.',
|
|
293
298
|
parameters: {
|
|
294
299
|
type: 'object',
|
|
295
300
|
properties: {
|
|
@@ -319,7 +324,7 @@ export function registerTools(ctx, runtime) {
|
|
|
319
324
|
|
|
320
325
|
ctx.tools.register({
|
|
321
326
|
name: 'labs_engage',
|
|
322
|
-
description: 'Submit the current panel as one sequence:
|
|
327
|
+
description: 'Submit the current panel as one sequence: one shot file per parameter point, handed to BLACS. Returns immediately with run_folder (the shot output folder), n_shots_at_submit and expected_n_shots; use labs_wait later. It cannot see compilation errors: runmanager shows those only in its output tab. Refuses when error_in_globals is true or another run is pending; a pending run with no shot at all is dropped after config.engage.staleS (default 180 s) and reported as dropped_stale_pending.',
|
|
323
328
|
parameters: {
|
|
324
329
|
type: 'object',
|
|
325
330
|
properties: {
|
|
@@ -363,7 +368,7 @@ export function registerTools(ctx, runtime) {
|
|
|
363
368
|
|
|
364
369
|
ctx.tools.register({
|
|
365
370
|
name: 'labs_wait',
|
|
366
|
-
description: 'Wait for the pending run (sequence) to finish (blocking, timeout in seconds). If it came from labs_protocol run, this call also finalizes it: postprocess, finish write-back
|
|
371
|
+
description: 'Wait for the pending run (sequence) to finish (blocking, timeout in seconds). If it came from labs_protocol run, this call also finalizes it: postprocess, finish write-back, panel restore. Returns expected vs actual shot files. A run folder that does not exist and is not the pending run fails immediately instead of waiting. Returns early with diagnosis no_shots or compile_stalled when no shot file appeared for config.engage.noShotS seconds (default 25): compilation errors are visible only in runmanager output tab, which this tool cannot read, so report that and ask the operator to paste the tab. diagnosis compiled_not_executed means shot files exist but BLACS acquired no data: check the BLACS GUI.',
|
|
367
372
|
parameters: {
|
|
368
373
|
type: 'object',
|
|
369
374
|
properties: {
|
|
@@ -382,6 +387,9 @@ export function registerTools(ctx, runtime) {
|
|
|
382
387
|
value.hint ? 'hint: ' + value.hint : '',
|
|
383
388
|
value.finalize && value.finalize.postprocess ? 'postprocess: ' + JSON.stringify(value.finalize.postprocess).slice(0, 400) : '',
|
|
384
389
|
value.finalize && value.finalize.finish ? 'finish: ' + JSON.stringify(value.finalize.finish).slice(0, 300) : '',
|
|
390
|
+
value.finalize && value.finalize.finish && Array.isArray(value.finalize.finish.skipped) && value.finalize.finish.skipped.length
|
|
391
|
+
? 'finish skipped: ' + value.finalize.finish.skipped.map((item) => item.key + ' <- ' + item.dotted + ' (' + item.reason + ')').join('; ')
|
|
392
|
+
: '',
|
|
385
393
|
value.finalize && value.finalize.restore ? 'restore: ' + (value.finalize.restore.restored ? 'yes' : 'no') : '',
|
|
386
394
|
]))),
|
|
387
395
|
async execute(args, exec) {
|
|
@@ -490,7 +498,7 @@ export function registerTools(ctx, runtime) {
|
|
|
490
498
|
|
|
491
499
|
ctx.tools.register({
|
|
492
500
|
name: 'labs_data',
|
|
493
|
-
description: 'Read a run folder (shot output folder) HDF5: first-shot globals (
|
|
501
|
+
description: 'Read a run folder (the shot output folder) HDF5: first-shot globals (keys=[...], ["*"] for all), results groups with results: true, and with scan: a per-shot axis summary (unique points, min/max, median step) plus numeric columns; limit prints up to 20 raw rows. Read-only. run_folder defaults to the pending run, else the last run submitted through this plugin (persisted across restarts); pass a historical folder explicitly. A missing folder is an error: the shot output folder is created by engage.',
|
|
494
502
|
parameters: {
|
|
495
503
|
type: 'object',
|
|
496
504
|
properties: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-labscript",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"description": "Control labscript-suite experiments from DeepSeek Harness: runmanager globals and scans, submits and waits, YAML fixed-operation protocols with postprocess write-back, panel snapshots, a built-in mock backend and a read-only doctor.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
package/python/bridge.py
CHANGED
|
@@ -15,6 +15,8 @@ import sys
|
|
|
15
15
|
import time
|
|
16
16
|
import datetime
|
|
17
17
|
import importlib
|
|
18
|
+
import importlib.util
|
|
19
|
+
import inspect
|
|
18
20
|
import traceback
|
|
19
21
|
|
|
20
22
|
|
|
@@ -318,7 +320,7 @@ class LabscriptBackend(object):
|
|
|
318
320
|
try:
|
|
319
321
|
from runmanager.remote import Client
|
|
320
322
|
except Exception as exc:
|
|
321
|
-
raise BridgeError("BACKEND_UNAVAILABLE", "labscript python environment not importable: " + str(exc))
|
|
323
|
+
raise BridgeError("BACKEND_UNAVAILABLE", "labscript python environment not importable: " + type(exc).__name__ + ": " + str(exc))
|
|
322
324
|
# Anything left unset is resolved by runmanager.remote.Client from labscript's
|
|
323
325
|
# labconfig ([servers] runmanager, [ports] runmanager, [timeouts]
|
|
324
326
|
# communication_timeout), the same way the runmanager GUI does it. Explicit
|
|
@@ -538,113 +540,10 @@ class MockBackend(object):
|
|
|
538
540
|
return {"aborted": True, "mode": "mock-labscript"}
|
|
539
541
|
|
|
540
542
|
|
|
541
|
-
class HttpSimBackend(object):
|
|
542
|
-
"""HTTP client for an optional local simulator service (development backend).
|
|
543
|
-
|
|
544
|
-
The simulator must speak the documented JSON/HTTP contract on a loopback address.
|
|
545
|
-
It is a development tool, not a physics backend for publication.
|
|
546
|
-
"""
|
|
547
|
-
|
|
548
|
-
def __init__(self, config):
|
|
549
|
-
simulator = config.get("simulator") or {}
|
|
550
|
-
url = str(simulator.get("url") or "http://127.0.0.1:18761").rstrip("/")
|
|
551
|
-
if not url.startswith("http://127.0.0.1:"):
|
|
552
|
-
raise BridgeError("BAD_CONFIG", "simulator.url must be http://127.0.0.1:<port>, got: " + url)
|
|
553
|
-
self.base = url
|
|
554
|
-
self.timeout = float(simulator.get("timeoutS") or 10)
|
|
555
|
-
self._opener = None
|
|
556
|
-
|
|
557
|
-
def opener(self):
|
|
558
|
-
import urllib.request
|
|
559
|
-
if self._opener is not None:
|
|
560
|
-
return self._opener
|
|
561
|
-
|
|
562
|
-
class NoRedirect(urllib.request.HTTPRedirectHandler):
|
|
563
|
-
def redirect_request(self, req, fp, code, msg, headers, newurl):
|
|
564
|
-
return None
|
|
565
|
-
|
|
566
|
-
self._opener = urllib.request.build_opener(NoRedirect, urllib.request.ProxyHandler({}))
|
|
567
|
-
return self._opener
|
|
568
|
-
|
|
569
|
-
def request(self, method, path, body=None):
|
|
570
|
-
import urllib.request
|
|
571
|
-
import urllib.error
|
|
572
|
-
data = None
|
|
573
|
-
headers = {}
|
|
574
|
-
if body is not None:
|
|
575
|
-
data = json.dumps(jsonable(body)).encode("utf-8")
|
|
576
|
-
headers["Content-Type"] = "application/json"
|
|
577
|
-
req = urllib.request.Request(self.base + path, data=data, headers=headers, method=method)
|
|
578
|
-
try:
|
|
579
|
-
with self.opener().open(req, timeout=self.timeout) as response:
|
|
580
|
-
raw = response.read().decode("utf-8")
|
|
581
|
-
except urllib.error.HTTPError as exc:
|
|
582
|
-
try:
|
|
583
|
-
payload = json.loads(exc.read().decode("utf-8"))
|
|
584
|
-
except Exception:
|
|
585
|
-
raise BridgeError("BACKEND_UNAVAILABLE", "http-sim HTTP " + str(exc.code) + " at " + path)
|
|
586
|
-
error = payload.get("error") or {}
|
|
587
|
-
raise BridgeError(str(error.get("code") or "BACKEND_UNAVAILABLE"), str(error.get("message") or "http-sim error"))
|
|
588
|
-
except Exception as exc:
|
|
589
|
-
raise BridgeError("BACKEND_UNAVAILABLE", "http-sim not reachable at " + self.base + ": " + str(exc))
|
|
590
|
-
try:
|
|
591
|
-
envelope = json.loads(raw)
|
|
592
|
-
except Exception:
|
|
593
|
-
raise BridgeError("BACKEND_UNAVAILABLE", "http-sim response is not JSON")
|
|
594
|
-
if envelope.get("ok") is not True:
|
|
595
|
-
error = envelope.get("error") or {}
|
|
596
|
-
raise BridgeError(str(error.get("code") or "BACKEND_UNAVAILABLE"), str(error.get("message") or "http-sim error"))
|
|
597
|
-
return envelope.get("data") or {}
|
|
598
|
-
|
|
599
|
-
def snapshot(self):
|
|
600
|
-
return self.request("GET", "/v1/rm/snapshot")
|
|
601
|
-
|
|
602
|
-
def get_globals(self, raw=False):
|
|
603
|
-
snap = self.snapshot()
|
|
604
|
-
key = "raw_globals" if raw else "globals"
|
|
605
|
-
return dict(snap.get(key) or {})
|
|
606
|
-
|
|
607
|
-
def set_globals(self, values, raw=False):
|
|
608
|
-
body = dict((k, v.tolist() if hasattr(v, "tolist") else v) for k, v in values.items())
|
|
609
|
-
data = self.request("POST", "/v1/rm/set_globals", {"values": body, "raw": bool(raw)})
|
|
610
|
-
return dict(data.get("written") or {})
|
|
611
|
-
|
|
612
|
-
def n_shots(self):
|
|
613
|
-
return int(self.snapshot().get("n_shots") or 0)
|
|
614
|
-
|
|
615
|
-
def error_in_globals(self):
|
|
616
|
-
return bool(self.snapshot().get("error_in_globals"))
|
|
617
|
-
|
|
618
|
-
def run_shots(self):
|
|
619
|
-
return True
|
|
620
|
-
|
|
621
|
-
def shuffle(self):
|
|
622
|
-
snap = self.snapshot()
|
|
623
|
-
return bool(snap.get("shuffle")) if "shuffle" in snap else None
|
|
624
|
-
|
|
625
|
-
def labscript_file(self):
|
|
626
|
-
return self.snapshot().get("labscript_file")
|
|
627
|
-
|
|
628
|
-
def shot_output_folder(self):
|
|
629
|
-
return self.snapshot().get("shot_output_folder")
|
|
630
|
-
|
|
631
|
-
def engage(self):
|
|
632
|
-
data = self.request("POST", "/v1/rm/engage", {})
|
|
633
|
-
if data.get("accepted") is not True:
|
|
634
|
-
raise BridgeError("ENGAGE_REJECTED", "http-sim did not accept engage: " + str(data.get("detail") or ""))
|
|
635
|
-
return {"accepted": True, "n_shots": data.get("n_shots"), "run_folder": data.get("run_folder"), "mode": "http-sim"}
|
|
636
|
-
|
|
637
|
-
def abort(self):
|
|
638
|
-
data = self.request("POST", "/v1/blacs/stop", {})
|
|
639
|
-
return {"aborted": True, "via": "http-sim/blacs/stop", "result": data}
|
|
640
|
-
|
|
641
|
-
|
|
642
543
|
def make_backend(config):
|
|
643
544
|
backend = (config.get("backend") or "mock-labscript").lower()
|
|
644
545
|
if backend == "mock-labscript":
|
|
645
546
|
return MockBackend(config)
|
|
646
|
-
if backend == "http-sim":
|
|
647
|
-
return HttpSimBackend(config)
|
|
648
547
|
if backend == "labscript":
|
|
649
548
|
return LabscriptBackend(config)
|
|
650
549
|
raise BridgeError("BAD_CONFIG", "unknown backend: " + backend)
|
|
@@ -791,7 +690,7 @@ def check_limits(config, set_values, scan_values, expected_n_shots):
|
|
|
791
690
|
# ---------------------------------------------------------------- postprocess
|
|
792
691
|
|
|
793
692
|
|
|
794
|
-
def run_postprocess(config, plan, run_folder,
|
|
693
|
+
def run_postprocess(config, plan, run_folder, scan_globals, reps):
|
|
795
694
|
name = plan.get("postprocess")
|
|
796
695
|
if not name:
|
|
797
696
|
return None
|
|
@@ -812,38 +711,61 @@ def run_postprocess(config, plan, run_folder, scan_global, reps):
|
|
|
812
711
|
func = getattr(module, func_name)
|
|
813
712
|
except Exception as exc:
|
|
814
713
|
raise BridgeError("POSTPROCESS_IMPORT_FAILED", "cannot import postprocess " + entry + ": " + str(exc))
|
|
714
|
+
axes = [str(key) for key in (scan_globals or [])]
|
|
715
|
+
scan_global = axes[0] if axes else None
|
|
716
|
+
kwargs = {"run_folder": run_folder, "scan_global": scan_global, "reps": reps, "name": name}
|
|
815
717
|
try:
|
|
816
|
-
|
|
817
|
-
except TypeError:
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
718
|
+
params = inspect.signature(func).parameters
|
|
719
|
+
except (TypeError, ValueError):
|
|
720
|
+
params = {}
|
|
721
|
+
if "scan_globals" in params or any(param.kind is param.VAR_KEYWORD for param in params.values()):
|
|
722
|
+
kwargs["scan_globals"] = axes
|
|
723
|
+
if not all(key in params for key in kwargs if key != "scan_globals"):
|
|
724
|
+
# Positional-only or differently named parameters: keep the historical call.
|
|
725
|
+
kwargs = {}
|
|
726
|
+
try:
|
|
727
|
+
result = func(**kwargs) if kwargs else func(run_folder, scan_global, reps, name)
|
|
822
728
|
except Exception as exc:
|
|
823
|
-
raise BridgeError("POSTPROCESS_FAILED", "postprocess raised: " + str(exc))
|
|
729
|
+
raise BridgeError("POSTPROCESS_FAILED", "postprocess raised: " + type(exc).__name__ + ": " + str(exc))
|
|
824
730
|
if not isinstance(result, dict):
|
|
825
731
|
raise BridgeError("POSTPROCESS_BAD_RESULT", "postprocess must return a dict, got " + type(result).__name__)
|
|
826
732
|
return jsonable(result)
|
|
827
733
|
|
|
828
734
|
|
|
829
|
-
|
|
735
|
+
_MISSING = object()
|
|
736
|
+
|
|
737
|
+
|
|
738
|
+
def resolve_dotted(obj, dotted, default=None):
|
|
830
739
|
current = obj
|
|
831
740
|
for part in str(dotted).split("."):
|
|
832
|
-
if not isinstance(current, dict):
|
|
833
|
-
return
|
|
834
|
-
current = current
|
|
741
|
+
if not isinstance(current, dict) or part not in current:
|
|
742
|
+
return default
|
|
743
|
+
current = current[part]
|
|
835
744
|
return current
|
|
836
745
|
|
|
837
746
|
|
|
747
|
+
def finish_value(result, dotted):
|
|
748
|
+
"""(number, None) for a usable finish.* value, else (None, reason)."""
|
|
749
|
+
resolved = resolve_dotted(result, dotted, _MISSING)
|
|
750
|
+
if resolved is _MISSING:
|
|
751
|
+
return None, "missing"
|
|
752
|
+
value = as_number(resolved)
|
|
753
|
+
if value is None:
|
|
754
|
+
return None, "not a number (" + type(resolved).__name__ + ")"
|
|
755
|
+
return value, None
|
|
756
|
+
|
|
757
|
+
|
|
838
758
|
def apply_finish(backend, config, plan, result, run_folder):
|
|
839
759
|
out = {}
|
|
760
|
+
skipped = []
|
|
840
761
|
finish = plan.get("finish") or {}
|
|
841
|
-
panel = finish.get("panel") or {}
|
|
842
762
|
writes = {}
|
|
843
|
-
for key, dotted in panel.items():
|
|
844
|
-
value =
|
|
845
|
-
if value is
|
|
846
|
-
|
|
763
|
+
for key, dotted in (finish.get("panel") or {}).items():
|
|
764
|
+
value, reason = finish_value(result, dotted)
|
|
765
|
+
if value is None:
|
|
766
|
+
skipped.append({"target": "panel", "key": str(key), "dotted": str(dotted), "reason": reason})
|
|
767
|
+
continue
|
|
768
|
+
writes[str(key)] = value
|
|
847
769
|
if writes:
|
|
848
770
|
out["panel"] = jsonable(backend.set_globals(writes, raw=False))
|
|
849
771
|
history = finish.get("history") or {}
|
|
@@ -854,8 +776,9 @@ def apply_finish(backend, config, plan, result, run_folder):
|
|
|
854
776
|
os.makedirs(os.path.dirname(log_path), exist_ok=True)
|
|
855
777
|
with open(log_path, "a", encoding="utf-8") as handle:
|
|
856
778
|
for key, dotted in history.items():
|
|
857
|
-
value =
|
|
779
|
+
value, reason = finish_value(result, dotted)
|
|
858
780
|
if value is None:
|
|
781
|
+
skipped.append({"target": "history", "key": str(key), "dotted": str(dotted), "reason": reason})
|
|
859
782
|
continue
|
|
860
783
|
row = {
|
|
861
784
|
"ts": now_iso(),
|
|
@@ -869,6 +792,8 @@ def apply_finish(backend, config, plan, result, run_folder):
|
|
|
869
792
|
rows.append(row)
|
|
870
793
|
if rows:
|
|
871
794
|
out["history"] = rows
|
|
795
|
+
if skipped:
|
|
796
|
+
out["skipped"] = skipped
|
|
872
797
|
return out
|
|
873
798
|
|
|
874
799
|
|
|
@@ -1119,6 +1044,14 @@ def cmd_engage(config, args):
|
|
|
1119
1044
|
return out
|
|
1120
1045
|
|
|
1121
1046
|
|
|
1047
|
+
# Same cause list for the stall branch and the timeout branch: BLACS did not run the shot.
|
|
1048
|
+
BLACS_NO_DATA_HINT = (
|
|
1049
|
+
"shot files exist but show no acquisition data: BLACS may be paused, may have kept the shot out of its queue "
|
|
1050
|
+
"(a shot's connection table must be a subset of the lab connection table) or re-queued it after a device "
|
|
1051
|
+
"programming timeout. Check the BLACS GUI, then labs_abort if needed."
|
|
1052
|
+
)
|
|
1053
|
+
|
|
1054
|
+
|
|
1122
1055
|
def wait_for_run(config, run_folder, expected, timeout_s):
|
|
1123
1056
|
if not run_folder:
|
|
1124
1057
|
raise BridgeError("BAD_ARGS", "run_folder is required")
|
|
@@ -1188,13 +1121,7 @@ def wait_for_run(config, run_folder, expected, timeout_s):
|
|
|
1188
1121
|
if compiled_since is None:
|
|
1189
1122
|
compiled_since = time.time()
|
|
1190
1123
|
elif time.time() - compiled_since >= stall:
|
|
1191
|
-
return result(
|
|
1192
|
-
False, False, "compiled_not_executed", time.time() - start,
|
|
1193
|
-
"shot files exist but show no acquisition data after " + str(int(stall)) + " s: BLACS may be "
|
|
1194
|
-
"paused, or it kept the shot out of the queue (its connection table must be a subset of the "
|
|
1195
|
-
"lab connection table) or a device group failed to program within 5 minutes (the queue then "
|
|
1196
|
-
"pauses and puts that shot back at its head), or the run is still being programmed. Check the "
|
|
1197
|
-
"BLACS GUI and the panel, then labs_abort if needed.")
|
|
1124
|
+
return result(False, False, "compiled_not_executed", time.time() - start, BLACS_NO_DATA_HINT)
|
|
1198
1125
|
else:
|
|
1199
1126
|
compiled_since = None
|
|
1200
1127
|
last_count = count
|
|
@@ -1221,9 +1148,7 @@ def wait_for_run(config, run_folder, expected, timeout_s):
|
|
|
1221
1148
|
hint = "only " + str(count) + " of " + str(expected) + " shots appeared before the timeout; the run may still be compiling or blocked."
|
|
1222
1149
|
elif executed is False:
|
|
1223
1150
|
diagnosis = "compiled_not_executed"
|
|
1224
|
-
hint =
|
|
1225
|
-
"shot (connection table not a subset of the lab connection table) or re-queued it after a device "
|
|
1226
|
-
"programming timeout. Check the BLACS GUI.")
|
|
1151
|
+
hint = BLACS_NO_DATA_HINT
|
|
1227
1152
|
else:
|
|
1228
1153
|
diagnosis = "partial"
|
|
1229
1154
|
hint = "shots appeared but the run was not confirmed complete before the timeout."
|
|
@@ -1241,8 +1166,6 @@ def cmd_abort(config, args):
|
|
|
1241
1166
|
if backend_name == "labscript":
|
|
1242
1167
|
note = ("labscript abort stops compiling/submitting new shots. Shots that are already compiled and queued "
|
|
1243
1168
|
"in BLACS, or currently running, are not stopped by it. This is not a hardware interlock.")
|
|
1244
|
-
elif backend_name == "http-sim":
|
|
1245
|
-
note = "http-sim abort stops the simulated queue (pause + clear)."
|
|
1246
1169
|
else:
|
|
1247
1170
|
note = "mock backend abort is a no-op."
|
|
1248
1171
|
result = backend.abort()
|
|
@@ -1527,8 +1450,8 @@ def cmd_protocol_finalize(config, args):
|
|
|
1527
1450
|
out = {"run_folder": run_folder}
|
|
1528
1451
|
result = None
|
|
1529
1452
|
if plan.get("postprocess"):
|
|
1530
|
-
|
|
1531
|
-
result = run_postprocess(config, plan, map_path(config, run_folder),
|
|
1453
|
+
scan_globals = [str(key) for key in plan["scan"]]
|
|
1454
|
+
result = run_postprocess(config, plan, map_path(config, run_folder), scan_globals, plan["reps"])
|
|
1532
1455
|
out["postprocess"] = result
|
|
1533
1456
|
if plan.get("finish"):
|
|
1534
1457
|
out["finish"] = apply_finish(backend, config, plan, result or {}, run_folder)
|
|
@@ -1560,17 +1483,25 @@ def cmd_doctor(config, args):
|
|
|
1560
1483
|
|
|
1561
1484
|
backend = (config.get("backend") or "mock-labscript").lower()
|
|
1562
1485
|
_add_check(checks, "backend", "ok", backend)
|
|
1486
|
+
_add_check(checks, "python-executable", "ok", sys.executable)
|
|
1563
1487
|
if backend == "labscript":
|
|
1488
|
+
# Presence only: importing labscript in this process has side effects (its
|
|
1489
|
+
# double-import denier walks the stack and leaves half-initialised modules
|
|
1490
|
+
# behind), which used to break the runmanager checks below and turn a working
|
|
1491
|
+
# environment into a false negative. Nothing here imports labscript itself.
|
|
1564
1492
|
missing = []
|
|
1565
1493
|
for module in ("labscript", "runmanager", "labscript_utils", "zprocess"):
|
|
1566
1494
|
try:
|
|
1567
|
-
importlib.
|
|
1495
|
+
present = importlib.util.find_spec(module) is not None
|
|
1568
1496
|
except Exception as exc:
|
|
1569
|
-
missing.append(module + ": " + str(exc))
|
|
1497
|
+
missing.append(module + ": " + type(exc).__name__ + ": " + str(exc))
|
|
1498
|
+
continue
|
|
1499
|
+
if not present:
|
|
1500
|
+
missing.append(module + ": not installed")
|
|
1570
1501
|
if missing:
|
|
1571
1502
|
_add_check(checks, "labscript-suite", "fail", "; ".join(missing), "use the labscript-suite python environment")
|
|
1572
1503
|
else:
|
|
1573
|
-
_add_check(checks, "labscript-suite", "ok", "labscript / runmanager / labscript_utils / zprocess
|
|
1504
|
+
_add_check(checks, "labscript-suite", "ok", "labscript / runmanager / labscript_utils / zprocess installed")
|
|
1574
1505
|
try:
|
|
1575
1506
|
probe = LabscriptBackend(config)
|
|
1576
1507
|
client = probe.client()
|
|
@@ -560,6 +560,178 @@ def test_doctor_mock():
|
|
|
560
560
|
shutil.rmtree(tmp, ignore_errors=True)
|
|
561
561
|
|
|
562
562
|
|
|
563
|
+
def test_doctor_checks_labscript_without_importing_it():
|
|
564
|
+
"""doctor must not import labscript: the failed import used to break the runmanager
|
|
565
|
+
checks that follow it in the same process, reporting a false negative."""
|
|
566
|
+
tmp = tempfile.mkdtemp(prefix="dsh-labscript-doctor-import-")
|
|
567
|
+
try:
|
|
568
|
+
cfg = make_config(tmp)
|
|
569
|
+
cfg["backend"] = "labscript"
|
|
570
|
+
|
|
571
|
+
class FakeClient(object):
|
|
572
|
+
def say_hello(self):
|
|
573
|
+
return True
|
|
574
|
+
|
|
575
|
+
def get_version(self):
|
|
576
|
+
return "fake-1.0"
|
|
577
|
+
|
|
578
|
+
class FakeBackend(object):
|
|
579
|
+
def __init__(self, config):
|
|
580
|
+
self.config = config
|
|
581
|
+
|
|
582
|
+
def client(self):
|
|
583
|
+
return FakeClient()
|
|
584
|
+
|
|
585
|
+
def endpoint(self):
|
|
586
|
+
return "127.0.0.1:42523 (config)"
|
|
587
|
+
|
|
588
|
+
def n_shots(self):
|
|
589
|
+
return 3
|
|
590
|
+
|
|
591
|
+
def run_shots(self):
|
|
592
|
+
return True
|
|
593
|
+
|
|
594
|
+
def error_in_globals(self):
|
|
595
|
+
return False
|
|
596
|
+
|
|
597
|
+
def shot_output_folder(self):
|
|
598
|
+
return "/tmp/runs"
|
|
599
|
+
|
|
600
|
+
original_find = bridge.importlib.util.find_spec
|
|
601
|
+
original_backend = bridge.LabscriptBackend
|
|
602
|
+
saved = sys.modules.pop("labscript", None)
|
|
603
|
+
bridge.importlib.util.find_spec = lambda name, package=None: object()
|
|
604
|
+
bridge.LabscriptBackend = FakeBackend
|
|
605
|
+
try:
|
|
606
|
+
out = bridge.cmd_doctor(cfg, {})
|
|
607
|
+
assert "labscript" not in sys.modules, "doctor imported labscript"
|
|
608
|
+
finally:
|
|
609
|
+
bridge.importlib.util.find_spec = original_find
|
|
610
|
+
bridge.LabscriptBackend = original_backend
|
|
611
|
+
if saved is not None:
|
|
612
|
+
sys.modules["labscript"] = saved
|
|
613
|
+
names = dict((check["name"], check["status"]) for check in out["checks"])
|
|
614
|
+
assert names["labscript-suite"] == "ok", names
|
|
615
|
+
assert names["runmanager"] == "ok", names
|
|
616
|
+
assert names["python-executable"] == "ok", names
|
|
617
|
+
finally:
|
|
618
|
+
shutil.rmtree(tmp, ignore_errors=True)
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
def test_doctor_reports_missing_modules_without_importing():
|
|
622
|
+
tmp = tempfile.mkdtemp(prefix="dsh-labscript-doctor-missing-")
|
|
623
|
+
try:
|
|
624
|
+
cfg = make_config(tmp)
|
|
625
|
+
cfg["backend"] = "labscript"
|
|
626
|
+
original_find = bridge.importlib.util.find_spec
|
|
627
|
+
bridge.importlib.util.find_spec = lambda name, package=None: None
|
|
628
|
+
try:
|
|
629
|
+
out = bridge.cmd_doctor(cfg, {})
|
|
630
|
+
finally:
|
|
631
|
+
bridge.importlib.util.find_spec = original_find
|
|
632
|
+
detail = dict((check["name"], check["detail"]) for check in out["checks"])
|
|
633
|
+
assert "not installed" in detail["labscript-suite"], detail
|
|
634
|
+
assert "No module named" not in detail["labscript-suite"], detail
|
|
635
|
+
finally:
|
|
636
|
+
shutil.rmtree(tmp, ignore_errors=True)
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
def test_finish_reports_values_instead_of_dropping_them():
|
|
640
|
+
tmp = tempfile.mkdtemp(prefix="dsh-labscript-finish-")
|
|
641
|
+
try:
|
|
642
|
+
cfg = make_config(tmp)
|
|
643
|
+
written = {}
|
|
644
|
+
|
|
645
|
+
class FakeBackend(object):
|
|
646
|
+
def set_globals(self, values, raw=False):
|
|
647
|
+
written.update(values)
|
|
648
|
+
return dict(values)
|
|
649
|
+
|
|
650
|
+
plan = {
|
|
651
|
+
"name": "demo",
|
|
652
|
+
"finish": {
|
|
653
|
+
"panel": {"duration_test_us": "fit.center", "amp": "fit.width", "flag": "fit.flag", "none": "fit.none"},
|
|
654
|
+
"history": {"demo.x": "fit.absent"},
|
|
655
|
+
},
|
|
656
|
+
}
|
|
657
|
+
result = {"fit": {"center": 2.5, "width": "abc", "flag": True, "none": None}}
|
|
658
|
+
out = bridge.apply_finish(FakeBackend(), cfg, plan, result, "/tmp/run")
|
|
659
|
+
assert written == {"duration_test_us": 2.5}, written
|
|
660
|
+
skipped = dict((item["key"], item) for item in out["skipped"])
|
|
661
|
+
assert skipped["amp"]["reason"] == "not a number (str)", skipped
|
|
662
|
+
assert skipped["flag"]["reason"] == "not a number (bool)", skipped
|
|
663
|
+
assert skipped["none"]["reason"] == "not a number (NoneType)", skipped
|
|
664
|
+
assert skipped["demo.x"]["target"] == "history", skipped
|
|
665
|
+
assert skipped["demo.x"]["reason"] == "missing", skipped
|
|
666
|
+
assert out["panel"] == {"duration_test_us": 2.5}, out
|
|
667
|
+
with open(cfg["calibrationLog"], "r", encoding="utf-8") as handle:
|
|
668
|
+
assert handle.read().strip() == "", "a skipped history value must not be logged"
|
|
669
|
+
finally:
|
|
670
|
+
shutil.rmtree(tmp, ignore_errors=True)
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
def test_postprocess_gets_every_scan_axis():
|
|
674
|
+
tmp = tempfile.mkdtemp(prefix="dsh-labscript-postprocess-axes-")
|
|
675
|
+
try:
|
|
676
|
+
cfg = make_config(tmp)
|
|
677
|
+
seen = {}
|
|
678
|
+
|
|
679
|
+
def analyze(run_folder, scan_global, reps, name, scan_globals=None):
|
|
680
|
+
seen.update({"run_folder": run_folder, "scan_global": scan_global, "reps": reps, "name": name, "scan_globals": scan_globals})
|
|
681
|
+
return {"fit": {"center": 1.0}}
|
|
682
|
+
|
|
683
|
+
module = types.ModuleType("fake_postprocess")
|
|
684
|
+
module.analyze = analyze
|
|
685
|
+
cfg["postprocess"] = {"entry": "fake_postprocess:analyze"}
|
|
686
|
+
original_import = bridge.importlib.import_module
|
|
687
|
+
|
|
688
|
+
def fake_import(name):
|
|
689
|
+
return module if name == "fake_postprocess" else original_import(name)
|
|
690
|
+
|
|
691
|
+
bridge.importlib.import_module = fake_import
|
|
692
|
+
try:
|
|
693
|
+
plan = {"name": "demo", "postprocess": "fake", "scan": {"duration_test_us": "x", "freq": "y"}}
|
|
694
|
+
result = bridge.run_postprocess(cfg, plan, "/tmp/run", ["duration_test_us", "freq"], 3)
|
|
695
|
+
finally:
|
|
696
|
+
bridge.importlib.import_module = original_import
|
|
697
|
+
assert result == {"fit": {"center": 1.0}}, result
|
|
698
|
+
assert seen["scan_globals"] == ["duration_test_us", "freq"], seen
|
|
699
|
+
assert seen["scan_global"] == "duration_test_us", seen
|
|
700
|
+
assert seen["reps"] == 3 and seen["name"] == "fake", seen
|
|
701
|
+
finally:
|
|
702
|
+
shutil.rmtree(tmp, ignore_errors=True)
|
|
703
|
+
|
|
704
|
+
|
|
705
|
+
def test_postprocess_keeps_supporting_a_positional_signature():
|
|
706
|
+
tmp = tempfile.mkdtemp(prefix="dsh-labscript-postprocess-legacy-")
|
|
707
|
+
try:
|
|
708
|
+
cfg = make_config(tmp)
|
|
709
|
+
seen = []
|
|
710
|
+
|
|
711
|
+
def analyze(a, b, c, d):
|
|
712
|
+
seen.extend([a, b, c, d])
|
|
713
|
+
return {"fit": {"center": 1.0}}
|
|
714
|
+
|
|
715
|
+
module = types.ModuleType("legacy_postprocess")
|
|
716
|
+
module.analyze = analyze
|
|
717
|
+
cfg["postprocess"] = {"entry": "legacy_postprocess:analyze"}
|
|
718
|
+
original_import = bridge.importlib.import_module
|
|
719
|
+
|
|
720
|
+
def fake_import(name):
|
|
721
|
+
return module if name == "legacy_postprocess" else original_import(name)
|
|
722
|
+
|
|
723
|
+
bridge.importlib.import_module = fake_import
|
|
724
|
+
try:
|
|
725
|
+
plan = {"name": "legacy", "postprocess": "legacy", "scan": {"a": "x", "b": "y"}}
|
|
726
|
+
result = bridge.run_postprocess(cfg, plan, "/tmp/run", ["a", "b"], 1)
|
|
727
|
+
finally:
|
|
728
|
+
bridge.importlib.import_module = original_import
|
|
729
|
+
assert result == {"fit": {"center": 1.0}}, result
|
|
730
|
+
assert seen == ["/tmp/run", "a", 1, "legacy"], seen
|
|
731
|
+
finally:
|
|
732
|
+
shutil.rmtree(tmp, ignore_errors=True)
|
|
733
|
+
|
|
734
|
+
|
|
563
735
|
def test_status_maps_paths_from_runmanager():
|
|
564
736
|
"""labs_status must return paths the agent's machine can open."""
|
|
565
737
|
tmp = tempfile.mkdtemp(prefix="dsh-labscript-py-map-")
|