labcode 0.1.8__tar.gz → 0.2.0__tar.gz

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.
Files changed (89) hide show
  1. {labcode-0.1.8 → labcode-0.2.0}/.gitignore +3 -0
  2. {labcode-0.1.8 → labcode-0.2.0}/PKG-INFO +58 -7
  3. {labcode-0.1.8 → labcode-0.2.0}/README.md +55 -4
  4. {labcode-0.1.8 → labcode-0.2.0}/examples/README.md +116 -8
  5. {labcode-0.1.8 → labcode-0.2.0}/examples/plate_line.env.yaml +0 -3
  6. labcode-0.2.0/examples/preflight_sila2_env.py +369 -0
  7. {labcode-0.1.8 → labcode-0.2.0}/examples/run_all_sila2_examples.py +8 -4
  8. labcode-0.2.0/examples/run_sila2_plate_cycle_no_atc.py +398 -0
  9. {labcode-0.1.8 → labcode-0.2.0}/examples/sila2_plate_cycle.wrapped.env.yaml +0 -3
  10. labcode-0.2.0/examples/sila2_plate_cycle_no_atc.boundary.yaml +22 -0
  11. labcode-0.2.0/examples/sila2_plate_cycle_no_atc.workflow.yaml +115 -0
  12. labcode-0.2.0/examples/sila2_plate_cycle_no_atc.wrapped.env.yaml +389 -0
  13. {labcode-0.1.8 → labcode-0.2.0}/examples/sila2_seal.env.yaml +0 -3
  14. {labcode-0.1.8 → labcode-0.2.0}/examples/sila2_seal.wrapped.env.yaml +0 -3
  15. {labcode-0.1.8 → labcode-0.2.0}/labcode/_child.py +11 -4
  16. {labcode-0.1.8 → labcode-0.2.0}/labcode/backend.py +69 -0
  17. {labcode-0.1.8 → labcode-0.2.0}/labcode/dialect.py +62 -2
  18. {labcode-0.1.8 → labcode-0.2.0}/labcode/extension.py +12 -0
  19. {labcode-0.1.8 → labcode-0.2.0}/labcode/probe.py +4 -1
  20. {labcode-0.1.8 → labcode-0.2.0}/labcode/record.py +5 -0
  21. {labcode-0.1.8 → labcode-0.2.0}/labcode/run_cli.py +15 -0
  22. {labcode-0.1.8 → labcode-0.2.0}/labcode/runner.py +4 -0
  23. {labcode-0.1.8 → labcode-0.2.0}/labcode/sila2_instrument.py +3 -3
  24. {labcode-0.1.8 → labcode-0.2.0}/labcode.egg-info/PKG-INFO +58 -7
  25. {labcode-0.1.8 → labcode-0.2.0}/labcode.egg-info/SOURCES.txt +6 -0
  26. {labcode-0.1.8 → labcode-0.2.0}/labcode.egg-info/requires.txt +2 -2
  27. {labcode-0.1.8 → labcode-0.2.0}/labcode.egg-info/scm_file_list.json +6 -0
  28. labcode-0.2.0/labcode.egg-info/scm_version.json +8 -0
  29. {labcode-0.1.8 → labcode-0.2.0}/pyproject.toml +22 -9
  30. {labcode-0.1.8 → labcode-0.2.0}/tests/fixtures/device_script.env.yaml +0 -3
  31. labcode-0.2.0/tests/fixtures/replenishment.env.yaml +51 -0
  32. {labcode-0.1.8 → labcode-0.2.0}/tests/fixtures/reroute_device.env.yaml +0 -3
  33. {labcode-0.1.8 → labcode-0.2.0}/tests/fixtures/reroute_transporter.env.yaml +0 -3
  34. {labcode-0.1.8 → labcode-0.2.0}/tests/fixtures/transport.env.yaml +0 -3
  35. {labcode-0.1.8 → labcode-0.2.0}/tests/test_backend.py +87 -4
  36. {labcode-0.1.8 → labcode-0.2.0}/tests/test_dialect.py +93 -0
  37. {labcode-0.1.8 → labcode-0.2.0}/tests/test_recording.py +0 -1
  38. {labcode-0.1.8 → labcode-0.2.0}/tests/test_run_cli.py +40 -2
  39. labcode-0.1.8/labcode.egg-info/scm_version.json +0 -8
  40. {labcode-0.1.8 → labcode-0.2.0}/.github/workflows/ci.yml +0 -0
  41. {labcode-0.1.8 → labcode-0.2.0}/.github/workflows/publish.yml +0 -0
  42. {labcode-0.1.8 → labcode-0.2.0}/LICENSE +0 -0
  43. {labcode-0.1.8 → labcode-0.2.0}/MANIFEST.in +0 -0
  44. {labcode-0.1.8 → labcode-0.2.0}/SPECIFICATIONS.md +0 -0
  45. {labcode-0.1.8 → labcode-0.2.0}/examples/outputs/plate_line.boundary.yaml +0 -0
  46. {labcode-0.1.8 → labcode-0.2.0}/examples/outputs/plate_line.observation.yaml +0 -0
  47. {labcode-0.1.8 → labcode-0.2.0}/examples/outputs/plate_line.plan.yaml +0 -0
  48. {labcode-0.1.8 → labcode-0.2.0}/examples/outputs/plate_line.svg +0 -0
  49. {labcode-0.1.8 → labcode-0.2.0}/examples/outputs/sila2_plate_cycle.boundary.yaml +0 -0
  50. {labcode-0.1.8 → labcode-0.2.0}/examples/outputs/sila2_plate_cycle.observation.yaml +0 -0
  51. {labcode-0.1.8 → labcode-0.2.0}/examples/outputs/sila2_plate_cycle.plan.yaml +0 -0
  52. {labcode-0.1.8 → labcode-0.2.0}/examples/outputs/sila2_plate_cycle.svg +0 -0
  53. {labcode-0.1.8 → labcode-0.2.0}/examples/plate_line.boundary.yaml +0 -0
  54. {labcode-0.1.8 → labcode-0.2.0}/examples/plate_line.workflow.yaml +0 -0
  55. {labcode-0.1.8 → labcode-0.2.0}/examples/render_plate_line.py +0 -0
  56. {labcode-0.1.8 → labcode-0.2.0}/examples/render_sila2_plate_cycle.py +0 -0
  57. {labcode-0.1.8 → labcode-0.2.0}/examples/run_sila2_plate_cycle.py +0 -0
  58. {labcode-0.1.8 → labcode-0.2.0}/examples/run_sila2_seal.py +0 -0
  59. {labcode-0.1.8 → labcode-0.2.0}/examples/sila2_plate_cycle.boundary.yaml +0 -0
  60. {labcode-0.1.8 → labcode-0.2.0}/examples/sila2_plate_cycle.workflow.yaml +0 -0
  61. {labcode-0.1.8 → labcode-0.2.0}/examples/sila2_seal.boundary.yaml +0 -0
  62. {labcode-0.1.8 → labcode-0.2.0}/examples/sila2_seal.workflow.yaml +0 -0
  63. {labcode-0.1.8 → labcode-0.2.0}/labcode/__init__.py +0 -0
  64. {labcode-0.1.8 → labcode-0.2.0}/labcode/__main__.py +0 -0
  65. {labcode-0.1.8 → labcode-0.2.0}/labcode/cli.py +0 -0
  66. {labcode-0.1.8 → labcode-0.2.0}/labcode/idgen.py +0 -0
  67. {labcode-0.1.8 → labcode-0.2.0}/labcode/objectid.py +0 -0
  68. {labcode-0.1.8 → labcode-0.2.0}/labcode/otel.py +0 -0
  69. {labcode-0.1.8 → labcode-0.2.0}/labcode/otel_sila2.py +0 -0
  70. {labcode-0.1.8 → labcode-0.2.0}/labcode/py.typed +0 -0
  71. {labcode-0.1.8 → labcode-0.2.0}/labcode/sila2.py +0 -0
  72. {labcode-0.1.8 → labcode-0.2.0}/labcode/sila2_commands.py +0 -0
  73. {labcode-0.1.8 → labcode-0.2.0}/labcode.egg-info/dependency_links.txt +0 -0
  74. {labcode-0.1.8 → labcode-0.2.0}/labcode.egg-info/entry_points.txt +0 -0
  75. {labcode-0.1.8 → labcode-0.2.0}/labcode.egg-info/top_level.txt +0 -0
  76. {labcode-0.1.8 → labcode-0.2.0}/setup.cfg +0 -0
  77. {labcode-0.1.8 → labcode-0.2.0}/tests/fixtures/device_script.workflow.yaml +0 -0
  78. {labcode-0.1.8 → labcode-0.2.0}/tests/fixtures/transport.workflow.yaml +0 -0
  79. {labcode-0.1.8 → labcode-0.2.0}/tests/test_cli.py +0 -0
  80. {labcode-0.1.8 → labcode-0.2.0}/tests/test_objectid.py +0 -0
  81. {labcode-0.1.8 → labcode-0.2.0}/tests/test_otel.py +0 -0
  82. {labcode-0.1.8 → labcode-0.2.0}/tests/test_otel_child.py +0 -0
  83. {labcode-0.1.8 → labcode-0.2.0}/tests/test_otel_grpc.py +0 -0
  84. {labcode-0.1.8 → labcode-0.2.0}/tests/test_otel_sila2.py +0 -0
  85. {labcode-0.1.8 → labcode-0.2.0}/tests/test_probe.py +0 -0
  86. {labcode-0.1.8 → labcode-0.2.0}/tests/test_record.py +0 -0
  87. {labcode-0.1.8 → labcode-0.2.0}/tests/test_sila2.py +0 -0
  88. {labcode-0.1.8 → labcode-0.2.0}/tests/test_sila2_commands.py +0 -0
  89. {labcode-0.1.8 → labcode-0.2.0}/tests/test_sila2_instrument.py +0 -0
