dsh-labscript 0.2.4 → 0.2.6
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 +26 -0
- package/README.md +2 -2
- package/lib/tools.js +98 -11
- package/package.json +1 -1
- package/python/bridge.py +49 -5
- package/python/tests/run_tests.py +45 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.6
|
|
4
|
+
|
|
5
|
+
- Fix: labs_data rendered only counts. It now prints selected first-shot globals
|
|
6
|
+
(keys=[...] to choose, ["*"] for all, <missing> for absent keys), the results group
|
|
7
|
+
names when results: true, and a scan axis summary (unique points, min/max, median
|
|
8
|
+
step, numeric column count). Pass limit to print up to 20 raw scan rows; the bridge
|
|
9
|
+
only returns rows when limit is given.
|
|
10
|
+
- Tests: first-shot key selection, scan axis summary and columns, on-demand rows, and
|
|
11
|
+
the rendered output for keys/results/scan/limit.
|
|
12
|
+
|
|
13
|
+
## 0.2.5
|
|
14
|
+
|
|
15
|
+
- labs_status can list every panel global: keys=["*"] prints all of them, sorted
|
|
16
|
+
(raw=true prints the raw expressions). This makes leftover array globals visible,
|
|
17
|
+
which is what makes labs_protocol refuse to run with N_SHOTS_MISMATCH.
|
|
18
|
+
- labs_status reports scan axes: the array-valued globals with their lengths and
|
|
19
|
+
n_shots_from_arrays (their product), and flags when the panel n_shots differs from
|
|
20
|
+
that product.
|
|
21
|
+
- labs_wait fails immediately when an explicit run folder does not exist and is not
|
|
22
|
+
this session's pending run (it used to wait for the full timeout), and the result
|
|
23
|
+
shows whether the folder exists.
|
|
24
|
+
- labs_data description: run_folder defaults to the pending run, else the last run
|
|
25
|
+
submitted through this plugin (persisted across restarts).
|
|
26
|
+
- cmd_progress returns folder_exists.
|
|
27
|
+
- Tests: scan axes, all-globals rendering, wait fast-fail, progress folder_exists.
|
|
28
|
+
|
|
3
29
|
## 0.2.4
|
|
4
30
|
|
|
5
31
|
- Fix: labs_data no longer answers h5: 0 for a run folder that does not exist. A missing
|
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, 39 tests (fake bridge, no python needed)
|
|
129
|
+
python python/tests/run_tests.py # 20 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.
|
package/lib/tools.js
CHANGED
|
@@ -46,11 +46,16 @@ function globalText(value, limit = 240) {
|
|
|
46
46
|
function globalLines(args, value) {
|
|
47
47
|
const globals = value.globals || {}
|
|
48
48
|
const keys = Array.isArray(args.keys) ? args.keys : []
|
|
49
|
+
const raw = args.raw === true && value.raw_globals ? value.raw_globals : null
|
|
50
|
+
if (keys.length === 1 && keys[0] === '*') {
|
|
51
|
+
const shown = raw || globals
|
|
52
|
+
const names = Object.keys(shown).sort()
|
|
53
|
+
return ['globals: all ' + names.length].concat(names.map((key) => ' ' + key + ' = ' + globalText(shown[key])))
|
|
54
|
+
}
|
|
49
55
|
if (keys.length === 0) {
|
|
50
56
|
const count = Object.keys(globals).length
|
|
51
|
-
return [count > 0 ? 'globals: ' + count + ' keys (pass keys=[...] to read values)' : 'globals: none']
|
|
57
|
+
return [count > 0 ? 'globals: ' + count + ' keys (pass keys=[...] or keys=["*"] to read values)' : 'globals: none']
|
|
52
58
|
}
|
|
53
|
-
const raw = args.raw === true && value.raw_globals ? value.raw_globals : null
|
|
54
59
|
const out = ['globals:']
|
|
55
60
|
for (const key of keys) {
|
|
56
61
|
const shown = raw ? raw[key] : globals[key]
|
|
@@ -59,6 +64,64 @@ function globalLines(args, value) {
|
|
|
59
64
|
return out
|
|
60
65
|
}
|
|
61
66
|
|
|
67
|
+
function scanLine(value) {
|
|
68
|
+
const axes = value.scan_axes && typeof value.scan_axes === 'object' ? value.scan_axes : {}
|
|
69
|
+
const names = Object.keys(axes).sort()
|
|
70
|
+
const product = value.n_shots_from_arrays
|
|
71
|
+
const mismatch = typeof product === 'number' && typeof value.n_shots === 'number' && value.n_shots !== product
|
|
72
|
+
if (names.length === 0 && !mismatch) return ''
|
|
73
|
+
const detail = names.length ? names.map((key) => key + '=' + axes[key]).join(', ') : 'none'
|
|
74
|
+
return 'scan axes: ' + detail + ' n_shots_from_arrays=' + product + (mismatch ? ' (panel n_shots=' + value.n_shots + ' differs)' : '')
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function resultsLine(value) {
|
|
78
|
+
const layout = value.results_layout
|
|
79
|
+
if (!layout) return ''
|
|
80
|
+
const names = Object.keys(layout).sort()
|
|
81
|
+
if (names.length === 0) return 'results: none'
|
|
82
|
+
return 'results: ' + names.map((group) => group + ' (' + layout[group].length + ' fields)').join(', ')
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function dataGlobalsLines(args, value) {
|
|
86
|
+
const globals = value.globals
|
|
87
|
+
if (!globals) return []
|
|
88
|
+
const keys = Array.isArray(args.keys) ? args.keys.map(String) : []
|
|
89
|
+
if (keys.length === 0) {
|
|
90
|
+
return ['globals: ' + Object.keys(globals).length + ' keys (pass keys=[...] or keys=["*"] to print values)']
|
|
91
|
+
}
|
|
92
|
+
const wanted = keys.length === 1 && keys[0] === '*' ? Object.keys(globals).sort() : keys
|
|
93
|
+
return ['globals:'].concat(wanted.map((key) => ' ' + key + ' = ' + (Object.prototype.hasOwnProperty.call(globals, key) ? globalText(globals[key]) : '<missing>')))
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function scanSummaryLine(value) {
|
|
97
|
+
const table = value.scan_table
|
|
98
|
+
if (!table) return ''
|
|
99
|
+
const axis = table.axis || {}
|
|
100
|
+
const unique = typeof axis.points === 'number' && axis.points > 0
|
|
101
|
+
? axis.points + ' unique ' + axis.min + '..' + axis.max + (axis.step === null || axis.step === undefined ? '' : ' step ' + axis.step)
|
|
102
|
+
: 'no numeric axis values'
|
|
103
|
+
const columns = Array.isArray(table.columns) ? table.columns.length : 0
|
|
104
|
+
return 'scan ' + table.scan + ': ' + table.n_points + ' shots, ' + unique + ', ' + columns + ' numeric columns' + (Array.isArray(table.rows) ? ' (' + table.rows.length + ' raw rows below)' : '')
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function scanRowLines(value) {
|
|
108
|
+
const table = value.scan_table
|
|
109
|
+
if (!table || !Array.isArray(table.rows) || table.rows.length === 0) return []
|
|
110
|
+
const lines = ['rows (first ' + table.rows.length + '):']
|
|
111
|
+
for (const row of table.rows) {
|
|
112
|
+
const parts = []
|
|
113
|
+
if (row.shot !== undefined) parts.push('shot=' + row.shot)
|
|
114
|
+
if (row[table.scan] !== undefined) parts.push(table.scan + '=' + row[table.scan])
|
|
115
|
+
for (const key of Object.keys(row).sort()) {
|
|
116
|
+
if (key === 'shot' || key === table.scan) continue
|
|
117
|
+
parts.push(key + '=' + row[key])
|
|
118
|
+
}
|
|
119
|
+
const line = parts.join(' ')
|
|
120
|
+
lines.push(' ' + (line.length > 240 ? line.slice(0, 240) + '…' : line))
|
|
121
|
+
}
|
|
122
|
+
return lines
|
|
123
|
+
}
|
|
124
|
+
|
|
62
125
|
async function pendingProgress(runtime, exec) {
|
|
63
126
|
const pending = runtime.state.pending
|
|
64
127
|
if (!pending) return null
|
|
@@ -129,11 +192,11 @@ export function registerTools(ctx, runtime) {
|
|
|
129
192
|
|
|
130
193
|
ctx.tools.register({
|
|
131
194
|
name: 'labs_status',
|
|
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
|
|
195
|
+
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>; keys=["*"] prints every global. scan axes lists array-valued globals (their lengths multiply to n_shots_from_arrays) and a differing panel n_shots is flagged. 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.',
|
|
133
196
|
parameters: {
|
|
134
197
|
type: 'object',
|
|
135
198
|
properties: {
|
|
136
|
-
keys: { type: 'array', items: { type: 'string' }, description: 'Only return these panel globals' },
|
|
199
|
+
keys: { type: 'array', items: { type: 'string' }, description: 'Only return these panel globals; ["*"] returns every global' },
|
|
137
200
|
raw: { type: 'boolean', description: 'Also return raw (expression) values' },
|
|
138
201
|
},
|
|
139
202
|
additionalProperties: false,
|
|
@@ -146,11 +209,14 @@ export function registerTools(ctx, runtime) {
|
|
|
146
209
|
'n_shots: ' + value.n_shots + ' error_in_globals: ' + value.error_in_globals + ' run_shots: ' + value.run_shots,
|
|
147
210
|
progressLine(value.last_run),
|
|
148
211
|
'panel history: ' + (value.panel_history ? value.panel_history.dir + ' (' + value.panel_history.count + ' snapshots)' : 'n/a'),
|
|
212
|
+
scanLine(value),
|
|
149
213
|
...globalLines(args, value),
|
|
150
214
|
]))),
|
|
151
215
|
async execute(args, exec) {
|
|
216
|
+
const keys = Array.isArray(args.keys) ? args.keys : []
|
|
217
|
+
const all = keys.length === 1 && keys[0] === '*'
|
|
152
218
|
const data = await runtime.runBridge('status', {
|
|
153
|
-
keys:
|
|
219
|
+
keys: all || keys.length === 0 ? undefined : keys,
|
|
154
220
|
raw: args.raw === true,
|
|
155
221
|
}, { signal: exec.signal })
|
|
156
222
|
if (runtime.state.pending) data.last_run = await pendingProgress(runtime, exec)
|
|
@@ -230,7 +296,7 @@ export function registerTools(ctx, runtime) {
|
|
|
230
296
|
|
|
231
297
|
ctx.tools.register({
|
|
232
298
|
name: 'labs_wait',
|
|
233
|
-
description: 'Wait for the pending run to finish (blocking, timeout in seconds). If the pending run 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 shots and, for protocols, the postprocess/finish/restore results.',
|
|
299
|
+
description: 'Wait for the pending run to finish (blocking, timeout in seconds). If the pending run 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 shots and, for protocols, the postprocess/finish/restore results. A run folder that does not exist and is not the pending run of this session fails immediately instead of waiting.',
|
|
234
300
|
parameters: {
|
|
235
301
|
type: 'object',
|
|
236
302
|
properties: {
|
|
@@ -244,6 +310,7 @@ export function registerTools(ctx, runtime) {
|
|
|
244
310
|
output: looseOutput((_args, value) => text(lines([
|
|
245
311
|
'run: ' + (value.mapped_run_folder || value.run_folder),
|
|
246
312
|
value.aborted ? 'wait cancelled by labs_abort (run aborted)' : '',
|
|
313
|
+
value.folder_exists === false ? 'folder: missing' : '',
|
|
247
314
|
'shots: ' + value.n_h5 + '/' + (value.expected_n_shots || '?') + ' complete: ' + value.complete + (value.timed_out ? ' TIMED OUT' : '') + (value.diagnosis ? ' diagnosis: ' + value.diagnosis : ''),
|
|
248
315
|
value.hint ? 'hint: ' + value.hint : '',
|
|
249
316
|
value.finalize && value.finalize.postprocess ? 'postprocess: ' + JSON.stringify(value.finalize.postprocess).slice(0, 400) : '',
|
|
@@ -256,6 +323,20 @@ export function registerTools(ctx, runtime) {
|
|
|
256
323
|
const runFolder = args.run_folder || (pending ? pending.run_folder : '')
|
|
257
324
|
if (!runFolder) throw new LabError('NO_PENDING_RUN', 'no run folder: submit with labs_engage or labs_protocol run first, or pass run_folder')
|
|
258
325
|
const expected = pending && pending.run_folder === runFolder ? pending.expected_n_shots : undefined
|
|
326
|
+
if (!(pending && pending.run_folder === runFolder)) {
|
|
327
|
+
let probe = null
|
|
328
|
+
try {
|
|
329
|
+
probe = await runtime.runBridge('progress', {
|
|
330
|
+
run_folder: runFolder,
|
|
331
|
+
expected_n_shots: expected,
|
|
332
|
+
}, { signal: exec.signal })
|
|
333
|
+
} catch (error) {
|
|
334
|
+
probe = null
|
|
335
|
+
}
|
|
336
|
+
if (probe && probe.folder_exists === false) {
|
|
337
|
+
throw new LabError('RUN_FOLDER_MISSING', 'run folder does not exist: ' + (probe.mapped_run_folder || runFolder) + '. Pass the folder of a run that exists (labs_engage / labs_protocol return one), or check pathMap.')
|
|
338
|
+
}
|
|
339
|
+
}
|
|
259
340
|
const timeoutS = Number(args.timeout_s || runtime.config.engage.timeoutS || 900)
|
|
260
341
|
let waited
|
|
261
342
|
try {
|
|
@@ -339,23 +420,27 @@ export function registerTools(ctx, runtime) {
|
|
|
339
420
|
|
|
340
421
|
ctx.tools.register({
|
|
341
422
|
name: 'labs_data',
|
|
342
|
-
description: 'Read a run folder HDF5: globals
|
|
423
|
+
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 scan axis summary (unique points, min/max, median step) plus the numeric result columns; pass limit to print 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); 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.',
|
|
343
424
|
parameters: {
|
|
344
425
|
type: 'object',
|
|
345
426
|
properties: {
|
|
346
427
|
run_folder: { type: 'string', description: 'Run folder; defaults to the run submitted from this DSH session (pass a historical run folder explicitly)' },
|
|
347
|
-
scan: { type: 'string', description: 'Panel global name used as the scan axis; adds
|
|
428
|
+
scan: { type: 'string', description: 'Panel global name used as the scan axis; adds an axis summary and numeric columns (pass limit for raw rows)' },
|
|
348
429
|
globals: { type: 'boolean', description: 'Include the first shot globals (default true)' },
|
|
430
|
+
keys: { type: 'array', items: { type: 'string' }, description: 'First-shot globals to print; ["*"] prints all. Missing keys show as <missing>.' },
|
|
431
|
+
limit: { type: 'number', minimum: 1, maximum: 20, description: 'How many raw scan rows to print (only with scan; default: none)' },
|
|
349
432
|
results: { type: 'boolean', description: 'Include the results layout' },
|
|
350
433
|
},
|
|
351
434
|
additionalProperties: false,
|
|
352
435
|
},
|
|
353
436
|
isConcurrencySafe: () => true,
|
|
354
|
-
output: looseOutput((
|
|
437
|
+
output: looseOutput((args, value) => text(lines([
|
|
355
438
|
'run: ' + (value.mapped_run_folder || value.run_folder),
|
|
356
439
|
'h5: ' + value.n_h5 + (value.empty ? ' (folder exists, no shots yet)' : ''),
|
|
357
|
-
value
|
|
358
|
-
|
|
440
|
+
resultsLine(value),
|
|
441
|
+
...dataGlobalsLines(args, value),
|
|
442
|
+
scanSummaryLine(value),
|
|
443
|
+
...scanRowLines(value),
|
|
359
444
|
]))),
|
|
360
445
|
async execute(args, exec) {
|
|
361
446
|
const runFolder = args.run_folder || (runtime.state.pending ? runtime.state.pending.run_folder : '') || (runtime.state.lastRun ? runtime.state.lastRun.run_folder : '')
|
|
@@ -363,6 +448,8 @@ export function registerTools(ctx, runtime) {
|
|
|
363
448
|
return runtime.runBridge('data', {
|
|
364
449
|
run_folder: runFolder,
|
|
365
450
|
scan: args.scan,
|
|
451
|
+
keys: Array.isArray(args.keys) ? args.keys : undefined,
|
|
452
|
+
limit: typeof args.limit === 'number' ? args.limit : undefined,
|
|
366
453
|
globals: args.globals !== false,
|
|
367
454
|
results: args.results === true,
|
|
368
455
|
}, { signal: exec.signal })
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-labscript",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.6",
|
|
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
|
@@ -919,10 +919,24 @@ def cmd_status(config, args):
|
|
|
919
919
|
backend = make_backend(config)
|
|
920
920
|
keys = args.get("keys")
|
|
921
921
|
raw = bool(args.get("raw"))
|
|
922
|
-
|
|
922
|
+
panel_values = backend.get_globals(raw=False)
|
|
923
923
|
raw_values = backend.get_globals(raw=True)
|
|
924
|
+
scan_axes = {}
|
|
925
|
+
n_shots_from_arrays = 1
|
|
926
|
+
for name, value in panel_values.items():
|
|
927
|
+
if isinstance(value, str) or not hasattr(value, "__len__"):
|
|
928
|
+
continue
|
|
929
|
+
try:
|
|
930
|
+
size = int(len(value))
|
|
931
|
+
except Exception:
|
|
932
|
+
continue
|
|
933
|
+
if size <= 1:
|
|
934
|
+
continue
|
|
935
|
+
scan_axes[str(name)] = size
|
|
936
|
+
n_shots_from_arrays *= size
|
|
937
|
+
values = dict(panel_values)
|
|
924
938
|
if keys:
|
|
925
|
-
values = dict((k,
|
|
939
|
+
values = dict((k, panel_values[k]) for k in keys if k in panel_values)
|
|
926
940
|
raw_values = dict((k, raw_values[k]) for k in keys if k in raw_values)
|
|
927
941
|
out = {
|
|
928
942
|
"backend": config.get("backend"),
|
|
@@ -930,6 +944,8 @@ def cmd_status(config, args):
|
|
|
930
944
|
"labscript": map_path(config, backend.labscript_file()),
|
|
931
945
|
"shot_output_folder": map_path(config, backend.shot_output_folder()),
|
|
932
946
|
"globals": jsonable(values),
|
|
947
|
+
"scan_axes": scan_axes,
|
|
948
|
+
"n_shots_from_arrays": n_shots_from_arrays,
|
|
933
949
|
"n_shots": backend.n_shots(),
|
|
934
950
|
"error_in_globals": backend.error_in_globals(),
|
|
935
951
|
"run_shots": backend.run_shots(),
|
|
@@ -1127,7 +1143,7 @@ def cmd_progress(config, args):
|
|
|
1127
1143
|
expected = int(args.get("expected_n_shots") or 0)
|
|
1128
1144
|
folder = map_path(config, run_folder)
|
|
1129
1145
|
shots = sorted(glob.glob(os.path.join(folder, "**", "*.h5"), recursive=True)) if folder else []
|
|
1130
|
-
return {"run_folder": run_folder, "mapped_run_folder": folder, "n_h5": len(shots), "expected_n_shots": expected, "complete": bool(expected and len(shots) >= expected)}
|
|
1146
|
+
return {"run_folder": run_folder, "mapped_run_folder": folder, "folder_exists": bool(folder) and os.path.isdir(folder), "n_h5": len(shots), "expected_n_shots": expected, "complete": bool(expected and len(shots) >= expected)}
|
|
1131
1147
|
|
|
1132
1148
|
|
|
1133
1149
|
|
|
@@ -1197,7 +1213,12 @@ def cmd_data(config, args):
|
|
|
1197
1213
|
with h5py.File(shots[0], "r", locking=False) as handle:
|
|
1198
1214
|
globals_attrs = dict(handle["globals"].attrs) if "globals" in handle else {}
|
|
1199
1215
|
if args.get("globals", True):
|
|
1200
|
-
|
|
1216
|
+
selected = jsonable(globals_attrs)
|
|
1217
|
+
keys = args.get("keys")
|
|
1218
|
+
if keys and list(map(str, keys)) != ["*"]:
|
|
1219
|
+
wanted = [str(key) for key in keys]
|
|
1220
|
+
selected = dict((key, selected[key]) for key in wanted if key in selected)
|
|
1221
|
+
out["globals"] = selected
|
|
1201
1222
|
if args.get("results"):
|
|
1202
1223
|
layout = {}
|
|
1203
1224
|
if "results" in handle:
|
|
@@ -1236,7 +1257,30 @@ def cmd_data(config, args):
|
|
|
1236
1257
|
if number is not None:
|
|
1237
1258
|
row[group_name + "." + key] = number
|
|
1238
1259
|
rows.append(row)
|
|
1239
|
-
|
|
1260
|
+
axis_values = [float(row[scan]) for row in rows if isinstance(row.get(scan), (int, float))]
|
|
1261
|
+
unique = sorted(set(axis_values))
|
|
1262
|
+
steps = [round(b - a, 9) for a, b in zip(unique, unique[1:])]
|
|
1263
|
+
columns = sorted(set(key for row in rows for key in row) - {"shot", scan})
|
|
1264
|
+
table = {
|
|
1265
|
+
"scan": scan,
|
|
1266
|
+
"n_points": len(rows),
|
|
1267
|
+
"axis": {
|
|
1268
|
+
"points": len(unique),
|
|
1269
|
+
"min": unique[0] if unique else None,
|
|
1270
|
+
"max": unique[-1] if unique else None,
|
|
1271
|
+
"step": (sorted(steps)[len(steps) // 2] if steps else None),
|
|
1272
|
+
},
|
|
1273
|
+
"columns": columns,
|
|
1274
|
+
}
|
|
1275
|
+
limit = args.get("limit")
|
|
1276
|
+
if limit:
|
|
1277
|
+
try:
|
|
1278
|
+
limit = max(1, min(20, int(limit)))
|
|
1279
|
+
except Exception:
|
|
1280
|
+
limit = 0
|
|
1281
|
+
if limit:
|
|
1282
|
+
table["rows"] = rows[:limit]
|
|
1283
|
+
out["scan_table"] = table
|
|
1240
1284
|
return out
|
|
1241
1285
|
|
|
1242
1286
|
|
|
@@ -232,7 +232,7 @@ def test_data_reading():
|
|
|
232
232
|
cfg = make_config(tmp)
|
|
233
233
|
started = bridge.cmd_protocol_start(cfg, {"protocol": protocol_raw()})
|
|
234
234
|
run_folder = started["engage"]["run_folder"]
|
|
235
|
-
data = bridge.cmd_data(cfg, {"run_folder": run_folder, "scan": "duration_test_us"})
|
|
235
|
+
data = bridge.cmd_data(cfg, {"run_folder": run_folder, "scan": "duration_test_us", "limit": 1})
|
|
236
236
|
assert data["n_h5"] == 6
|
|
237
237
|
assert data["scan_table"]["n_points"] == 6
|
|
238
238
|
assert data["globals"]["amplitude"] == 0.5
|
|
@@ -344,6 +344,7 @@ def test_progress_reports_mapped_run_folder():
|
|
|
344
344
|
out = bridge.cmd_progress(cfg, {"run_folder": "E:\\Experiments\\nope", "expected_n_shots": 1})
|
|
345
345
|
assert out["run_folder"] == "E:\\Experiments\\nope", out
|
|
346
346
|
assert out["mapped_run_folder"] == "\\\\lab-pc\\Experiments\\nope", out
|
|
347
|
+
assert out["folder_exists"] is False, out
|
|
347
348
|
|
|
348
349
|
|
|
349
350
|
def test_data_missing_run_folder():
|
|
@@ -388,6 +389,49 @@ def test_status_drops_missing_keys():
|
|
|
388
389
|
shutil.rmtree(tmp, ignore_errors=True)
|
|
389
390
|
|
|
390
391
|
|
|
392
|
+
def test_status_scan_axes():
|
|
393
|
+
tmp = tempfile.mkdtemp(prefix="dsh-labscript-scan-axes-")
|
|
394
|
+
try:
|
|
395
|
+
cfg = make_config(tmp)
|
|
396
|
+
bridge.cmd_set(cfg, {"values": {"duration_test_us": [0.0, 0.5, 1.0]}})
|
|
397
|
+
out = bridge.cmd_status(cfg, {})
|
|
398
|
+
assert out["scan_axes"].get("duration_test_us") == 3, out["scan_axes"]
|
|
399
|
+
assert out["n_shots_from_arrays"] == 3, out["n_shots_from_arrays"]
|
|
400
|
+
assert out["n_shots"] == 3, out["n_shots"]
|
|
401
|
+
partial = bridge.cmd_status(cfg, {"keys": ["amplitude"]})
|
|
402
|
+
assert "duration_test_us" not in partial["globals"], partial["globals"]
|
|
403
|
+
assert partial["scan_axes"].get("duration_test_us") == 3, partial["scan_axes"]
|
|
404
|
+
finally:
|
|
405
|
+
shutil.rmtree(tmp, ignore_errors=True)
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def test_data_keys_and_scan_summary():
|
|
409
|
+
tmp = tempfile.mkdtemp(prefix="dsh-labscript-data-keys-")
|
|
410
|
+
try:
|
|
411
|
+
cfg = make_config(tmp)
|
|
412
|
+
started = bridge.cmd_protocol_start(cfg, {"protocol": protocol_raw()})
|
|
413
|
+
run_folder = started["engage"]["run_folder"]
|
|
414
|
+
one = bridge.cmd_data(cfg, {"run_folder": run_folder, "keys": ["amplitude"]})
|
|
415
|
+
assert list(one["globals"].keys()) == ["amplitude"], one["globals"]
|
|
416
|
+
absent = bridge.cmd_data(cfg, {"run_folder": run_folder, "keys": ["nope"]})
|
|
417
|
+
assert absent["globals"] == {}, absent["globals"]
|
|
418
|
+
full = bridge.cmd_data(cfg, {"run_folder": run_folder, "keys": ["*"]})
|
|
419
|
+
assert len(full["globals"]) > 1, full["globals"]
|
|
420
|
+
summary = bridge.cmd_data(cfg, {"run_folder": run_folder, "scan": "duration_test_us"})
|
|
421
|
+
table = summary["scan_table"]
|
|
422
|
+
assert table["n_points"] == 6, table
|
|
423
|
+
assert table["axis"]["points"] == 3, table["axis"]
|
|
424
|
+
assert table["axis"]["min"] == 0.0 and table["axis"]["max"] == 2.0, table["axis"]
|
|
425
|
+
assert table["axis"]["step"] == 1.0, table["axis"]
|
|
426
|
+
assert "shot" not in table["columns"] and "duration_test_us" not in table["columns"], table["columns"]
|
|
427
|
+
assert "rows" not in table, list(table.keys())
|
|
428
|
+
limited = bridge.cmd_data(cfg, {"run_folder": run_folder, "scan": "duration_test_us", "limit": 2})
|
|
429
|
+
assert len(limited["scan_table"]["rows"]) == 2, limited["scan_table"]
|
|
430
|
+
assert limited["scan_table"]["rows"][0]["shot"], limited["scan_table"]["rows"][0]
|
|
431
|
+
finally:
|
|
432
|
+
shutil.rmtree(tmp, ignore_errors=True)
|
|
433
|
+
|
|
434
|
+
|
|
391
435
|
def main():
|
|
392
436
|
tests = [value for name, value in sorted(globals().items()) if name.startswith("test_")]
|
|
393
437
|
failures = 0
|