dsh-labscript 0.2.2 → 0.2.4
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 +23 -0
- package/README.md +5 -3
- package/lib/tools.js +30 -5
- package/package.json +1 -1
- package/python/bridge.py +9 -2
- package/python/tests/run_tests.py +42 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.4
|
|
4
|
+
|
|
5
|
+
- Fix: labs_data no longer answers h5: 0 for a run folder that does not exist. A missing
|
|
6
|
+
folder raises RUN_FOLDER_MISSING with the raw and mapped paths plus a hint: runmanager's
|
|
7
|
+
shot output folder is the folder the NEXT run will use and only exists after engage, so
|
|
8
|
+
historical runs must be passed explicitly. An existing folder with no shots reports
|
|
9
|
+
empty: true instead.
|
|
10
|
+
- Fix: labs_status with keys=[...] drops keys that are not panel globals instead of
|
|
11
|
+
returning null, so the rendered output shows <missing>. UI group names (for example
|
|
12
|
+
Select_Sequence) are not globals: read their leaf keys or a run's h5.
|
|
13
|
+
- Descriptions: labs_status says the shot output folder is the NEXT run folder and that
|
|
14
|
+
keys are leaf global names; labs_data explains the run_folder default and the error.
|
|
15
|
+
- Tests: missing run folder, empty run folder, missing keys dropped.
|
|
16
|
+
|
|
17
|
+
## 0.2.3
|
|
18
|
+
|
|
19
|
+
- Fix: labs_status now prints panel values. keys=[...] and raw=true were accepted but the
|
|
20
|
+
rendered summary never showed any global, so the model could only see the key count
|
|
21
|
+
(the tool result content is the render output; the returned object is not in context).
|
|
22
|
+
With keys the selected values are printed (240 chars each, <missing> when absent);
|
|
23
|
+
without keys it prints the count and a hint to pass keys.
|
|
24
|
+
- Tests: labs_status value rendering (values, count-only, missing keys).
|
|
25
|
+
|
|
3
26
|
## 0.2.2
|
|
4
27
|
|
|
5
28
|
- Fix: labs_status and labs_engage report pathMap-mapped paths. `labs_status.labscript`
|
package/README.md
CHANGED
|
@@ -125,8 +125,8 @@ is not a physics simulation.
|
|
|
125
125
|
## Development
|
|
126
126
|
|
|
127
127
|
npm install # only runtime dependency: yaml
|
|
128
|
-
npm test # node --test,
|
|
129
|
-
python python/tests/run_tests.py #
|
|
128
|
+
npm test # node --test, 36 tests (fake bridge, no python needed)
|
|
129
|
+
python python/tests/run_tests.py # 18 tests (needs numpy + h5py)
|
|
130
130
|
LABSCRIPT_PYTHON=/path/to/python3 node scripts/demo.mjs # end-to-end mock demo
|
|
131
131
|
|
|
132
132
|
A fresh clone must run `npm install` before `npm test`; the CI workflow does this.
|
|
@@ -157,7 +157,9 @@ A fresh clone must run `npm install` before `npm test`; the CI workflow does thi
|
|
|
157
157
|
partial). compiled_not_executed means shots exist but show no acquisition data yet;
|
|
158
158
|
BLACS may be paused or not executing the queue.
|
|
159
159
|
- Real runmanager engage does not return a run folder; the bridge captures the
|
|
160
|
-
output folder before engage and reports it as the run folder.
|
|
160
|
+
output folder before engage and reports it as the run folder. That shot output
|
|
161
|
+
folder is the NEXT run's folder and does not exist until engage, so labs_data
|
|
162
|
+
errors (RUN_FOLDER_MISSING) instead of reporting h5: 0 for a path that is not there.
|
|
161
163
|
- The panel is snapshotted before every write; labs_restore restores only the keys the
|
|
162
164
|
write touched (full=true restores the whole panel), so unrelated concurrent edits survive.
|
|
163
165
|
- Actions are logged to events.jsonl next to the snapshots.
|
package/lib/tools.js
CHANGED
|
@@ -35,6 +35,30 @@ function progressLine(run) {
|
|
|
35
35
|
return parts.join(' ')
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
// The bridge returns the whole panel; without this the summary alone hides it.
|
|
39
|
+
function globalText(value, limit = 240) {
|
|
40
|
+
let rendered
|
|
41
|
+
try { rendered = JSON.stringify(value) } catch { rendered = String(value) }
|
|
42
|
+
if (rendered === undefined) rendered = String(value)
|
|
43
|
+
return rendered.length > limit ? rendered.slice(0, limit) + '…' : rendered
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function globalLines(args, value) {
|
|
47
|
+
const globals = value.globals || {}
|
|
48
|
+
const keys = Array.isArray(args.keys) ? args.keys : []
|
|
49
|
+
if (keys.length === 0) {
|
|
50
|
+
const count = Object.keys(globals).length
|
|
51
|
+
return [count > 0 ? 'globals: ' + count + ' keys (pass keys=[...] to read values)' : 'globals: none']
|
|
52
|
+
}
|
|
53
|
+
const raw = args.raw === true && value.raw_globals ? value.raw_globals : null
|
|
54
|
+
const out = ['globals:']
|
|
55
|
+
for (const key of keys) {
|
|
56
|
+
const shown = raw ? raw[key] : globals[key]
|
|
57
|
+
out.push(' ' + key + ' = ' + (shown === undefined ? '<missing>' : globalText(shown)))
|
|
58
|
+
}
|
|
59
|
+
return out
|
|
60
|
+
}
|
|
61
|
+
|
|
38
62
|
async function pendingProgress(runtime, exec) {
|
|
39
63
|
const pending = runtime.state.pending
|
|
40
64
|
if (!pending) return null
|
|
@@ -105,7 +129,7 @@ export function registerTools(ctx, runtime) {
|
|
|
105
129
|
|
|
106
130
|
ctx.tools.register({
|
|
107
131
|
name: 'labs_status',
|
|
108
|
-
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. Paths in the output are already mapped for this machine through pathMap; do not map them again.',
|
|
132
|
+
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>. The shot output folder is the folder the NEXT run will use; it does not exist until engage. Paths in the output are already mapped for this machine through pathMap; do not map them again.',
|
|
109
133
|
parameters: {
|
|
110
134
|
type: 'object',
|
|
111
135
|
properties: {
|
|
@@ -115,13 +139,14 @@ export function registerTools(ctx, runtime) {
|
|
|
115
139
|
additionalProperties: false,
|
|
116
140
|
},
|
|
117
141
|
isConcurrencySafe: () => true,
|
|
118
|
-
output: looseOutput((
|
|
142
|
+
output: looseOutput((args, value) => text(lines([
|
|
119
143
|
'backend: ' + value.backend + ' plugin: v' + runtime.version,
|
|
120
144
|
'labscript: ' + value.labscript,
|
|
121
145
|
'shot output: ' + value.shot_output_folder,
|
|
122
146
|
'n_shots: ' + value.n_shots + ' error_in_globals: ' + value.error_in_globals + ' run_shots: ' + value.run_shots,
|
|
123
147
|
progressLine(value.last_run),
|
|
124
148
|
'panel history: ' + (value.panel_history ? value.panel_history.dir + ' (' + value.panel_history.count + ' snapshots)' : 'n/a'),
|
|
149
|
+
...globalLines(args, value),
|
|
125
150
|
]))),
|
|
126
151
|
async execute(args, exec) {
|
|
127
152
|
const data = await runtime.runBridge('status', {
|
|
@@ -314,11 +339,11 @@ export function registerTools(ctx, runtime) {
|
|
|
314
339
|
|
|
315
340
|
ctx.tools.register({
|
|
316
341
|
name: 'labs_data',
|
|
317
|
-
description: 'Read a run folder HDF5: globals of the first shot (execution facts) and, with scan: a per-shot table of scan values and numeric results. Read-only.',
|
|
342
|
+
description: 'Read a run folder HDF5: globals of the first shot (execution facts) and, with scan: a per-shot table of scan values and numeric results. Read-only. run_folder defaults to the run this DSH session submitted; for a historical run pass its folder explicitly. A missing folder is an error: runmanager\'s shot output is the NEXT run folder and only exists after engage.',
|
|
318
343
|
parameters: {
|
|
319
344
|
type: 'object',
|
|
320
345
|
properties: {
|
|
321
|
-
run_folder: { type: 'string', description: 'Run folder; defaults to the
|
|
346
|
+
run_folder: { type: 'string', description: 'Run folder; defaults to the run submitted from this DSH session (pass a historical run folder explicitly)' },
|
|
322
347
|
scan: { type: 'string', description: 'Panel global name used as the scan axis; adds a per-shot table' },
|
|
323
348
|
globals: { type: 'boolean', description: 'Include the first shot globals (default true)' },
|
|
324
349
|
results: { type: 'boolean', description: 'Include the results layout' },
|
|
@@ -328,7 +353,7 @@ export function registerTools(ctx, runtime) {
|
|
|
328
353
|
isConcurrencySafe: () => true,
|
|
329
354
|
output: looseOutput((_args, value) => text(lines([
|
|
330
355
|
'run: ' + (value.mapped_run_folder || value.run_folder),
|
|
331
|
-
'h5: ' + value.n_h5,
|
|
356
|
+
'h5: ' + value.n_h5 + (value.empty ? ' (folder exists, no shots yet)' : ''),
|
|
332
357
|
value.scan_table ? 'scan ' + value.scan_table.scan + ': ' + value.scan_table.n_points + ' shots' : '',
|
|
333
358
|
value.globals ? 'globals: ' + Object.keys(value.globals).length + ' keys' : '',
|
|
334
359
|
]))),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-labscript",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
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
|
@@ -922,8 +922,8 @@ def cmd_status(config, args):
|
|
|
922
922
|
values = backend.get_globals(raw=False)
|
|
923
923
|
raw_values = backend.get_globals(raw=True)
|
|
924
924
|
if keys:
|
|
925
|
-
values = dict((k, values
|
|
926
|
-
raw_values = dict((k, raw_values
|
|
925
|
+
values = dict((k, values[k]) for k in keys if k in values)
|
|
926
|
+
raw_values = dict((k, raw_values[k]) for k in keys if k in raw_values)
|
|
927
927
|
out = {
|
|
928
928
|
"backend": config.get("backend"),
|
|
929
929
|
"version": backend.version() if hasattr(backend, "version") else None,
|
|
@@ -1182,9 +1182,16 @@ def cmd_data(config, args):
|
|
|
1182
1182
|
if not run_folder:
|
|
1183
1183
|
raise BridgeError("BAD_ARGS", "run_folder is required")
|
|
1184
1184
|
folder = map_path(config, run_folder)
|
|
1185
|
+
if not os.path.isdir(folder):
|
|
1186
|
+
raise BridgeError("RUN_FOLDER_MISSING", "run folder does not exist: " + str(folder), {
|
|
1187
|
+
"run_folder": run_folder,
|
|
1188
|
+
"mapped_run_folder": folder,
|
|
1189
|
+
"hint": "runmanager's shot output folder is the folder the NEXT run will use; it does not exist until engage. Pass a historical run folder explicitly.",
|
|
1190
|
+
})
|
|
1185
1191
|
shots = sorted(glob.glob(os.path.join(folder, "**", "*.h5"), recursive=True))
|
|
1186
1192
|
out = {"run_folder": run_folder, "mapped_run_folder": folder, "n_h5": len(shots)}
|
|
1187
1193
|
if not shots:
|
|
1194
|
+
out["empty"] = True
|
|
1188
1195
|
return out
|
|
1189
1196
|
scan = args.get("scan")
|
|
1190
1197
|
with h5py.File(shots[0], "r", locking=False) as handle:
|
|
@@ -346,6 +346,48 @@ def test_progress_reports_mapped_run_folder():
|
|
|
346
346
|
assert out["mapped_run_folder"] == "\\\\lab-pc\\Experiments\\nope", out
|
|
347
347
|
|
|
348
348
|
|
|
349
|
+
def test_data_missing_run_folder():
|
|
350
|
+
tmp = tempfile.mkdtemp(prefix="dsh-labscript-data-missing-")
|
|
351
|
+
try:
|
|
352
|
+
cfg = make_config(tmp)
|
|
353
|
+
missing = os.path.join(tmp, "no_such_run")
|
|
354
|
+
try:
|
|
355
|
+
bridge.cmd_data(cfg, {"run_folder": missing})
|
|
356
|
+
except bridge.BridgeError as exc:
|
|
357
|
+
assert exc.code == "RUN_FOLDER_MISSING", exc.message
|
|
358
|
+
assert exc.detail["mapped_run_folder"] == missing, exc.detail
|
|
359
|
+
assert "next" in exc.detail["hint"].lower(), exc.detail
|
|
360
|
+
else:
|
|
361
|
+
raise AssertionError("expected RUN_FOLDER_MISSING")
|
|
362
|
+
finally:
|
|
363
|
+
shutil.rmtree(tmp, ignore_errors=True)
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def test_data_empty_run_folder():
|
|
367
|
+
tmp = tempfile.mkdtemp(prefix="dsh-labscript-data-empty-")
|
|
368
|
+
try:
|
|
369
|
+
cfg = make_config(tmp)
|
|
370
|
+
run_dir = os.path.join(tmp, "empty_run")
|
|
371
|
+
os.makedirs(run_dir, exist_ok=True)
|
|
372
|
+
out = bridge.cmd_data(cfg, {"run_folder": run_dir})
|
|
373
|
+
assert out["n_h5"] == 0, out
|
|
374
|
+
assert out.get("empty") is True, out
|
|
375
|
+
finally:
|
|
376
|
+
shutil.rmtree(tmp, ignore_errors=True)
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def test_status_drops_missing_keys():
|
|
380
|
+
tmp = tempfile.mkdtemp(prefix="dsh-labscript-status-missing-")
|
|
381
|
+
try:
|
|
382
|
+
cfg = make_config(tmp)
|
|
383
|
+
out = bridge.cmd_status(cfg, {"keys": ["amplitude", "Select_Sequence"], "raw": True})
|
|
384
|
+
assert "amplitude" in out["globals"], out["globals"]
|
|
385
|
+
assert "Select_Sequence" not in out["globals"], out["globals"]
|
|
386
|
+
assert "Select_Sequence" not in out["raw_globals"], out["raw_globals"]
|
|
387
|
+
finally:
|
|
388
|
+
shutil.rmtree(tmp, ignore_errors=True)
|
|
389
|
+
|
|
390
|
+
|
|
349
391
|
def main():
|
|
350
392
|
tests = [value for name, value in sorted(globals().items()) if name.startswith("test_")]
|
|
351
393
|
failures = 0
|