@@ -12,3 +12,6 @@ venv/
12
12
  # Local working notes, not part of the published project (until explicitly committed).
13
13
  CLAUDE.md
14
14
  dev-notes/
15
+
16
+ # The bench-specific environments name real hosts and ports; this repository is public.
17
+ *.remote.env.yaml
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: labcode
3
- Version: 0.1.8
3
+ Version: 0.2.0
4
4
  Summary: labcode -- a dialect wrapper over the Object-Flow Programming Language toolchain
5
5
  Author-email: Kazunari Kaizu <kwaizu@gmail.com>
6
6
  License-Expression: MIT
@@ -21,8 +21,8 @@ Requires-Python: >=3.10
21
21
  Description-Content-Type: text/markdown
22
22
  License-File: LICENSE
23
23
  Requires-Dist: ofplang-validate>=0.1.3
24
- Requires-Dist: ofplang-schedule>=0.1.3
25
- Requires-Dist: ofplang-run>=0.1.13
24
+ Requires-Dist: ofplang-schedule>=0.2.3
25
+ Requires-Dist: ofplang-run>=0.3.0
26
26
  Provides-Extra: test
27
27
  Requires-Dist: pytest>=7.0; extra == "test"
28
28
  Requires-Dist: opentelemetry-sdk>=1.20; extra == "test"
