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 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, 38 tests (fake bridge, no python needed)
129
- python python/tests/run_tests.py # 19 tests (needs numpy + h5py)
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 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 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.',
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 a per-shot table' },
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((_args, value) => text(lines([
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.scan_table ? 'scan ' + value.scan_table.scan + ': ' + value.scan_table.n_points + ' shots' : '',
391
- value.globals ? 'globals: ' + Object.keys(value.globals).length + ' keys' : '',
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.5",
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
- out["globals"] = jsonable(globals_attrs)
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
- out["scan_table"] = {"scan": scan, "n_points": len(rows), "rows": rows}
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