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.
- {labcode-0.1.8 → labcode-0.2.0}/.gitignore +3 -0
- {labcode-0.1.8 → labcode-0.2.0}/PKG-INFO +58 -7
- {labcode-0.1.8 → labcode-0.2.0}/README.md +55 -4
- {labcode-0.1.8 → labcode-0.2.0}/examples/README.md +116 -8
- {labcode-0.1.8 → labcode-0.2.0}/examples/plate_line.env.yaml +0 -3
- labcode-0.2.0/examples/preflight_sila2_env.py +369 -0
- {labcode-0.1.8 → labcode-0.2.0}/examples/run_all_sila2_examples.py +8 -4
- labcode-0.2.0/examples/run_sila2_plate_cycle_no_atc.py +398 -0
- {labcode-0.1.8 → labcode-0.2.0}/examples/sila2_plate_cycle.wrapped.env.yaml +0 -3
- labcode-0.2.0/examples/sila2_plate_cycle_no_atc.boundary.yaml +22 -0
- labcode-0.2.0/examples/sila2_plate_cycle_no_atc.workflow.yaml +115 -0
- labcode-0.2.0/examples/sila2_plate_cycle_no_atc.wrapped.env.yaml +389 -0
- {labcode-0.1.8 → labcode-0.2.0}/examples/sila2_seal.env.yaml +0 -3
- {labcode-0.1.8 → labcode-0.2.0}/examples/sila2_seal.wrapped.env.yaml +0 -3
- {labcode-0.1.8 → labcode-0.2.0}/labcode/_child.py +11 -4
- {labcode-0.1.8 → labcode-0.2.0}/labcode/backend.py +69 -0
- {labcode-0.1.8 → labcode-0.2.0}/labcode/dialect.py +62 -2
- {labcode-0.1.8 → labcode-0.2.0}/labcode/extension.py +12 -0
- {labcode-0.1.8 → labcode-0.2.0}/labcode/probe.py +4 -1
- {labcode-0.1.8 → labcode-0.2.0}/labcode/record.py +5 -0
- {labcode-0.1.8 → labcode-0.2.0}/labcode/run_cli.py +15 -0
- {labcode-0.1.8 → labcode-0.2.0}/labcode/runner.py +4 -0
- {labcode-0.1.8 → labcode-0.2.0}/labcode/sila2_instrument.py +3 -3
- {labcode-0.1.8 → labcode-0.2.0}/labcode.egg-info/PKG-INFO +58 -7
- {labcode-0.1.8 → labcode-0.2.0}/labcode.egg-info/SOURCES.txt +6 -0
- {labcode-0.1.8 → labcode-0.2.0}/labcode.egg-info/requires.txt +2 -2
- {labcode-0.1.8 → labcode-0.2.0}/labcode.egg-info/scm_file_list.json +6 -0
- labcode-0.2.0/labcode.egg-info/scm_version.json +8 -0
- {labcode-0.1.8 → labcode-0.2.0}/pyproject.toml +22 -9
- {labcode-0.1.8 → labcode-0.2.0}/tests/fixtures/device_script.env.yaml +0 -3
- labcode-0.2.0/tests/fixtures/replenishment.env.yaml +51 -0
- {labcode-0.1.8 → labcode-0.2.0}/tests/fixtures/reroute_device.env.yaml +0 -3
- {labcode-0.1.8 → labcode-0.2.0}/tests/fixtures/reroute_transporter.env.yaml +0 -3
- {labcode-0.1.8 → labcode-0.2.0}/tests/fixtures/transport.env.yaml +0 -3
- {labcode-0.1.8 → labcode-0.2.0}/tests/test_backend.py +87 -4
- {labcode-0.1.8 → labcode-0.2.0}/tests/test_dialect.py +93 -0
- {labcode-0.1.8 → labcode-0.2.0}/tests/test_recording.py +0 -1
- {labcode-0.1.8 → labcode-0.2.0}/tests/test_run_cli.py +40 -2
- labcode-0.1.8/labcode.egg-info/scm_version.json +0 -8
- {labcode-0.1.8 → labcode-0.2.0}/.github/workflows/ci.yml +0 -0
- {labcode-0.1.8 → labcode-0.2.0}/.github/workflows/publish.yml +0 -0
- {labcode-0.1.8 → labcode-0.2.0}/LICENSE +0 -0
- {labcode-0.1.8 → labcode-0.2.0}/MANIFEST.in +0 -0
- {labcode-0.1.8 → labcode-0.2.0}/SPECIFICATIONS.md +0 -0
- {labcode-0.1.8 → labcode-0.2.0}/examples/outputs/plate_line.boundary.yaml +0 -0
- {labcode-0.1.8 → labcode-0.2.0}/examples/outputs/plate_line.observation.yaml +0 -0
- {labcode-0.1.8 → labcode-0.2.0}/examples/outputs/plate_line.plan.yaml +0 -0
- {labcode-0.1.8 → labcode-0.2.0}/examples/outputs/plate_line.svg +0 -0
- {labcode-0.1.8 → labcode-0.2.0}/examples/outputs/sila2_plate_cycle.boundary.yaml +0 -0
- {labcode-0.1.8 → labcode-0.2.0}/examples/outputs/sila2_plate_cycle.observation.yaml +0 -0
- {labcode-0.1.8 → labcode-0.2.0}/examples/outputs/sila2_plate_cycle.plan.yaml +0 -0
- {labcode-0.1.8 → labcode-0.2.0}/examples/outputs/sila2_plate_cycle.svg +0 -0
- {labcode-0.1.8 → labcode-0.2.0}/examples/plate_line.boundary.yaml +0 -0
- {labcode-0.1.8 → labcode-0.2.0}/examples/plate_line.workflow.yaml +0 -0
- {labcode-0.1.8 → labcode-0.2.0}/examples/render_plate_line.py +0 -0
- {labcode-0.1.8 → labcode-0.2.0}/examples/render_sila2_plate_cycle.py +0 -0
- {labcode-0.1.8 → labcode-0.2.0}/examples/run_sila2_plate_cycle.py +0 -0
- {labcode-0.1.8 → labcode-0.2.0}/examples/run_sila2_seal.py +0 -0
- {labcode-0.1.8 → labcode-0.2.0}/examples/sila2_plate_cycle.boundary.yaml +0 -0
- {labcode-0.1.8 → labcode-0.2.0}/examples/sila2_plate_cycle.workflow.yaml +0 -0
- {labcode-0.1.8 → labcode-0.2.0}/examples/sila2_seal.boundary.yaml +0 -0
- {labcode-0.1.8 → labcode-0.2.0}/examples/sila2_seal.workflow.yaml +0 -0
- {labcode-0.1.8 → labcode-0.2.0}/labcode/__init__.py +0 -0
- {labcode-0.1.8 → labcode-0.2.0}/labcode/__main__.py +0 -0
- {labcode-0.1.8 → labcode-0.2.0}/labcode/cli.py +0 -0
- {labcode-0.1.8 → labcode-0.2.0}/labcode/idgen.py +0 -0
- {labcode-0.1.8 → labcode-0.2.0}/labcode/objectid.py +0 -0
- {labcode-0.1.8 → labcode-0.2.0}/labcode/otel.py +0 -0
- {labcode-0.1.8 → labcode-0.2.0}/labcode/otel_sila2.py +0 -0
- {labcode-0.1.8 → labcode-0.2.0}/labcode/py.typed +0 -0
- {labcode-0.1.8 → labcode-0.2.0}/labcode/sila2.py +0 -0
- {labcode-0.1.8 → labcode-0.2.0}/labcode/sila2_commands.py +0 -0
- {labcode-0.1.8 → labcode-0.2.0}/labcode.egg-info/dependency_links.txt +0 -0
- {labcode-0.1.8 → labcode-0.2.0}/labcode.egg-info/entry_points.txt +0 -0
- {labcode-0.1.8 → labcode-0.2.0}/labcode.egg-info/top_level.txt +0 -0
- {labcode-0.1.8 → labcode-0.2.0}/setup.cfg +0 -0
- {labcode-0.1.8 → labcode-0.2.0}/tests/fixtures/device_script.workflow.yaml +0 -0
- {labcode-0.1.8 → labcode-0.2.0}/tests/fixtures/transport.workflow.yaml +0 -0
- {labcode-0.1.8 → labcode-0.2.0}/tests/test_cli.py +0 -0
- {labcode-0.1.8 → labcode-0.2.0}/tests/test_objectid.py +0 -0
- {labcode-0.1.8 → labcode-0.2.0}/tests/test_otel.py +0 -0
- {labcode-0.1.8 → labcode-0.2.0}/tests/test_otel_child.py +0 -0
- {labcode-0.1.8 → labcode-0.2.0}/tests/test_otel_grpc.py +0 -0
- {labcode-0.1.8 → labcode-0.2.0}/tests/test_otel_sila2.py +0 -0
- {labcode-0.1.8 → labcode-0.2.0}/tests/test_probe.py +0 -0
- {labcode-0.1.8 → labcode-0.2.0}/tests/test_record.py +0 -0
- {labcode-0.1.8 → labcode-0.2.0}/tests/test_sila2.py +0 -0
- {labcode-0.1.8 → labcode-0.2.0}/tests/test_sila2_commands.py +0 -0
- {labcode-0.1.8 → labcode-0.2.0}/tests/test_sila2_instrument.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: labcode
|
|
3
|
-
Version: 0.
|
|
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.
|
|
25
|
-
Requires-Dist: ofplang-run>=0.
|
|
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
|
|
118
|
-
|
|
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
|
|
74
|
-
|
|
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/
|
|
189
|
-
|
|
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
|
|
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
|
|
427
|
-
(they are round trips that put the plate back where it started, and
|
|
428
|
-
needs open, so they follow one another without intervention) — prints a
|
|
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.
|