@@ -114,8 +114,8 @@ What `lc run` brings of its own, beyond dispatching:
114
114
  installed into whichever interpreter runs the scripts.
115
115
  - **recording a run** — with `--trace`, what the run did is recorded as OpenTelemetry
116
116
  traces: one trace per run, a span per operation, and — measured inside the process that
117
- issued them — a span per SiLA2 connection, per command, and per gRPC call underneath
118
- either. Off by default; the extra is `pip install labcode[otel]`.
117
+ issued them — a span per SiLA2 connection, per command, and per gRPC call each of those
118
+ made. Off by default; the extra is `pip install labcode[otel]`.
119
119
 
120
120
  ## Usage
121
121
 
@@ -139,7 +139,7 @@ described below.
139
139
  lc run <workflow> --env <env>
140
140
  [--boundary DOC] [-o OUT] [--boundary-out FILE] [--observation-out FILE]
141
141
  [--seconds-per-tick S] [--op-timeout S | --no-op-timeout] [--no-probe]
142
- [--trace] [--mission-id ID] [--object-ids seeded|real]
142
+ [--ignore-resources] [--trace] [--mission-id ID] [--object-ids seeded|real]
143
143
  ```
144
144
 
145
145
  - `<workflow>` — the portable v0 workflow: *what* happens.
@@ -149,7 +149,12 @@ lc run <workflow> --env <env>
149
149
  `{spot, view}` descriptor per entry input / final output port. `spot` says where a
150
150
  boundary Object sits; `view` supplies an input's value. A workflow with Object-bearing
151
151
  entry inputs needs one, since each must be placed on a spot and only the operator knows
152
- where the labware is.
152
+ where the labware is. Where a device declares a consumable and some mode draws on it,
153
+ an `inventories: {levels: ...}` section says what each stock holds **at the start of
154
+ the run** — the level later on is never stated, it is worked out from that and what the
155
+ run has done since. It is not echoed into `--boundary-out`, because that document is
156
+ written to be fed back and the next run would take this run's opening stock for its
157
+ own.
153
158
  - `-o OUT` — write the final execution status (spec §6/§7) here; the default is stdout.
154
159
  - `--boundary-out FILE` — write the result boundary: the same schema as `--boundary`, with
155
160
  each produced output's `view` filled in, including the `_id` its Object was minted with
@@ -167,6 +172,9 @@ lc run <workflow> --env <env>
167
172
  else 7200 real seconds. The two forms exclude each other.
168
173
  - `--no-probe` — ignore the environment's `x-labcode.probe` policies and treat every
169
174
  machine as reachable (§1.5). The documents are still validated.
175
+ - `--ignore-resources` — switch the consumable model off. The environment's resource
176
+ declarations are still checked for shape but none is applied, so a bench whose devices
177
+ declare stocks nobody is tracking runs without the boundary saying what they held.
170
178
  - `--trace` — record what the run did (see below). Off by default.
171
179
  - `--mission-id ID` — the campaign this run belongs to. Recorded with the run and given no
172
180
  meaning by labcode: several runs may share one, and nothing here reads it back.
@@ -190,6 +198,49 @@ The remaining options tune the replan loop rather than describe the run —
190
198
  `--poll-interval`, `--margin`, `--seed`, `--speed`, `--max-ticks`, `--no-validate` — and
191
199
  are covered by `lc run --help`.
192
200
 
201
+ ### Refilling a stock
202
+
203
+ Where a device declares a consumable and the environment says a replenisher can reach it,
204
+ a stock that would run out is **topped up instead of ending the run**. The procedure goes
205
+ on the `replenishments[]` route — the pair is what has a procedure, while the machine has
206
+ only an address, the same division `transporters` and `transports` have:
207
+
208
+ ```yaml
209
+ replenishers:
210
+ - id: dispenser
211
+ x-labcode:
212
+ connection: { kind: sila2, host: 10.0.0.9, port: 50055, insecure: true }
213
+
214
+ replenishments:
215
+ - replenisher: dispenser
216
+ device: reader
217
+ duration: 4 # ticks: the scheduler's estimate of the visit
218
+ x-labcode:
219
+ script:
220
+ language: python
221
+ code: |
222
+ import time
223
+ time.sleep(80) # real seconds: what the visit actually takes
224
+ ```
225
+
226
+ The script is handed `replenisher`, `device` and the `amounts` the scheduler derived, and
227
+ is expected to put that in. It is **not** handed the duration: a real refill takes as long
228
+ as it takes, so a stand-in says so in its own code — which is why the two numbers above
229
+ are written separately. Like a transport script it returns nothing; it acts.
230
+
231
+ A route with no script runs as a timed visit: both machines are held for the declared
232
+ duration and nothing is commanded. That is a real thing to write (an operator tops the
233
+ stock up while the schedule waits for them) and an easy one to write by accident, so it is
234
+ warned about.
235
+
236
+ `flavor: sila2` is **refused on a refill route** for now: a sila2 script is handed clients,
237
+ and which machine's clients a refill should receive — the replenisher's, or both ends' as a
238
+ transport may ask for — is not settled. Use `python`.
239
+
240
+ A refill holds the device it fills *and* the replenisher filling it, so it never overlaps
241
+ the work it feeds. It is recorded (`--trace`) as a `replenishment` span naming both
242
+ machines.
243
+
193
244
  ### Recording a run
194
245
 
195
246
  `--trace` records what the run did as OpenTelemetry traces. It needs the extra, in the
@@ -70,8 +70,8 @@ What `lc run` brings of its own, beyond dispatching:
70
70
  installed into whichever interpreter runs the scripts.
71
71
  - **recording a run** — with `--trace`, what the run did is recorded as OpenTelemetry
72
72
  traces: one trace per run, a span per operation, and — measured inside the process that
73
- issued them — a span per SiLA2 connection, per command, and per gRPC call underneath
74
- either. Off by default; the extra is `pip install labcode[otel]`.
73
+ issued them — a span per SiLA2 connection, per command, and per gRPC call each of those
74
+ made. Off by default; the extra is `pip install labcode[otel]`.
75
75
 
76
76
  ## Usage
77
77
 
@@ -95,7 +95,7 @@ described below.
95
95
  lc run <workflow> --env <env>
96
96
  [--boundary DOC] [-o OUT] [--boundary-out FILE] [--observation-out FILE]
97
97
  [--seconds-per-tick S] [--op-timeout S | --no-op-timeout] [--no-probe]
98
- [--trace] [--mission-id ID] [--object-ids seeded|real]
98
+ [--ignore-resources] [--trace] [--mission-id ID] [--object-ids seeded|real]
99
99
  ```
