dsh-labscript 0.2.5 → 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 +10 -0
- package/README.md +2 -2
- package/lib/tools.js +59 -5
- package/package.json +1 -1
- package/python/bridge.py +30 -2
- package/python/tests/run_tests.py +28 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
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
|
+
|
|
3
13
|
## 0.2.5
|
|
4
14
|
|
|
5
15
|
- labs_status can list every panel global: keys=["*"] prints all of them, sorted
|
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
|
@@ -74,6 +74,54 @@ function scanLine(value) {
|
|
|
74
74
|
return 'scan axes: ' + detail + ' n_shots_from_arrays=' + product + (mismatch ? ' (panel n_shots=' + value.n_shots + ' differs)' : '')
|
|
75
75
|
}
|
|
76
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
|
+
|
|
77
125
|
async function pendingProgress(runtime, exec) {
|
|
78
126
|
const pending = runtime.state.pending
|
|
79
127
|
if (!pending) return null
|
|
@@ -372,23 +420,27 @@ export function registerTools(ctx, runtime) {
|
|
|
372
420
|
|
|
373
421
|
ctx.tools.register({
|
|
374
422
|
name: 'labs_data',
|
|
375
|
-
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.',
|
|
376
424
|
parameters: {
|
|
377
425
|
type: 'object',
|
|
378
426
|
properties: {
|
|
379
427
|
run_folder: { type: 'string', description: 'Run folder; defaults to the run submitted from this DSH session (pass a historical run folder explicitly)' },
|
|
380
|
-
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)' },
|
|
381
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)' },
|
|
382
432
|
results: { type: 'boolean', description: 'Include the results layout' },
|
|
383
433
|
},
|
|
384
434
|
additionalProperties: false,
|
|
385
435
|
},
|
|
386
436
|
isConcurrencySafe: () => true,
|
|
387
|
-
output: looseOutput((
|
|
437
|
+
output: looseOutput((args, value) => text(lines([
|
|
388
438
|
'run: ' + (value.mapped_run_folder || value.run_folder),
|
|
389
439
|
'h5: ' + value.n_h5 + (value.empty ? ' (folder exists, no shots yet)' : ''),
|
|
390
|
-
value
|
|
391
|
-
|
|
440
|
+
resultsLine(value),
|
|
441
|
+
...dataGlobalsLines(args, value),
|
|
442
|
+
scanSummaryLine(value),
|
|
443
|
+
...scanRowLines(value),
|
|
392
444
|
]))),
|
|
393
445
|
async execute(args, exec) {
|
|
394
446
|
const runFolder = args.run_folder || (runtime.state.pending ? runtime.state.pending.run_folder : '') || (runtime.state.lastRun ? runtime.state.lastRun.run_folder : '')
|
|
@@ -396,6 +448,8 @@ export function registerTools(ctx, runtime) {
|
|
|
396
448
|
return runtime.runBridge('data', {
|
|
397
449
|
run_folder: runFolder,
|
|
398
450
|
scan: args.scan,
|
|
451
|
+
keys: Array.isArray(args.keys) ? args.keys : undefined,
|
|
452
|
+
limit: typeof args.limit === 'number' ? args.limit : undefined,
|
|
399
453
|
globals: args.globals !== false,
|
|
400
454
|
results: args.results === true,
|
|
401
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
|
@@ -1213,7 +1213,12 @@ def cmd_data(config, args):
|
|
|
1213
1213
|
with h5py.File(shots[0], "r", locking=False) as handle:
|
|
1214
1214
|
globals_attrs = dict(handle["globals"].attrs) if "globals" in handle else {}
|
|
1215
1215
|
if args.get("globals", True):
|
|
1216
|
-
|
|
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
|
|
1217
1222
|
if args.get("results"):
|
|
1218
1223
|
layout = {}
|
|
1219
1224
|
if "results" in handle:
|
|
@@ -1252,7 +1257,30 @@ def cmd_data(config, args):
|
|
|
1252
1257
|
if number is not None:
|
|
1253
1258
|
row[group_name + "." + key] = number
|
|
1254
1259
|
rows.append(row)
|
|
1255
|
-
|
|
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
|
|
1256
1284
|
return out
|
|
1257
1285
|
|
|
1258
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
|
|
@@ -405,6 +405,33 @@ def test_status_scan_axes():
|
|
|
405
405
|
shutil.rmtree(tmp, ignore_errors=True)
|
|
406
406
|
|
|
407
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
|
+
|
|
408
435
|
def main():
|
|
409
436
|
tests = [value for name, value in sorted(globals().items()) if name.startswith("test_")]
|
|
410
437
|
failures = 0
|