dsh-labscript 0.2.4 → 0.2.5

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,21 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.5
4
+
5
+ - labs_status can list every panel global: keys=["*"] prints all of them, sorted
6
+ (raw=true prints the raw expressions). This makes leftover array globals visible,
7
+ which is what makes labs_protocol refuse to run with N_SHOTS_MISMATCH.
8
+ - labs_status reports scan axes: the array-valued globals with their lengths and
9
+ n_shots_from_arrays (their product), and flags when the panel n_shots differs from
10
+ that product.
11
+ - labs_wait fails immediately when an explicit run folder does not exist and is not
12
+ this session's pending run (it used to wait for the full timeout), and the result
13
+ shows whether the folder exists.
14
+ - labs_data description: run_folder defaults to the pending run, else the last run
15
+ submitted through this plugin (persisted across restarts).
16
+ - cmd_progress returns folder_exists.
17
+ - Tests: scan axes, all-globals rendering, wait fast-fail, progress folder_exists.
18
+
3
19
  ## 0.2.4
4
20
 
5
21
  - 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, 36 tests (fake bridge, no python needed)
129
- python python/tests/run_tests.py # 18 tests (needs numpy + h5py)
128
+ npm test # node --test, 38 tests (fake bridge, no python needed)
129
+ python python/tests/run_tests.py # 19 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,16 @@ 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
+
62
77
  async function pendingProgress(runtime, exec) {
63
78
  const pending = runtime.state.pending
64
79
  if (!pending) return null
@@ -129,11 +144,11 @@ export function registerTools(ctx, runtime) {
129
144
 
130
145
  ctx.tools.register({
131
146
  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>. 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.',
147
+ 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
148
  parameters: {
134
149
  type: 'object',
135
150
  properties: {
136
- keys: { type: 'array', items: { type: 'string' }, description: 'Only return these panel globals' },
151
+ keys: { type: 'array', items: { type: 'string' }, description: 'Only return these panel globals; ["*"] returns every global' },
137
152
  raw: { type: 'boolean', description: 'Also return raw (expression) values' },
138
153
  },
139
154
  additionalProperties: false,
@@ -146,11 +161,14 @@ export function registerTools(ctx, runtime) {
146
161
  'n_shots: ' + value.n_shots + ' error_in_globals: ' + value.error_in_globals + ' run_shots: ' + value.run_shots,
147
162
  progressLine(value.last_run),
148
163
  'panel history: ' + (value.panel_history ? value.panel_history.dir + ' (' + value.panel_history.count + ' snapshots)' : 'n/a'),
164
+ scanLine(value),
149
165
  ...globalLines(args, value),
150
166
  ]))),
151
167
  async execute(args, exec) {
168
+ const keys = Array.isArray(args.keys) ? args.keys : []
169
+ const all = keys.length === 1 && keys[0] === '*'
152
170
  const data = await runtime.runBridge('status', {
153
- keys: Array.isArray(args.keys) ? args.keys : undefined,
171
+ keys: all || keys.length === 0 ? undefined : keys,
154
172
  raw: args.raw === true,
155
173
  }, { signal: exec.signal })
156
174
  if (runtime.state.pending) data.last_run = await pendingProgress(runtime, exec)
@@ -230,7 +248,7 @@ export function registerTools(ctx, runtime) {
230
248
 
231
249
  ctx.tools.register({
232
250
  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.',
251
+ 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
252
  parameters: {
235
253
  type: 'object',
236
254
  properties: {
@@ -244,6 +262,7 @@ export function registerTools(ctx, runtime) {
244
262
  output: looseOutput((_args, value) => text(lines([
245
263
  'run: ' + (value.mapped_run_folder || value.run_folder),
246
264
  value.aborted ? 'wait cancelled by labs_abort (run aborted)' : '',
265
+ value.folder_exists === false ? 'folder: missing' : '',
247
266
  'shots: ' + value.n_h5 + '/' + (value.expected_n_shots || '?') + ' complete: ' + value.complete + (value.timed_out ? ' TIMED OUT' : '') + (value.diagnosis ? ' diagnosis: ' + value.diagnosis : ''),
248
267
  value.hint ? 'hint: ' + value.hint : '',
249
268
  value.finalize && value.finalize.postprocess ? 'postprocess: ' + JSON.stringify(value.finalize.postprocess).slice(0, 400) : '',
@@ -256,6 +275,20 @@ export function registerTools(ctx, runtime) {
256
275
  const runFolder = args.run_folder || (pending ? pending.run_folder : '')
257
276
  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
277
  const expected = pending && pending.run_folder === runFolder ? pending.expected_n_shots : undefined
278
+ if (!(pending && pending.run_folder === runFolder)) {
279
+ let probe = null
280
+ try {
281
+ probe = await runtime.runBridge('progress', {
282
+ run_folder: runFolder,
283
+ expected_n_shots: expected,
284
+ }, { signal: exec.signal })
285
+ } catch (error) {
286
+ probe = null
287
+ }
288
+ if (probe && probe.folder_exists === false) {
289
+ 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.')
290
+ }
291
+ }
259
292
  const timeoutS = Number(args.timeout_s || runtime.config.engage.timeoutS || 900)
260
293
  let waited
261
294
  try {
@@ -339,7 +372,7 @@ export function registerTools(ctx, runtime) {
339
372
 
340
373
  ctx.tools.register({
341
374
  name: 'labs_data',
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.',
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.',
343
376
  parameters: {
344
377
  type: 'object',
345
378
  properties: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-labscript",
3
- "version": "0.2.4",
3
+ "version": "0.2.5",
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
- values = backend.get_globals(raw=False)
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, values[k]) for k in keys if k in values)
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
 
@@ -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,22 @@ 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
+
391
408
  def main():
392
409
  tests = [value for name, value in sorted(globals().items()) if name.startswith("test_")]
393
410
  failures = 0