100
100
 
101
101
  - `<workflow>` — the portable v0 workflow: *what* happens.
@@ -105,7 +105,12 @@ lc run <workflow> --env <env>
105
105
  `{spot, view}` descriptor per entry input / final output port. `spot` says where a
106
106
  boundary Object sits; `view` supplies an input's value. A workflow with Object-bearing
107
107
  entry inputs needs one, since each must be placed on a spot and only the operator knows
108
- where the labware is.
108
+ where the labware is. Where a device declares a consumable and some mode draws on it,
109
+ an `inventories: {levels: ...}` section says what each stock holds **at the start of
110
+ the run** — the level later on is never stated, it is worked out from that and what the
111
+ run has done since. It is not echoed into `--boundary-out`, because that document is
112
+ written to be fed back and the next run would take this run's opening stock for its
113
+ own.
109
114
  - `-o OUT` — write the final execution status (spec §6/§7) here; the default is stdout.
110
115
  - `--boundary-out FILE` — write the result boundary: the same schema as `--boundary`, with
111
116
  each produced output's `view` filled in, including the `_id` its Object was minted with
@@ -123,6 +128,9 @@ lc run <workflow> --env <env>
123
128
  else 7200 real seconds. The two forms exclude each other.
124
129
  - `--no-probe` — ignore the environment's `x-labcode.probe` policies and treat every
