dsh-labscript 0.2.8 → 0.3.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 +48 -0
- package/README.md +43 -5
- package/docs/README.zh.md +14 -0
- package/examples/config.example.yml +3 -1
- package/lib/client.js +7 -7
- package/lib/runtime.js +5 -2
- package/lib/skill.js +30 -15
- package/lib/store.mjs +17 -9
- package/lib/tools.js +15 -8
- package/package.json +1 -1
- package/python/bridge.py +91 -28
- package/python/tests/run_tests.py +115 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,53 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.1
|
|
4
|
+
|
|
5
|
+
- The DSH-side bridge budget (`requestTimeoutMs`) now defaults to 180 s instead of 60 s, and the
|
|
6
|
+
`labs_doctor` probe uses it too. It has to stay above runmanager's own client timeout, which
|
|
7
|
+
now comes from labconfig (`[timeouts] communication_timeout`): at 60 s both budgets were equal,
|
|
8
|
+
so a slow RPC would be killed by the host before runmanager could report the real error.
|
|
9
|
+
|
|
10
|
+
## 0.3.0
|
|
11
|
+
|
|
12
|
+
Implements the upstream request written from the field (labscript-suite workflow terms and
|
|
13
|
+
`runmanager.remote` limits). No output field is renamed or removed.
|
|
14
|
+
|
|
15
|
+
- `n_shots` is now documented and rendered as the authoritative shot count. The scan-axes line is
|
|
16
|
+
labelled a heuristic: runmanager expands list/array globals but passes tuples through, and an
|
|
17
|
+
expansion setting of `none` turns an array into a non-axis; neither is visible through
|
|
18
|
+
`runmanager.remote`. A mismatch now reads "n_shots is authoritative (a tuple global or an
|
|
19
|
+
expansion setting of none is not an axis)" instead of printing two numbers with no verdict. The
|
|
20
|
+
bridge's axis guess ignores tuples and dicts, and the mock backend follows the same rule (it used
|
|
21
|
+
to multiply tuples in, pinning the wrong semantics into the tests).
|
|
22
|
+
- Skill and tool descriptions no longer tell the agent to "read the runmanager output": compilation
|
|
23
|
+
errors appear only in runmanager's output tab (the compiler subprocess writes its traceback to
|
|
24
|
+
stderr and runmanager redirects it to an in-memory Qt widget), so the instruction is now to say
|
|
25
|
+
that no shot file appeared, ask the operator to paste the tab, and not guess the cause.
|
|
26
|
+
`labs_set` also warns that a global name must not shadow a Python builtin or a
|
|
27
|
+
`from labscript import *` name: runmanager accepts it and labscript fails at compile time.
|
|
28
|
+
- `compiled_not_executed` explains the other official causes: BLACS may keep a shot out of its
|
|
29
|
+
queue (the shot's connection table must be a subset of the lab connection table) or put it back
|
|
30
|
+
at the head after a device programming timeout; all of that is visible only in the BLACS GUI.
|
|
31
|
+
More shot files than expected now points at a changed panel or BLACS Repeat all / Repeat last,
|
|
32
|
+
which re-runs completed shots without creating new files.
|
|
33
|
+
- `labs_status` reports `shuffle` (from `get_shuffle`; "unknown" when that runmanager version has
|
|
34
|
+
no such method). With shuffle on, shot order is not axis order: read each shot's parameters from
|
|
35
|
+
its own h5 globals.
|
|
36
|
+
- runmanager host/port/timeout default to labscript's labconfig when unset, resolved by
|
|
37
|
+
`runmanager.remote.Client` itself (`[servers]/[ports]/[timeouts]`, port 42523, timeout 60 s);
|
|
38
|
+
configured values still win. The Plugins page now leaves host/port blank for that reason, and
|
|
39
|
+
clearing them hands the endpoint back to labconfig. `labs_doctor` prints the effective endpoint
|
|
40
|
+
and whether it came from config or labconfig.
|
|
41
|
+
- mock backend: `mock.errorInGlobals` makes `error_in_globals` true so the engagement refusal can
|
|
42
|
+
be tested, and `mock.shuffle` sets the reported shuffle flag.
|
|
43
|
+
- Docs: a terminology table maps the plugin's short names to the suite's (shot output folder,
|
|
44
|
+
sequence, shot file, parameter space/axes, acquired data) without renaming fields.
|
|
45
|
+
- Compilation logs stay out of scope: nothing about a compilation failure is written anywhere
|
|
46
|
+
readable (see README), so no `labs_compile_log` was added; a guessed diagnosis would be worse
|
|
47
|
+
than none.
|
|
48
|
+
- Tests: 33 python + 47 node (axis heuristic with tuples/dicts, mock error/shuffle switches,
|
|
49
|
+
labconfig client defaults, extra shot files, scan/shuffle rendering, endpoint clearing).
|
|
50
|
+
|
|
3
51
|
## 0.2.8
|
|
4
52
|
|
|
5
53
|
Fixes from a real labscript-suite session (2026-11-14, 145 tool calls, no run submitted by the
|
package/README.md
CHANGED
|
@@ -40,9 +40,9 @@ config). Run `labs_doctor` first if anything looks misconfigured.
|
|
|
40
40
|
| Tool | What it does |
|
|
41
41
|
|---|---|
|
|
42
42
|
| labs_doctor | Read-only configuration diagnostic: python, numpy/h5py, labscript/runmanager, paths, approval, versions. |
|
|
43
|
-
| labs_status | Read the panel: labscript, globals, n_shots, error_in_globals, run_shots, pending run progress, snapshot directory |
|
|
43
|
+
| labs_status | Read the panel: labscript, globals, n_shots, error_in_globals, run_shots, shuffle, pending run progress, snapshot directory |
|
|
44
44
|
| labs_set | Write panel globals (or raw expressions, or the repetition counter). Snapshots the panel first. Does not run. |
|
|
45
|
-
| labs_engage | Submit the current panel
|
|
45
|
+
| labs_engage | Submit the current panel as one sequence (shot files to BLACS); returns immediately with n_shots_at_submit and expected_n_shots |
|
|
46
46
|
| labs_wait | Wait (blocking, with timeout) for the pending run; finalizes a pending protocol: postprocess, finish write-back, restore |
|
|
47
47
|
| labs_abort | Abort the current submission |
|
|
48
48
|
| labs_data | Read the run folder HDF5: first-shot globals and, with scan, a per-shot table |
|
|
@@ -84,7 +84,7 @@ Add one row to your DSH profile patch (see examples/config.example.yml):
|
|
|
84
84
|
config:
|
|
85
85
|
python: /path/to/labscript-suite/python
|
|
86
86
|
backend: labscript
|
|
87
|
-
runmanager: { host: 127.0.0.1, port: 42523, timeoutS: 10 }
|
|
87
|
+
# runmanager: { host: 127.0.0.1, port: 42523, timeoutS: 10 } # optional; labconfig otherwise
|
|
88
88
|
protocolsDir: /path/to/protocols
|
|
89
89
|
scriptsFile: /path/to/scripts.json
|
|
90
90
|
hardLimitsFile: /path/to/hard_limits.json
|
|
@@ -103,6 +103,13 @@ already mapped: `labs_status`, `labs_engage`, `labs_wait`, `labs_data` and
|
|
|
103
103
|
`labs_protocol` show local paths, while `run_folder` keeps the raw runmanager
|
|
104
104
|
value next to `mapped_run_folder`.
|
|
105
105
|
|
|
106
|
+
`runmanager` may be omitted: host, port and timeout then come from labscript's labconfig
|
|
107
|
+
(`[servers] runmanager`, `[ports] runmanager` — default 42523 — and
|
|
108
|
+
`[timeouts] communication_timeout` — default 60 s), the same way `runmanager.remote.Client`
|
|
109
|
+
resolves them. Configured values override labconfig. The Plugins page leaves host/port blank
|
|
110
|
+
for that reason; typing them pins them. The DSH-side bridge budget (`requestTimeoutMs`,
|
|
111
|
+
default 180 s) stays above that timeout, so a slow RPC is not cut short by the host.
|
|
112
|
+
|
|
106
113
|
`engage` controls how a submitted run is watched: `timeoutS` (default 900) is the
|
|
107
114
|
total wait budget of one `labs_wait`, `noShotS` (default 25) makes the wait return
|
|
108
115
|
early with a diagnosis when no shot appeared, `staleS` (default 180) drops a pending
|
|
@@ -133,8 +140,8 @@ is not a physics simulation.
|
|
|
133
140
|
## Development
|
|
134
141
|
|
|
135
142
|
npm install # only runtime dependency: yaml
|
|
136
|
-
npm test # node --test,
|
|
137
|
-
python python/tests/run_tests.py #
|
|
143
|
+
npm test # node --test, 47 tests (fake bridge, no python needed)
|
|
144
|
+
python python/tests/run_tests.py # 33 tests (needs numpy + h5py)
|
|
138
145
|
LABSCRIPT_PYTHON=/path/to/python3 node scripts/demo.mjs # end-to-end mock demo
|
|
139
146
|
|
|
140
147
|
A fresh clone must run `npm install` before `npm test`; the CI workflow does this.
|
|
@@ -144,6 +151,20 @@ A fresh clone must run `npm install` before `npm test`; the CI workflow does thi
|
|
|
144
151
|
- docs/README.zh.md - Chinese guide
|
|
145
152
|
- docs/INSTALL.zh.md - local DSH profile install
|
|
146
153
|
|
|
154
|
+
## Terminology
|
|
155
|
+
|
|
156
|
+
The plugin uses short names; the labscript suite uses its own. Both describe the same objects,
|
|
157
|
+
and the field names are unchanged:
|
|
158
|
+
|
|
159
|
+
| plugin | labscript suite |
|
|
160
|
+
|---|---|
|
|
161
|
+
| run folder, `run_folder` | **shot output folder** (one **sequence**'s shot files) |
|
|
162
|
+
| run, engage | one **sequence**: one shot file per point in the parameter space |
|
|
163
|
+
| shot h5, `n_h5` | **shot file** (`.h5`) |
|
|
164
|
+
| scan, scan axis | **parameter space**, **axis**, **expansion** (`none`/`outer`/zip group/`auto`) |
|
|
165
|
+
| `n_executed` | shots that **acquired data** (`images/`, `results/`) |
|
|
166
|
+
| postprocess | lyse **single-shot / multi-shot routine** |
|
|
167
|
+
|
|
147
168
|
## Boundaries
|
|
148
169
|
|
|
149
170
|
- Real hardware: engage/protocol run ask for a DSH approval (one-shot) when the
|
|
@@ -154,6 +175,23 @@ A fresh clone must run `npm install` before `npm test`; the CI workflow does thi
|
|
|
154
175
|
count as 0/1. They are advisory configuration, not a safety interlock.
|
|
155
176
|
- labs_protocol show warns when a set/scan key is missing from the currently loaded
|
|
156
177
|
labscript; protocol runs ask for confirmation before proceeding on real hardware.
|
|
178
|
+
- The panel's `n_shots` is the authoritative shot count. The reported scan axes and
|
|
179
|
+
`n_shots_from_arrays` are a heuristic: runmanager expands list/array globals but passes
|
|
180
|
+
tuples through unwrapped, and an expansion setting of `none` turns an array back into a
|
|
181
|
+
non-axis; neither is visible through the remote API. When the two numbers disagree,
|
|
182
|
+
`n_shots` wins and the difference is reported as such.
|
|
183
|
+
- `labs_status` reports `shuffle`. With shuffle on, shot order is not axis order, so each
|
|
184
|
+
shot's parameters must be read from its own h5 globals.
|
|
185
|
+
- Compilation errors appear only in runmanager's output tab. The compiler subprocess writes the
|
|
186
|
+
traceback to its stderr, which runmanager redirects to that tab (an in-memory Qt widget);
|
|
187
|
+
`runmanager.remote` has no interface for it and nothing is written to a log file. The plugin
|
|
188
|
+
therefore reports that no shot file appeared and asks for the tab to be pasted, instead of
|
|
189
|
+
guessing why compilation failed.
|
|
190
|
+
- A shot file exists as soon as a sequence starts; `images/` and `results/` are written only
|
|
191
|
+
after execution, which is what `n_executed` counts. BLACS can keep a shot out of its queue
|
|
192
|
+
(its connection table must be a subset of the lab connection table) or put it back at the head
|
|
193
|
+
after a device programming timeout, and BLACS Repeat all / Repeat last re-runs completed shots
|
|
194
|
+
without creating new files; none of that is visible to the plugin.
|
|
157
195
|
- Completion is detected from the run folder on the filesystem (HDF5 count,
|
|
158
196
|
stable file sizes, and a readable last shot); stock labscript has no
|
|
159
197
|
run-state query.
|
package/docs/README.zh.md
CHANGED
|
@@ -27,6 +27,9 @@ hard limits 全部通过配置接入。
|
|
|
27
27
|
- 要求:DSH >= 0.2、Node ^22.19/24;Python >= 3.9。mock 模式只要 numpy+h5py;
|
|
28
28
|
真机用 labscript-suite 环境。详见 docs/INSTALL.zh.md。
|
|
29
29
|
- 默认 backend 是 mock-labscript;真机必须显式配置 backend: labscript。
|
|
30
|
+
- runmanager 的 host/port/timeout 可以不填:不填就读 labscript 的 labconfig
|
|
31
|
+
(`[servers]/[ports] runmanager`,端口默认 42523;`[timeouts] communication_timeout`,
|
|
32
|
+
默认 60 s),和 runmanager 自己的客户端一致;填了就以配置为准。插件页留空即走 labconfig。
|
|
30
33
|
- 安装后在 DSH 插件页填 backend、python、runmanager host/port、protocolsDir;
|
|
31
34
|
其余高级配置(scriptsFile、hard limits、postprocess、approval、pathMap、
|
|
32
35
|
engage 的超时/早退参数)写在 profile 的 cordis.patch.yml。
|
|
@@ -62,4 +65,15 @@ postprocess 是后处理:跑完后调用配置的 Python 函数(entry + pyth
|
|
|
62
65
|
丢弃并报 dropped_stale_pending,不再永久挡住提交;RUN_PENDING 的错误里也会写清年龄和两种出路。
|
|
63
66
|
- 配了 pathMap 时 pending 记录映射后的目录(原始路径保留为 remote_run_folder),所以
|
|
64
67
|
labs_wait 传映射后目录或原始目录都能对上,不会再说"run folder does not exist"。
|
|
68
|
+
- **shot 数以面板 `n_shots` 为准**。工具报的 scan axes / n_shots_from_arrays 只是猜测:
|
|
69
|
+
runmanager 会把 list/array 展开成轴,但 **tuple 不展开**,而且 expansion 被设成 `none` 时
|
|
70
|
+
数组就不再是轴——这两件事从 remote 接口都看不到。两个数字不一致时按 `n_shots` 报,并说明差别。
|
|
71
|
+
- `labs_status` 会显示 `shuffle`。shuffle 打开时 shot 顺序不等于轴顺序,每发参数要从该发 h5 的
|
|
72
|
+
globals 读,不要按序号推。
|
|
73
|
+
- 编译报错只出现在 runmanager 的 output tab:编译器子进程把回溯写到自己的 stderr,runmanager
|
|
74
|
+
把它转到这个 Qt 界面上(内存里,不落盘),`runmanager.remote` 没有任何读取接口。所以插件只能报
|
|
75
|
+
"n 秒内没有出现 shot 文件",并请操作员贴 output tab,不猜原因。
|
|
76
|
+
- shot 文件在 sequence 开始时就写出来了;`images/`、`results/` 是执行后才写,`n_executed`
|
|
77
|
+
数的就是这个。BLACS 可能因为连接表不是实验室连接表的子集而拒收、或设备编程超时把 shot 放回队首,
|
|
78
|
+
BLACS 的 Repeat all / Repeat last 会把跑完的 shot 重跑而不新增文件——这些插件都看不到。
|
|
65
79
|
|
|
@@ -19,7 +19,9 @@
|
|
|
19
19
|
# config:
|
|
20
20
|
# python: /path/to/labscript-suite/python
|
|
21
21
|
# backend: labscript
|
|
22
|
-
# runmanager
|
|
22
|
+
# # runmanager 可以不写:host/port/timeout 默认读 labscript 的 labconfig
|
|
23
|
+
# # ([servers]/[ports]/[timeouts],端口默认 42523、超时默认 60 s),填了才覆盖。
|
|
24
|
+
# # runmanager: { host: 127.0.0.1, port: 42523, timeoutS: 10 }
|
|
23
25
|
# protocolsDir: /path/to/your/protocols
|
|
24
26
|
# scriptsFile: /path/to/scripts.json
|
|
25
27
|
# hardLimitsFile: /path/to/hard_limits.json
|
package/lib/client.js
CHANGED
|
@@ -78,8 +78,8 @@ window.__ModuleLoader__.load({
|
|
|
78
78
|
const [version, setVersion] = useState('')
|
|
79
79
|
const [backend, setBackend] = useState('mock-labscript')
|
|
80
80
|
const [python, setPython] = useState('')
|
|
81
|
-
const [host, setHost] = useState('
|
|
82
|
-
const [port, setPort] = useState(
|
|
81
|
+
const [host, setHost] = useState('')
|
|
82
|
+
const [port, setPort] = useState('')
|
|
83
83
|
const [protocolsDir, setProtocolsDir] = useState('')
|
|
84
84
|
const [busy, setBusy] = useState('')
|
|
85
85
|
const [message, setMessage] = useState(null)
|
|
@@ -94,8 +94,8 @@ window.__ModuleLoader__.load({
|
|
|
94
94
|
const current = payload.config || {}
|
|
95
95
|
setBackend(current.backend || 'mock-labscript')
|
|
96
96
|
setPython(current.python || '')
|
|
97
|
-
setHost((current.runmanager && current.runmanager.host) || '
|
|
98
|
-
setPort(
|
|
97
|
+
setHost((current.runmanager && current.runmanager.host) || '')
|
|
98
|
+
setPort(current.runmanager && current.runmanager.port ? String(current.runmanager.port) : '')
|
|
99
99
|
setProtocolsDir(current.protocolsDir || '')
|
|
100
100
|
} catch (error) {
|
|
101
101
|
setMessage({ kind: 'err', text: error.message })
|
|
@@ -118,7 +118,7 @@ window.__ModuleLoader__.load({
|
|
|
118
118
|
body: JSON.stringify({
|
|
119
119
|
backend,
|
|
120
120
|
python,
|
|
121
|
-
runmanager: { host, port: Number(port) },
|
|
121
|
+
runmanager: { host: host.trim(), port: port === '' ? '' : Number(port) },
|
|
122
122
|
protocolsDir,
|
|
123
123
|
}),
|
|
124
124
|
})
|
|
@@ -160,8 +160,8 @@ window.__ModuleLoader__.load({
|
|
|
160
160
|
),
|
|
161
161
|
),
|
|
162
162
|
h(Row, { label: 'python', hint: '真机填 labscript-suite 环境的 python;模拟器只需 numpy+h5py。' }, h('input', { type: 'text', value: python, onChange: (event) => setPython(event.target.value) })),
|
|
163
|
-
h(Row, { label: 'runmanager host' }, h('input', { type: 'text', value: host, onChange: (event) => setHost(event.target.value) })),
|
|
164
|
-
h(Row, { label: 'runmanager port' }, h('input', { type: 'number', value: port, onChange: (event) => setPort(event.target.value) })),
|
|
163
|
+
h(Row, { label: 'runmanager host', hint: '留空则用 labscript 的 labconfig([servers] runmanager)。' }, h('input', { type: 'text', value: host, placeholder: 'labconfig 里的地址', onChange: (event) => setHost(event.target.value) })),
|
|
164
|
+
h(Row, { label: 'runmanager port', hint: '留空则用 labconfig([ports] runmanager,默认 42523)。' }, h('input', { type: 'number', value: port, placeholder: 'labconfig', onChange: (event) => setPort(event.target.value) })),
|
|
165
165
|
),
|
|
166
166
|
h('section', { className: 'lsc-section' },
|
|
167
167
|
h('h3', null, '实验'),
|
package/lib/runtime.js
CHANGED
|
@@ -69,7 +69,10 @@ function normalizeConfig(raw) {
|
|
|
69
69
|
mock: input.mock || {},
|
|
70
70
|
simulator: input.simulator || {},
|
|
71
71
|
dshHome: input.dshHome || '',
|
|
72
|
-
|
|
72
|
+
// Must stay above runmanager's own client timeout (labconfig [timeouts]
|
|
73
|
+
// communication_timeout, default 60 s): the bridge is killed when this expires, which
|
|
74
|
+
// would hide the real error behind a generic timeout.
|
|
75
|
+
requestTimeoutMs: Number(input.requestTimeoutMs || 180000),
|
|
73
76
|
}
|
|
74
77
|
}
|
|
75
78
|
|
|
@@ -303,7 +306,7 @@ export function createRuntime(ctx, rawConfig) {
|
|
|
303
306
|
}
|
|
304
307
|
if (config.python) {
|
|
305
308
|
try {
|
|
306
|
-
const data = await runBridge('doctor', {}, { timeoutMs:
|
|
309
|
+
const data = await runBridge('doctor', {}, { timeoutMs: config.requestTimeoutMs })
|
|
307
310
|
for (const check of data.checks || []) checks.push(check)
|
|
308
311
|
} catch (error) {
|
|
309
312
|
add('python-bridge', 'fail', error.message, error.detail && error.detail.hint ? error.detail.hint : 'run the bridge manually to see the traceback')
|
package/lib/skill.js
CHANGED
|
@@ -10,8 +10,11 @@ export const SKILL_CONTENT = [
|
|
|
10
10
|
'not your memory, is the truth. Never guess a global value or a scan axis.',
|
|
11
11
|
'',
|
|
12
12
|
'## Before any write',
|
|
13
|
-
'1. Call labs_status. Confirm the loaded labscript, n_shots, error_in_globals, run_shots',
|
|
14
|
-
' and the keys you are about to touch.',
|
|
13
|
+
'1. Call labs_status. Confirm the loaded labscript, n_shots, error_in_globals, run_shots,',
|
|
14
|
+
' shuffle and the keys you are about to touch. n_shots is the authoritative shot count:',
|
|
15
|
+
' the scan axes line is only a heuristic (runmanager does not expand tuples, and an',
|
|
16
|
+
' expansion setting of none makes an array a non-axis), so report a difference instead',
|
|
17
|
+
' of recomputing the count yourself.',
|
|
15
18
|
'2. Real hardware: the user must approve this turn before panel writes, engage or abort.',
|
|
16
19
|
' The plugin asks for its own one-shot approval on engage/protocol run in the real',
|
|
17
20
|
' backend; that approval does not replace the user conversation requirement.',
|
|
@@ -21,13 +24,19 @@ export const SKILL_CONTENT = [
|
|
|
21
24
|
'## Running',
|
|
22
25
|
'- labs_set writes the panel. It does not run anything. Always pass note: one sentence',
|
|
23
26
|
' saying what this batch is for (it is written to the event log).',
|
|
24
|
-
'-
|
|
25
|
-
'
|
|
27
|
+
'- One engage submits one sequence: runmanager writes one shot file per point into the',
|
|
28
|
+
' shot output folder (called run folder in these tools) and hands each file to BLACS.',
|
|
26
29
|
'- labs_engage submits a run and returns immediately with n_shots_at_submit and',
|
|
27
30
|
' expected_n_shots. Do not poll in a loop; when the user wants results, call labs_wait.',
|
|
28
31
|
' After submitting you may end the turn or do independent work.',
|
|
29
|
-
'-
|
|
32
|
+
'- labs_engage cannot see compilation errors: runmanager shows them only in its output',
|
|
33
|
+
' tab, which runmanager.remote cannot read. If nothing runs, see Stop rules.',
|
|
34
|
+
'- n_h5 counts shot files, which labscript writes before compiling each shot; only',
|
|
30
35
|
' n_executed (or the diagnosis) says whether shots actually acquired data.',
|
|
36
|
+
'- shuffle true means shot order is not axis order: read each shot parameters from its',
|
|
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.',
|
|
31
40
|
'- labs_protocol run submits a fixed protocol; labs_wait finalizes it (postprocess,',
|
|
32
41
|
' finish write-back, panel restore).',
|
|
33
42
|
'- backend defaults to mock-labscript; real hardware is only used when the config says',
|
|
@@ -40,22 +49,28 @@ export const SKILL_CONTENT = [
|
|
|
40
49
|
' write touched (pass full=true for the whole panel), so concurrent edits elsewhere are kept.',
|
|
41
50
|
'',
|
|
42
51
|
'## Reading results',
|
|
43
|
-
'- labs_data reads the
|
|
44
|
-
' per-shot table. HDF5 is authoritative; the panel is only intermediate state.',
|
|
52
|
+
'- labs_data reads the shot files HDF5: globals of the first shot and, with scan: a',
|
|
53
|
+
' per-shot table. HDF5 is authoritative; the panel is only intermediate state. images/',
|
|
54
|
+
' and results/ are written only after execution.',
|
|
45
55
|
'- Report facts: run folder, expected vs actual shots, postprocess numbers, warnings.',
|
|
46
56
|
' If a number is missing, say so instead of inventing one.',
|
|
47
57
|
'',
|
|
48
58
|
'## Stop rules',
|
|
49
59
|
'- error_in_globals true: fix or ask; never engage.',
|
|
50
60
|
'- labs_wait returns before the timeout with diagnosis no_shots or compile_stalled when no',
|
|
51
|
-
' shot appeared for config.engage.noShotS seconds (default 25).
|
|
52
|
-
'
|
|
53
|
-
'
|
|
54
|
-
'
|
|
55
|
-
'
|
|
56
|
-
'
|
|
57
|
-
'
|
|
58
|
-
'
|
|
61
|
+
' shot file appeared for config.engage.noShotS seconds (default 25). Compilation errors are',
|
|
62
|
+
' visible only in runmanager output tab, which this tool cannot read: say exactly that, ask',
|
|
63
|
+
' the operator to paste the tab, and do not guess the cause or resubmit blindly. Call',
|
|
64
|
+
' labs_wait again only if you expect a slow compile. A submission that never produced a shot',
|
|
65
|
+
' is dropped by the next labs_engage once it is config.engage.staleS seconds old (default',
|
|
66
|
+
' 180) and reported as dropped_stale_pending.',
|
|
67
|
+
'- diagnosis=compiled_not_executed, or timed_out with shot files but no acquisition data:',
|
|
68
|
+
' BLACS did not run that shot. It may be paused, may have refused the shot because its',
|
|
69
|
+
' connection table is not a subset of the lab connection table, or a device group may have',
|
|
70
|
+
' failed to program within 5 minutes (BLACS then pauses the queue and puts the shot back at',
|
|
71
|
+
' its head). None of this is visible to this tool: look at the BLACS GUI, then abort or ask.',
|
|
72
|
+
'- More shots finished than expected: check the panel n_shots and expansion settings first,',
|
|
73
|
+
' then BLACS Repeat all / Repeat last, which re-run existing shot files.',
|
|
59
74
|
'- A run is already pending: wait or abort before submitting another.',
|
|
60
75
|
].join(String.fromCharCode(10))
|
|
61
76
|
|
package/lib/store.mjs
CHANGED
|
@@ -34,8 +34,13 @@ export function normalizeUiConfig(input) {
|
|
|
34
34
|
throw new Error('backend must be one of ' + BACKENDS.join(', ') + ', got: ' + backend)
|
|
35
35
|
}
|
|
36
36
|
const runmanager = source.runmanager && typeof source.runmanager === 'object' ? source.runmanager : {}
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
// host/port stay optional: when they are not stored, the bridge lets runmanager.remote
|
|
38
|
+
// resolve them from labscript's labconfig ([servers]/[ports] runmanager), the same way
|
|
39
|
+
// the runmanager GUI does. Storing the old hardcoded defaults here would pin them.
|
|
40
|
+
const host = runmanager.host === undefined || runmanager.host === null ? '' : String(runmanager.host).trim()
|
|
41
|
+
const portText = runmanager.port === undefined || runmanager.port === null ? '' : String(runmanager.port).trim()
|
|
42
|
+
const port = portText === '' ? undefined : Number(portText)
|
|
43
|
+
if (port !== undefined && (!Number.isInteger(port) || port < 1 || port > 65535)) throw new Error('runmanager.port must be 1-65535')
|
|
39
44
|
const timeoutS = runmanager.timeoutS === undefined || runmanager.timeoutS === null || runmanager.timeoutS === ''
|
|
40
45
|
? undefined
|
|
41
46
|
: Number(runmanager.timeoutS)
|
|
@@ -45,12 +50,11 @@ export function normalizeUiConfig(input) {
|
|
|
45
50
|
const out = {
|
|
46
51
|
backend,
|
|
47
52
|
python: String(source.python || '').trim(),
|
|
48
|
-
runmanager: {
|
|
49
|
-
host: String(runmanager.host || '127.0.0.1').trim(),
|
|
50
|
-
port,
|
|
51
|
-
},
|
|
53
|
+
runmanager: {},
|
|
52
54
|
protocolsDir: String(source.protocolsDir || '').trim(),
|
|
53
55
|
}
|
|
56
|
+
if (host !== '') out.runmanager.host = host
|
|
57
|
+
if (port !== undefined) out.runmanager.port = port
|
|
54
58
|
if (timeoutS !== undefined) out.runmanager.timeoutS = timeoutS
|
|
55
59
|
return out
|
|
56
60
|
}
|
|
@@ -75,9 +79,13 @@ export function readUiConfig(home) {
|
|
|
75
79
|
export function writeUiConfig(input, home) {
|
|
76
80
|
const previous = readUiConfig(home).config || {}
|
|
77
81
|
const patch = input && typeof input === 'object' ? input : {}
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
82
|
+
const runmanager = Object.assign({}, previous.runmanager || {}, patch.runmanager || {})
|
|
83
|
+
// An empty host/port means "unset, ask labconfig": drop the stored key instead of
|
|
84
|
+
// keeping the previous value, so a lab that moves runmanager can clear it.
|
|
85
|
+
for (const key of Object.keys(runmanager)) {
|
|
86
|
+
if (runmanager[key] === '' || runmanager[key] === null || runmanager[key] === undefined) delete runmanager[key]
|
|
87
|
+
}
|
|
88
|
+
const config = normalizeUiConfig(Object.assign({}, previous, patch, { runmanager }))
|
|
81
89
|
const file = uiConfigPath(home)
|
|
82
90
|
fs.mkdirSync(path.dirname(file), { recursive: true })
|
|
83
91
|
const temp = file + '.' + process.pid + '.' + Date.now() + '.tmp'
|
package/lib/tools.js
CHANGED
|
@@ -67,14 +67,21 @@ function globalLines(args, value) {
|
|
|
67
67
|
return out
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
// runmanager decides the shot count from the expansion settings, which runmanager.remote
|
|
71
|
+
// cannot read: a tuple is not an axis and an array can be switched to expansion "none".
|
|
72
|
+
// The panel's n_shots is therefore the only authoritative number; this line is a hint.
|
|
70
73
|
function scanLine(value) {
|
|
71
74
|
const axes = value.scan_axes && typeof value.scan_axes === 'object' ? value.scan_axes : {}
|
|
72
75
|
const names = Object.keys(axes).sort()
|
|
73
76
|
const product = value.n_shots_from_arrays
|
|
74
|
-
const
|
|
77
|
+
const known = typeof value.n_shots === 'number'
|
|
78
|
+
const mismatch = typeof product === 'number' && known && value.n_shots !== product
|
|
75
79
|
if (names.length === 0 && !mismatch) return ''
|
|
76
80
|
const detail = names.length ? names.map((key) => key + '=' + axes[key]).join(', ') : 'none'
|
|
77
|
-
|
|
81
|
+
const verdict = mismatch
|
|
82
|
+
? ' product ' + product + ' != n_shots ' + value.n_shots + ': n_shots is authoritative (a tuple global or an expansion setting of none is not an axis)'
|
|
83
|
+
: ' product ' + product + ' n_shots ' + (known ? value.n_shots : '?') + ' (authoritative)'
|
|
84
|
+
return 'scan axes (heuristic): ' + detail + verdict
|
|
78
85
|
}
|
|
79
86
|
|
|
80
87
|
function resultsLine(value) {
|
|
@@ -248,7 +255,7 @@ export function registerTools(ctx, runtime) {
|
|
|
248
255
|
|
|
249
256
|
ctx.tools.register({
|
|
250
257
|
name: 'labs_status',
|
|
251
|
-
description: 'Read the labscript runmanager panel: loaded labscript, globals (evaluated; raw expressions with raw=true), n_shots, error_in_globals, run_shots, shot output folder, pending run progress, and the panel snapshot directory. Read-only; call this before any write. Pass keys=[...] (leaf global names; UI group names are not globals) to print the selected panel values (240 chars each; raw=true prints the raw expression), missing keys are shown as <missing
|
|
258
|
+
description: 'Read the labscript runmanager panel: loaded labscript, globals (evaluated; raw expressions with raw=true), n_shots, error_in_globals, run_shots, shuffle, shot output folder (called run folder in this tool: where the NEXT engage writes its shot files; it does not exist until engage), pending run progress, and the panel snapshot directory. Read-only; call this before any write. Pass keys=[...] (leaf global names; UI group names are not globals) to print the selected panel values (240 chars each; raw=true prints the raw expression), missing keys are shown as <missing> (the global is in an inactive group or misspelled); keys=["*"] prints every global. scan axes is a heuristic: it counts array-valued globals and their lengths multiply to n_shots_from_arrays, but runmanager does not expand tuples and an expansion setting of none turns an array into a non-axis, neither of which is visible from the remote API. n_shots is authoritative: when the two differ, report n_shots and say that at least one array-valued global is not an axis. shuffle true means shot order does not follow the axis order. Paths in the output are already mapped for this machine through pathMap; do not map them again.',
|
|
252
259
|
parameters: {
|
|
253
260
|
type: 'object',
|
|
254
261
|
properties: {
|
|
@@ -262,7 +269,7 @@ export function registerTools(ctx, runtime) {
|
|
|
262
269
|
'backend: ' + value.backend + ' plugin: v' + runtime.version,
|
|
263
270
|
'labscript: ' + value.labscript,
|
|
264
271
|
'shot output: ' + value.shot_output_folder,
|
|
265
|
-
'n_shots: ' + value.n_shots + ' error_in_globals: ' + value.error_in_globals + ' run_shots: ' + value.run_shots,
|
|
272
|
+
'n_shots: ' + value.n_shots + ' error_in_globals: ' + value.error_in_globals + ' run_shots: ' + value.run_shots + ' shuffle: ' + (value.shuffle === undefined || value.shuffle === null ? 'unknown' : value.shuffle),
|
|
266
273
|
progressLine(value.last_run),
|
|
267
274
|
'panel history: ' + (value.panel_history ? value.panel_history.dir + ' (' + value.panel_history.count + ' snapshots)' : 'n/a'),
|
|
268
275
|
scanLine(value),
|
|
@@ -282,7 +289,7 @@ export function registerTools(ctx, runtime) {
|
|
|
282
289
|
|
|
283
290
|
ctx.tools.register({
|
|
284
291
|
name: 'labs_set',
|
|
285
|
-
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 normally (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 full panel snapshot is taken before the write. Does not run anything.',
|
|
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 normally (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 list global becomes a scan axis only if runmanager actually expands it (a tuple is passed through whole, and an expansion setting of none drops the axis). A global name must not shadow a Python builtin or a name from "from labscript import *": runmanager accepts the name, labscript fails at compile time, and that failure is visible only in runmanager output tab. A full panel snapshot is taken before the write. Does not run anything.',
|
|
286
293
|
parameters: {
|
|
287
294
|
type: 'object',
|
|
288
295
|
properties: {
|
|
@@ -312,7 +319,7 @@ export function registerTools(ctx, runtime) {
|
|
|
312
319
|
|
|
313
320
|
ctx.tools.register({
|
|
314
321
|
name: 'labs_engage',
|
|
315
|
-
description: 'Submit the current panel
|
|
322
|
+
description: 'Submit the current panel as one sequence: runmanager writes one shot file per point and hands it to BLACS (this is the "shot output folder", called run_folder in this tool). Returns immediately with run_folder, n_shots_at_submit and expected_n_shots; use labs_wait later to wait for completion. It cannot see compilation errors: runmanager shows those only in its output tab. Refuses when error_in_globals is true or another run is still pending; a pending run that produced no shot at all for config.engage.staleS (default 180 s) is dropped and reported as dropped_stale_pending.',
|
|
316
323
|
parameters: {
|
|
317
324
|
type: 'object',
|
|
318
325
|
properties: {
|
|
@@ -356,7 +363,7 @@ export function registerTools(ctx, runtime) {
|
|
|
356
363
|
|
|
357
364
|
ctx.tools.register({
|
|
358
365
|
name: 'labs_wait',
|
|
359
|
-
description: 'Wait for the pending run to finish (blocking, timeout in seconds). If
|
|
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 of panel and calibration history, and panel restore. Returns expected vs actual shot files and, for protocols, the postprocess/finish/restore results. 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 exactly that and ask the operator to paste the tab; call labs_wait again only if you expect a slow compile. diagnosis compiled_not_executed means shot files exist but BLACS acquired no data: BLACS may be paused, may have refused the shot because its connection table is not a subset of the lab connection table, or a device group failed to program within 5 minutes (the queue then pauses and puts the shot back at its head) - all visible only in the BLACS GUI. More shot files than expected means the panel n_shots changed after submit, or BLACS Repeat all / Repeat last re-ran shots. A run folder that does not exist and is not the pending run of this session fails immediately instead of waiting.',
|
|
360
367
|
parameters: {
|
|
361
368
|
type: 'object',
|
|
362
369
|
properties: {
|
|
@@ -483,7 +490,7 @@ export function registerTools(ctx, runtime) {
|
|
|
483
490
|
|
|
484
491
|
ctx.tools.register({
|
|
485
492
|
name: 'labs_data',
|
|
486
|
-
description: 'Read a run folder HDF5: first-shot globals (pass keys=[...] to print selected values, ["*"] for all), the results groups with results: true, and with scan: a per-shot
|
|
493
|
+
description: 'Read a run folder (shot output folder) HDF5: first-shot globals (pass keys=[...] to print selected values, ["*"] for all), the results groups with results: true, and with scan: a per-shot axis summary (unique points, min/max, median step) plus the numeric result columns; pass limit to print up to 20 raw rows. Each shot file holds the evaluated globals of that shot, its hardware commands and, after execution, images/ and results/ - a shot with no images/results was not executed. With shuffle on, read each shot\'s parameters from its own file instead of assuming axis order. Read-only. run_folder defaults to the pending run, else the last run submitted through this plugin (persisted across restarts); for a historical run pass its folder explicitly. A missing folder is an error: the shot output folder is the NEXT sequence\'s folder and only exists after engage.',
|
|
487
494
|
parameters: {
|
|
488
495
|
type: 'object',
|
|
489
496
|
properties: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-labscript",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
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
|
@@ -211,6 +211,28 @@ def eval_panel_value(raw):
|
|
|
211
211
|
return str(raw)
|
|
212
212
|
|
|
213
213
|
|
|
214
|
+
def axis_length(value):
|
|
215
|
+
"""Length of the axis runmanager expands from this global, or None.
|
|
216
|
+
|
|
217
|
+
Heuristic: runmanager's automatic expansion turns list/array globals into axes
|
|
218
|
+
("outer") and passes tuples through whole, so a tuple is not an axis. Expansion
|
|
219
|
+
settings (none / zip group) are not visible from runmanager.remote, so a list that
|
|
220
|
+
was switched to "none" still looks like an axis here. The panel's n_shots is the
|
|
221
|
+
authoritative shot count; this is only a hint.
|
|
222
|
+
"""
|
|
223
|
+
if isinstance(value, (str, bytes, dict, tuple)):
|
|
224
|
+
return None
|
|
225
|
+
if not hasattr(value, "__len__"):
|
|
226
|
+
return None
|
|
227
|
+
ndim = getattr(value, "ndim", None)
|
|
228
|
+
if ndim is not None:
|
|
229
|
+
return int(value.size) if ndim >= 1 else None
|
|
230
|
+
try:
|
|
231
|
+
return int(len(value))
|
|
232
|
+
except Exception:
|
|
233
|
+
return None
|
|
234
|
+
|
|
235
|
+
|
|
214
236
|
def raw_repr(value):
|
|
215
237
|
if isinstance(value, bool):
|
|
216
238
|
return "True" if value else "False"
|
|
@@ -297,18 +319,39 @@ class LabscriptBackend(object):
|
|
|
297
319
|
from runmanager.remote import Client
|
|
298
320
|
except Exception as exc:
|
|
299
321
|
raise BridgeError("BACKEND_UNAVAILABLE", "labscript python environment not importable: " + str(exc))
|
|
322
|
+
# Anything left unset is resolved by runmanager.remote.Client from labscript's
|
|
323
|
+
# labconfig ([servers] runmanager, [ports] runmanager, [timeouts]
|
|
324
|
+
# communication_timeout), the same way the runmanager GUI does it. Explicit
|
|
325
|
+
# config always wins.
|
|
300
326
|
rm = self.config.get("runmanager") or {}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
327
|
+
kwargs = {}
|
|
328
|
+
if rm.get("host"):
|
|
329
|
+
kwargs["host"] = rm["host"]
|
|
330
|
+
if rm.get("port"):
|
|
331
|
+
kwargs["port"] = int(rm["port"])
|
|
332
|
+
if rm.get("timeoutS"):
|
|
333
|
+
kwargs["timeout"] = float(rm["timeoutS"])
|
|
334
|
+
try:
|
|
335
|
+
client = Client(**kwargs)
|
|
336
|
+
except Exception as exc:
|
|
337
|
+
raise BridgeError("BACKEND_UNAVAILABLE", "cannot create a runmanager client: " + type(exc).__name__ + ": " + str(exc))
|
|
304
338
|
try:
|
|
305
|
-
client = Client(host=host, port=port, timeout=timeout)
|
|
306
339
|
client.say_hello()
|
|
307
340
|
except Exception as exc:
|
|
308
|
-
raise BridgeError("BACKEND_UNAVAILABLE", "runmanager not reachable at " + host + ":" + str(port) + ": " + str(exc))
|
|
341
|
+
raise BridgeError("BACKEND_UNAVAILABLE", "runmanager not reachable at " + str(getattr(client, "host", "?")) + ":" + str(getattr(client, "port", "?")) + ": " + str(exc))
|
|
309
342
|
self._client = client
|
|
310
343
|
return client
|
|
311
344
|
|
|
345
|
+
def endpoint(self):
|
|
346
|
+
"""Where this client points, and whether the values came from config or labconfig."""
|
|
347
|
+
rm = self.config.get("runmanager") or {}
|
|
348
|
+
source = "config" if (rm.get("host") or rm.get("port")) else "labconfig"
|
|
349
|
+
try:
|
|
350
|
+
client = self.client()
|
|
351
|
+
return str(client.host) + ":" + str(client.port) + " (" + source + ", timeout " + str(client.timeout) + "s)"
|
|
352
|
+
except Exception as exc:
|
|
353
|
+
return "<unknown> (" + source + "): " + str(exc)
|
|
354
|
+
|
|
312
355
|
def version(self):
|
|
313
356
|
try:
|
|
314
357
|
return str(self.client().get_version())
|
|
@@ -337,6 +380,13 @@ class LabscriptBackend(object):
|
|
|
337
380
|
except Exception:
|
|
338
381
|
return None
|
|
339
382
|
|
|
383
|
+
def shuffle(self):
|
|
384
|
+
# Older runmanager versions have no get_shuffle; report unknown instead of failing.
|
|
385
|
+
try:
|
|
386
|
+
return bool(self.client().get_shuffle())
|
|
387
|
+
except Exception:
|
|
388
|
+
return None
|
|
389
|
+
|
|
340
390
|
def labscript_file(self):
|
|
341
391
|
try:
|
|
342
392
|
return self.client().get_labscript_file()
|
|
@@ -415,7 +465,9 @@ class MockBackend(object):
|
|
|
415
465
|
panel = self.read_panel()
|
|
416
466
|
if raw:
|
|
417
467
|
return dict(panel)
|
|
418
|
-
|
|
468
|
+
# Keep Python types (tuple vs list matters to the expansion rules); callers
|
|
469
|
+
# jsonable() values before they leave the bridge.
|
|
470
|
+
return dict((k, eval_panel_value(v)) for k, v in panel.items())
|
|
419
471
|
|
|
420
472
|
def set_globals(self, values, raw=False):
|
|
421
473
|
panel = self.read_panel()
|
|
@@ -425,17 +477,21 @@ class MockBackend(object):
|
|
|
425
477
|
return dict((k, panel[k]) for k in values)
|
|
426
478
|
|
|
427
479
|
def n_shots(self):
|
|
428
|
-
import numpy as np
|
|
429
480
|
total = 1
|
|
430
481
|
for value in self.get_globals(raw=False).values():
|
|
431
|
-
|
|
432
|
-
if
|
|
482
|
+
length = axis_length(value)
|
|
483
|
+
if length is None or length <= 0:
|
|
433
484
|
continue
|
|
434
|
-
total *=
|
|
485
|
+
total *= length
|
|
435
486
|
return int(total)
|
|
436
487
|
|
|
437
488
|
def error_in_globals(self):
|
|
438
|
-
|
|
489
|
+
# Simulator switch: mock.errorInGlobals makes the mock panel report an evaluation
|
|
490
|
+
# error, so the GLOBALS_ERROR refusal path can be tested without real hardware.
|
|
491
|
+
return bool((self.config.get("mock") or {}).get("errorInGlobals"))
|
|
492
|
+
|
|
493
|
+
def shuffle(self):
|
|
494
|
+
return bool((self.config.get("mock") or {}).get("shuffle"))
|
|
439
495
|
|
|
440
496
|
def run_shots(self):
|
|
441
497
|
return True
|
|
@@ -452,11 +508,8 @@ class MockBackend(object):
|
|
|
452
508
|
values = self.get_globals(raw=False)
|
|
453
509
|
arrays = {}
|
|
454
510
|
for key, value in values.items():
|
|
455
|
-
if
|
|
456
|
-
|
|
457
|
-
arr = np.asarray(value)
|
|
458
|
-
if arr.ndim >= 1 and arr.size > 1:
|
|
459
|
-
arrays[key] = arr
|
|
511
|
+
if (axis_length(value) or 0) > 1:
|
|
512
|
+
arrays[key] = np.asarray(value).ravel()
|
|
460
513
|
total = 1
|
|
461
514
|
for arr in arrays.values():
|
|
462
515
|
total *= int(arr.size)
|
|
@@ -565,6 +618,10 @@ class HttpSimBackend(object):
|
|
|
565
618
|
def run_shots(self):
|
|
566
619
|
return True
|
|
567
620
|
|
|
621
|
+
def shuffle(self):
|
|
622
|
+
snap = self.snapshot()
|
|
623
|
+
return bool(snap.get("shuffle")) if "shuffle" in snap else None
|
|
624
|
+
|
|
568
625
|
def labscript_file(self):
|
|
569
626
|
return self.snapshot().get("labscript_file")
|
|
570
627
|
|
|
@@ -948,13 +1005,8 @@ def cmd_status(config, args):
|
|
|
948
1005
|
scan_axes = {}
|
|
949
1006
|
n_shots_from_arrays = 1
|
|
950
1007
|
for name, value in panel_values.items():
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
try:
|
|
954
|
-
size = int(len(value))
|
|
955
|
-
except Exception:
|
|
956
|
-
continue
|
|
957
|
-
if size <= 1:
|
|
1008
|
+
size = axis_length(value)
|
|
1009
|
+
if size is None or size <= 1:
|
|
958
1010
|
continue
|
|
959
1011
|
scan_axes[str(name)] = size
|
|
960
1012
|
n_shots_from_arrays *= size
|
|
@@ -973,6 +1025,7 @@ def cmd_status(config, args):
|
|
|
973
1025
|
"n_shots": backend.n_shots(),
|
|
974
1026
|
"error_in_globals": backend.error_in_globals(),
|
|
975
1027
|
"run_shots": backend.run_shots(),
|
|
1028
|
+
"shuffle": backend.shuffle() if hasattr(backend, "shuffle") else None,
|
|
976
1029
|
"panel_history": panel_history_info(config),
|
|
977
1030
|
}
|
|
978
1031
|
if raw:
|
|
@@ -1123,7 +1176,12 @@ def wait_for_run(config, run_folder, expected, timeout_s):
|
|
|
1123
1176
|
if stable_since is None:
|
|
1124
1177
|
stable_since = time.time()
|
|
1125
1178
|
elif time.time() - stable_since >= settle:
|
|
1126
|
-
|
|
1179
|
+
hint = None
|
|
1180
|
+
if want and len(shots) != want:
|
|
1181
|
+
hint = ("finished with " + str(len(shots)) + " shot files for expected_n_shots=" + str(want) +
|
|
1182
|
+
": the panel's n_shots changed after submit, or shots were re-run (BLACS Repeat all / "
|
|
1183
|
+
"Repeat last re-executes existing shot files and does not create new ones).")
|
|
1184
|
+
return result(True, False, "complete", time.time() - start, hint)
|
|
1127
1185
|
else:
|
|
1128
1186
|
stable_since = None
|
|
1129
1187
|
if ready and executed is False:
|
|
@@ -1132,8 +1190,11 @@ def wait_for_run(config, run_folder, expected, timeout_s):
|
|
|
1132
1190
|
elif time.time() - compiled_since >= stall:
|
|
1133
1191
|
return result(
|
|
1134
1192
|
False, False, "compiled_not_executed", time.time() - start,
|
|
1135
|
-
"
|
|
1136
|
-
"
|
|
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.")
|
|
1137
1198
|
else:
|
|
1138
1199
|
compiled_since = None
|
|
1139
1200
|
last_count = count
|
|
@@ -1160,7 +1221,9 @@ def wait_for_run(config, run_folder, expected, timeout_s):
|
|
|
1160
1221
|
hint = "only " + str(count) + " of " + str(expected) + " shots appeared before the timeout; the run may still be compiling or blocked."
|
|
1161
1222
|
elif executed is False:
|
|
1162
1223
|
diagnosis = "compiled_not_executed"
|
|
1163
|
-
hint = "
|
|
1224
|
+
hint = ("shot files exist but no acquisition data was found: BLACS may be paused, may have refused the "
|
|
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.")
|
|
1164
1227
|
else:
|
|
1165
1228
|
diagnosis = "partial"
|
|
1166
1229
|
hint = "shots appeared but the run was not confirmed complete before the timeout."
|
|
@@ -1512,7 +1575,7 @@ def cmd_doctor(config, args):
|
|
|
1512
1575
|
probe = LabscriptBackend(config)
|
|
1513
1576
|
client = probe.client()
|
|
1514
1577
|
version = client.get_version() if hasattr(client, "get_version") else "unknown"
|
|
1515
|
-
_add_check(checks, "runmanager", "ok", str(version))
|
|
1578
|
+
_add_check(checks, "runmanager", "ok", str(version) + " at " + probe.endpoint())
|
|
1516
1579
|
try:
|
|
1517
1580
|
_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()))
|
|
1518
1581
|
except Exception as exc:
|
|
@@ -9,6 +9,7 @@ import shutil
|
|
|
9
9
|
import sys
|
|
10
10
|
import tempfile
|
|
11
11
|
import time
|
|
12
|
+
import types
|
|
12
13
|
|
|
13
14
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
14
15
|
sys.path.insert(0, os.path.dirname(HERE))
|
|
@@ -278,6 +279,120 @@ def test_wait_diagnosis_compiled_not_executed():
|
|
|
278
279
|
shutil.rmtree(tmp, ignore_errors=True)
|
|
279
280
|
|
|
280
281
|
|
|
282
|
+
def test_axis_heuristic_ignores_tuples_and_dicts():
|
|
283
|
+
"""runmanager does not expand tuples, so the scan-axes hint must not count them."""
|
|
284
|
+
tmp = tempfile.mkdtemp(prefix="dsh-labscript-axis-")
|
|
285
|
+
try:
|
|
286
|
+
cfg = make_config(tmp)
|
|
287
|
+
out = bridge.cmd_set(cfg, {"values": {"pulse_shape": "(0.1, 0.2, 0.3)"}, "raw": True})
|
|
288
|
+
assert out["missing"] == [], out
|
|
289
|
+
status = bridge.cmd_status(cfg, {})
|
|
290
|
+
assert "pulse_shape" not in status["scan_axes"], status["scan_axes"]
|
|
291
|
+
assert status["n_shots"] == 1, status["n_shots"]
|
|
292
|
+
assert status["n_shots_from_arrays"] == 1, status["n_shots_from_arrays"]
|
|
293
|
+
# a list/array global is an axis, a dict is not
|
|
294
|
+
bridge.cmd_set(cfg, {"values": {"freq": "np.arange(0.5, 6.7, 0.2)", "shape": "{'a': 1, 'b': 2}"}, "raw": True})
|
|
295
|
+
status = bridge.cmd_status(cfg, {})
|
|
296
|
+
assert status["scan_axes"].get("freq") == 31, status["scan_axes"]
|
|
297
|
+
assert "shape" not in status["scan_axes"], status["scan_axes"]
|
|
298
|
+
assert status["n_shots"] == 31, status["n_shots"]
|
|
299
|
+
assert status["n_shots_from_arrays"] == 31, status["n_shots_from_arrays"]
|
|
300
|
+
finally:
|
|
301
|
+
shutil.rmtree(tmp, ignore_errors=True)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def test_mock_error_in_globals_blocks_engage():
|
|
305
|
+
tmp = tempfile.mkdtemp(prefix="dsh-labscript-mock-error-")
|
|
306
|
+
try:
|
|
307
|
+
cfg = make_config(tmp)
|
|
308
|
+
assert bridge.cmd_status(cfg, {})["error_in_globals"] is False
|
|
309
|
+
cfg["mock"]["errorInGlobals"] = True
|
|
310
|
+
assert bridge.cmd_status(cfg, {})["error_in_globals"] is True
|
|
311
|
+
try:
|
|
312
|
+
bridge.cmd_engage(cfg, {})
|
|
313
|
+
except bridge.BridgeError as exc:
|
|
314
|
+
assert exc.code == "GLOBALS_ERROR", exc.code
|
|
315
|
+
else:
|
|
316
|
+
raise AssertionError("expected GLOBALS_ERROR")
|
|
317
|
+
finally:
|
|
318
|
+
shutil.rmtree(tmp, ignore_errors=True)
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def test_mock_reports_shuffle():
|
|
322
|
+
tmp = tempfile.mkdtemp(prefix="dsh-labscript-mock-shuffle-")
|
|
323
|
+
try:
|
|
324
|
+
cfg = make_config(tmp)
|
|
325
|
+
assert bridge.cmd_status(cfg, {})["shuffle"] is False
|
|
326
|
+
cfg["mock"]["shuffle"] = True
|
|
327
|
+
assert bridge.cmd_status(cfg, {})["shuffle"] is True
|
|
328
|
+
finally:
|
|
329
|
+
shutil.rmtree(tmp, ignore_errors=True)
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def test_real_client_uses_labconfig_defaults():
|
|
333
|
+
"""Unset host/port/timeout must be resolved by runmanager.remote from labconfig."""
|
|
334
|
+
class FakeClient(object):
|
|
335
|
+
last = None
|
|
336
|
+
|
|
337
|
+
def __init__(self, **kwargs):
|
|
338
|
+
self.kwargs = kwargs
|
|
339
|
+
self.host = kwargs.get("host") or "localhost"
|
|
340
|
+
self.port = kwargs.get("port") or 42523
|
|
341
|
+
self.timeout = kwargs.get("timeout") or 60
|
|
342
|
+
FakeClient.last = self
|
|
343
|
+
|
|
344
|
+
def say_hello(self):
|
|
345
|
+
return "hello"
|
|
346
|
+
|
|
347
|
+
def get_shuffle(self):
|
|
348
|
+
return True
|
|
349
|
+
|
|
350
|
+
class NoShuffleClient(FakeClient):
|
|
351
|
+
def get_shuffle(self):
|
|
352
|
+
raise AttributeError("get_shuffle")
|
|
353
|
+
|
|
354
|
+
fake_remote = types.ModuleType("runmanager.remote")
|
|
355
|
+
fake_remote.Client = FakeClient
|
|
356
|
+
fake_package = types.ModuleType("runmanager")
|
|
357
|
+
fake_package.remote = fake_remote
|
|
358
|
+
saved = dict((name, sys.modules.get(name)) for name in ("runmanager", "runmanager.remote"))
|
|
359
|
+
sys.modules["runmanager"] = fake_package
|
|
360
|
+
sys.modules["runmanager.remote"] = fake_remote
|
|
361
|
+
try:
|
|
362
|
+
bare = bridge.LabscriptBackend({})
|
|
363
|
+
client = bare.client()
|
|
364
|
+
assert client.kwargs == {}, client.kwargs
|
|
365
|
+
assert bare.shuffle() is True
|
|
366
|
+
explicit = bridge.LabscriptBackend({"runmanager": {"host": "10.0.0.5", "port": 43000, "timeoutS": 25}})
|
|
367
|
+
assert explicit.client().kwargs == {"host": "10.0.0.5", "port": 43000, "timeout": 25.0}
|
|
368
|
+
assert "10.0.0.5" in explicit.endpoint() and "config" in explicit.endpoint()
|
|
369
|
+
fake_remote.Client = NoShuffleClient
|
|
370
|
+
assert bridge.LabscriptBackend({}).shuffle() is None
|
|
371
|
+
finally:
|
|
372
|
+
for name, module in saved.items():
|
|
373
|
+
if module is None:
|
|
374
|
+
sys.modules.pop(name, None)
|
|
375
|
+
else:
|
|
376
|
+
sys.modules[name] = module
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def test_wait_reports_extra_shot_files():
|
|
380
|
+
tmp = tempfile.mkdtemp(prefix="dsh-labscript-extra-shots-")
|
|
381
|
+
try:
|
|
382
|
+
cfg = make_config(tmp)
|
|
383
|
+
cfg["engage"] = {"timeoutS": 5, "pollS": 0.05, "settleS": 0.1}
|
|
384
|
+
run_dir = os.path.join(tmp, "extra_run")
|
|
385
|
+
os.makedirs(run_dir, exist_ok=True)
|
|
386
|
+
for index in range(4):
|
|
387
|
+
write_shot(os.path.join(run_dir, "shot_000%d.h5" % index), executed=True)
|
|
388
|
+
result = bridge.wait_for_run(cfg, run_dir, 2, 4)
|
|
389
|
+
assert result["complete"] is True, result
|
|
390
|
+
assert result["n_h5"] == 4, result
|
|
391
|
+
assert "Repeat all" in result["hint"], result
|
|
392
|
+
finally:
|
|
393
|
+
shutil.rmtree(tmp, ignore_errors=True)
|
|
394
|
+
|
|
395
|
+
|
|
281
396
|
def write_shot(path, executed):
|
|
282
397
|
"""Write a shot h5 like labscript does: globals first, acquisition data later."""
|
|
283
398
|
import h5py
|