125
130
  machine as reachable (§1.5). The documents are still validated.
131
+ - `--ignore-resources` — switch the consumable model off. The environment's resource
132
+ declarations are still checked for shape but none is applied, so a bench whose devices
133
+ declare stocks nobody is tracking runs without the boundary saying what they held.
126
134
  - `--trace` — record what the run did (see below). Off by default.
127
135
  - `--mission-id ID` — the campaign this run belongs to. Recorded with the run and given no
128
136
  meaning by labcode: several runs may share one, and nothing here reads it back.
@@ -146,6 +154,49 @@ The remaining options tune the replan loop rather than describe the run —
146
154
  `--poll-interval`, `--margin`, `--seed`, `--speed`, `--max-ticks`, `--no-validate` — and
147
155
  are covered by `lc run --help`.
148
156
 
157
+ ### Refilling a stock
158
+
159
+ Where a device declares a consumable and the environment says a replenisher can reach it,
160
+ a stock that would run out is **topped up instead of ending the run**. The procedure goes
161
+ on the `replenishments[]` route — the pair is what has a procedure, while the machine has
162
+ only an address, the same division `transporters` and `transports` have:
163
+
164
+ ```yaml
165
+ replenishers:
166
+ - id: dispenser
167
+ x-labcode:
168
+ connection: { kind: sila2, host: 10.0.0.9, port: 50055, insecure: true }
169
+
170
+ replenishments:
171
+ - replenisher: dispenser
172
+ device: reader
173
+ duration: 4 # ticks: the scheduler's estimate of the visit
174
+ x-labcode:
175
+ script:
176
+ language: python
177
+ code: |
178
+ import time
179
+ time.sleep(80) # real seconds: what the visit actually takes
180
+ ```
181
+
182
+ The script is handed `replenisher`, `device` and the `amounts` the scheduler derived, and
183
+ is expected to put that in. It is **not** handed the duration: a real refill takes as long
184
+ as it takes, so a stand-in says so in its own code — which is why the two numbers above
185
+ are written separately. Like a transport script it returns nothing; it acts.
186
+
187
+ A route with no script runs as a timed visit: both machines are held for the declared
188
+ duration and nothing is commanded. That is a real thing to write (an operator tops the
189
+ stock up while the schedule waits for them) and an easy one to write by accident, so it is
190
+ warned about.
191
+
192
+ `flavor: sila2` is **refused on a refill route** for now: a sila2 script is handed clients,
193
+ and which machine's clients a refill should receive — the replenisher's, or both ends' as a
194
+ transport may ask for — is not settled. Use `python`.
195
+
196
+ A refill holds the device it fills *and* the replenisher filling it, so it never overlaps
197
+ the work it feeds. It is recorded (`--trace`) as a `replenishment` span naming both
198
+ machines.
199
+
149
200
  ### Recording a run
150
201
 
151
202
  `--trace` records what the run did as OpenTelemetry traces. It needs the extra, in the
@@ -185,11 +185,11 @@ polls forever. A script that knows its own commands should still bound them itse
185
185
 
186
186
  ### Prerequisites
187
187
 
188
- **Verified against [ofplang-sila2-backend](https://github.com/kaizu/sila2-demo) branch `ardea`
189
- (commit `de3c4fd`)** — a virtual lab of mock SiLA2 instrument servers. That branch is where its
188
+ **Verified against [ofplang-sila2-backend](https://github.com/ofplang/sila2-backend) at commit
189
+ `de3c4fd`** — a virtual lab of mock SiLA2 instrument servers. That commit is where its
190
190
  transporter became a mock of **Ardea**, a machine that exists, serving the real one's nine
191
- Feature definitions unchanged; it is not merged, so a checkout of `main` will not run these
192
- examples. That lab is a *reference, not a
191
+ Feature definitions unchanged; it reached `main` in the `ardea` merge, so a checkout of `main`
192
+ runs these examples. That lab is a *reference, not a
193
193
  requirement*: the scripts speak plain SiLA2, so pointing them at real instruments is a matter
194
194
  of changing the host and port in the environment. The version is recorded so a run without
195
195
  hardware has something known to reproduce against; it is deliberately not asserted on, since
@@ -417,14 +417,122 @@ scripts issue real commands. The committed copies were produced on the realistic
417
417
  times vary between runs — by tens of seconds, now that five 30 s transfers dominate — while the
418
418
  sequence, the identities and the produced values do not.
419
419
 
420
+ ## `sila2_plate_cycle_no_atc` — the same circuit, on a bench with no thermal cycler
421
+
422
+ The full circuit above needs an automated thermal cycler. When there is not one to use, the
423
+ workflow that goes to the bench is this one — `sila2_plate_cycle` with that step removed and the
424
+ rest in the order a bench with an **unsealed** plate can run them:
425
+
426
+ ```
427
+ (in) plate ──▶ seal ──▶ rotate ──▶ peel ──▶ (out) plate
428
+ │ │
429
+ (out) cycle_count (out) tape_left
430
+ ```
431
+
432
+ Seal, spin, peel — so the plate ends the circuit in the state it began it, and the run repeats
433
+ without anyone reconditioning a plate in between. That is the same property the round trip has
434
+ for the plate's *location*, applied to its condition; peeling first would need a sealed plate to
435
+ start from and hand back a sealed one.
436
+
437
+ - [`sila2_plate_cycle_no_atc.workflow.yaml`](sila2_plate_cycle_no_atc.workflow.yaml) is the
438
+ circuit minus one node, with the remaining three in the new order: the plate is sealed, spun
439
+ down and unsealed, and the *same* plate comes back to `station.slot1`.
440
+ - [`sila2_plate_cycle_no_atc.wrapped.env.yaml`](sila2_plate_cycle_no_atc.wrapped.env.yaml)
441
+ drops the `thermal_cycler` device, its routes and the `thermal_cycle` mode, and joins four
442
+ different pairs of places: to the sealer, to the centrifuge, to the peeler, home. Only the
443
+ centrifuge has a door, so only the two routes at its ends open anything. Every instrument
444
+ script and every duration is the full circuit's, unchanged.
445
+ - [`sila2_plate_cycle_no_atc.boundary.yaml`](sila2_plate_cycle_no_atc.boundary.yaml) is the
446
+ full circuit's boundary without `elapsed_time` — that reading was the cycler's.
447
+
448
+ Nothing connects to the cycler, so the run does not care whether that server is up: the check
449
+ asserts it was never scheduled, which is what makes a pass here evidence for the bench it was
450
+ written for rather than for the full lab.
451
+
452
+ ```sh
453
+ python examples/run_sila2_plate_cycle_no_atc.py
454
+ ```
455
+
456
+ Same prerequisites and conventions as `sila2_plate_cycle` (the lab up, `sila2` importable, one
457
+ plate on `station.slot1`, exit code 0 means every check passed, `--artifacts DIR` keeps the
458
+ documents). It checks the same things minus the cycler's reading: every activity completed,
459
+ four transports and three instrument steps, both readings real, the plate home at
460
+ `station.slot1`, and all three instruments handling the plate with the same `_id`.
461
+
462
+ Verified against the reference lab on both timing profiles at `--seconds-per-tick 1.0`. On
463
+ `command_durations.realistic.yaml` the circuit is a makespan of about 190–220 (two ops fewer
464
+ than the full one's 260–290); on the default profile every op finishes early and the run still
465
+ completes. Running it twice in a row is the more interesting pass: the second run starts with
466
+ the centrifuge closed by the first, so it only gets anywhere if the transport really can open
467
+ the door.
468
+
469
+ `--trace` records the run (`labcode.record`) and prints its trace id, so the SiLA2 commands, the
470
+ client builds and the poll waits *inside* each operation are separable — which is what to look
471
+ at when a duration turns out to be wrong. The exporter is configured by OpenTelemetry's own
472
+ environment variables:
473
+
474
+ ```sh
475
+ OTEL_EXPORTER_OTLP_ENDPOINT=http://127.0.0.1:4318 \
476
+ python examples/run_sila2_plate_cycle_no_atc.py --trace --artifacts run1
477
+ ```
478
+
479
+ One thing to get right first: **point the exporter at `127.0.0.1`, not `localhost`.** Where the
480
+ collector binds IPv4 only, `localhost` can resolve to `::1` first, and the exporter's retry
481
+ backoff then stalls whichever operation was exporting. Measured on the reference lab at
482
+ `--seconds-per-tick 1.0`: makespan 216 untraced, **219** traced via `127.0.0.1` (so recording
483
+ itself costs next to nothing), **279** traced via `localhost` — one 39 s move became 95 s, and
484
+ the run reported a 54 s poll cycle. Both traced runs recorded the same 114 spans, so the trace
485
+ was not the poorer for it; only the schedule was.
486
+
487
+ Even so, take a duration from an *untraced* run and use the trace to explain it. Recording puts
488
+ an exporter in every operation's child process, and an operation's measured time is exactly what
489
+ these numbers are for.
490
+
420
491
  ### Run every SiLA2 example
421
492
 
422
493
  ```sh
423
494
  python examples/run_all_sila2_examples.py
424
495
  ```
425
496
 
426
- Runs each environment above in turn — both `sila2_seal` environments and `sila2_plate_cycle`
427
- (they are round trips that put the plate back where it started, and each opens whatever it
428
- needs open, so they follow one another without intervention) — prints a pass/fail summary, and
429
- exits non-zero if any failed. Only the examples
497
+ Runs each environment above in turn — both `sila2_seal` environments, `sila2_plate_cycle` and
498
+ `sila2_plate_cycle_no_atc` (they are round trips that put the plate back where it started, and
499
+ each opens whatever it needs open, so they follow one another without intervention) — prints a
500
+ pass/fail summary, and exits non-zero if any failed. Only the examples
430
501
  that need the lab are included; `render_plate_line.py` needs nothing but Python.
502
+
503
+ ## Taking one of these to a bench
504
+
505
+ The examples above point at the reference lab because it is what CI and a laptop can run. An
506
+ environment that speaks plain SiLA2 goes to real instruments by **changing hosts and ports and
507
+ nothing else** — so a bench environment is best written as a copy of the mock one with the
508
+ addresses replaced, and nothing else touched. Then `diff` says what is bench-specific.
509
+
510
+ `*.remote.env.yaml` is **git-ignored**: this repository is public, and a bench's host/port
511
+ inventory is not something to publish by accident. Keep the copy local, or commit it
512
+ deliberately.
513
+
514
+ ### Check the bench before commanding anything
515
+
516
+ [`preflight_sila2_env.py`](preflight_sila2_env.py) takes any labcode environment and checks it
517
+ against the machines it names, **issuing no SiLA2 command**:
518
+
519
+ ```sh
520
+ python examples/preflight_sila2_env.py --env examples/<your>.remote.env.yaml
521
+ ```
522
+
523
+ Everything it does is a read — building labcode's own client (which fetches every Feature
524
+ definition, and is where TLS, a wrong port and a half-open server show up), the `SiLAService`
525
+ properties every server serves, and the transporter's non-observable
526
+ `CarriageService.StationNames`. Nothing moves, nothing opens, nothing starts. It reports
527
+
528
+ - each machine's identity and the Features it serves;
529
+ - whether every Feature the environment's **scripts** name is actually served — a script
530
+ reaching for a Feature the server does not implement otherwise fails mid-run, with a plate
531
+ somewhere;
532
+ - whether every station name the transport scripts use is one the transporter **knows** — an
533
+ unknown one fails with `InvalidStation`.
534
+
535
+ What it cannot check is the one thing the hardware will not catch either: whether a station
536
+ name the machine *does* know is the place the workflow means. `Base4` that is the sealer on one
537
+ bench and the cycler on another does not fail — it puts a plate somewhere else. That mapping
538
+ has to be confirmed against the bench, and it is the thing to confirm before a first run.
@@ -141,6 +141,3 @@ processes:
141
141
  code: |
142
142
  # `plate` is the incoming Plate's view; storing is side-effect only.
143
143
  return {}
144
-
145
- objective:
146
- kind: makespan