labcode 0.2.0__tar.gz → 0.3.1__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.2.0 → labcode-0.3.1}/PKG-INFO +16 -12
  2. {labcode-0.2.0 → labcode-0.3.1}/README.md +12 -8
  3. {labcode-0.2.0 → labcode-0.3.1}/SPECIFICATIONS.md +124 -36
  4. {labcode-0.2.0 → labcode-0.3.1}/examples/README.md +5 -5
  5. {labcode-0.2.0 → labcode-0.3.1}/examples/run_sila2_plate_cycle.py +12 -0
  6. {labcode-0.2.0 → labcode-0.3.1}/examples/run_sila2_seal.py +16 -0
  7. {labcode-0.2.0 → labcode-0.3.1}/labcode/backend.py +2 -2
  8. {labcode-0.2.0 → labcode-0.3.1}/labcode/dialect.py +66 -20
  9. {labcode-0.2.0 → labcode-0.3.1}/labcode/extension.py +32 -0
  10. {labcode-0.2.0 → labcode-0.3.1}/labcode.egg-info/PKG-INFO +16 -12
  11. {labcode-0.2.0 → labcode-0.3.1}/labcode.egg-info/requires.txt +3 -3
  12. labcode-0.3.1/labcode.egg-info/scm_version.json +8 -0
  13. {labcode-0.2.0 → labcode-0.3.1}/pyproject.toml +19 -6
  14. {labcode-0.2.0 → labcode-0.3.1}/tests/test_backend.py +34 -0
  15. {labcode-0.2.0 → labcode-0.3.1}/tests/test_dialect.py +82 -0
  16. labcode-0.2.0/labcode.egg-info/scm_version.json +0 -8
  17. {labcode-0.2.0 → labcode-0.3.1}/.github/workflows/ci.yml +0 -0
  18. {labcode-0.2.0 → labcode-0.3.1}/.github/workflows/publish.yml +0 -0
  19. {labcode-0.2.0 → labcode-0.3.1}/.gitignore +0 -0
  20. {labcode-0.2.0 → labcode-0.3.1}/LICENSE +0 -0
  21. {labcode-0.2.0 → labcode-0.3.1}/MANIFEST.in +0 -0
  22. {labcode-0.2.0 → labcode-0.3.1}/examples/outputs/plate_line.boundary.yaml +0 -0
  23. {labcode-0.2.0 → labcode-0.3.1}/examples/outputs/plate_line.observation.yaml +0 -0
  24. {labcode-0.2.0 → labcode-0.3.1}/examples/outputs/plate_line.plan.yaml +0 -0
  25. {labcode-0.2.0 → labcode-0.3.1}/examples/outputs/plate_line.svg +0 -0
  26. {labcode-0.2.0 → labcode-0.3.1}/examples/outputs/sila2_plate_cycle.boundary.yaml +0 -0
  27. {labcode-0.2.0 → labcode-0.3.1}/examples/outputs/sila2_plate_cycle.observation.yaml +0 -0
  28. {labcode-0.2.0 → labcode-0.3.1}/examples/outputs/sila2_plate_cycle.plan.yaml +0 -0
  29. {labcode-0.2.0 → labcode-0.3.1}/examples/outputs/sila2_plate_cycle.svg +0 -0
  30. {labcode-0.2.0 → labcode-0.3.1}/examples/plate_line.boundary.yaml +0 -0
  31. {labcode-0.2.0 → labcode-0.3.1}/examples/plate_line.env.yaml +0 -0
  32. {labcode-0.2.0 → labcode-0.3.1}/examples/plate_line.workflow.yaml +0 -0
  33. {labcode-0.2.0 → labcode-0.3.1}/examples/preflight_sila2_env.py +0 -0
  34. {labcode-0.2.0 → labcode-0.3.1}/examples/render_plate_line.py +0 -0
  35. {labcode-0.2.0 → labcode-0.3.1}/examples/render_sila2_plate_cycle.py +0 -0
  36. {labcode-0.2.0 → labcode-0.3.1}/examples/run_all_sila2_examples.py +0 -0
  37. {labcode-0.2.0 → labcode-0.3.1}/examples/run_sila2_plate_cycle_no_atc.py +0 -0
  38. {labcode-0.2.0 → labcode-0.3.1}/examples/sila2_plate_cycle.boundary.yaml +0 -0
  39. {labcode-0.2.0 → labcode-0.3.1}/examples/sila2_plate_cycle.workflow.yaml +0 -0
  40. {labcode-0.2.0 → labcode-0.3.1}/examples/sila2_plate_cycle.wrapped.env.yaml +0 -0
  41. {labcode-0.2.0 → labcode-0.3.1}/examples/sila2_plate_cycle_no_atc.boundary.yaml +0 -0
  42. {labcode-0.2.0 → labcode-0.3.1}/examples/sila2_plate_cycle_no_atc.workflow.yaml +0 -0
  43. {labcode-0.2.0 → labcode-0.3.1}/examples/sila2_plate_cycle_no_atc.wrapped.env.yaml +0 -0
  44. {labcode-0.2.0 → labcode-0.3.1}/examples/sila2_seal.boundary.yaml +0 -0
  45. {labcode-0.2.0 → labcode-0.3.1}/examples/sila2_seal.env.yaml +0 -0
  46. {labcode-0.2.0 → labcode-0.3.1}/examples/sila2_seal.workflow.yaml +0 -0
  47. {labcode-0.2.0 → labcode-0.3.1}/examples/sila2_seal.wrapped.env.yaml +0 -0
  48. {labcode-0.2.0 → labcode-0.3.1}/labcode/__init__.py +0 -0
  49. {labcode-0.2.0 → labcode-0.3.1}/labcode/__main__.py +0 -0
  50. {labcode-0.2.0 → labcode-0.3.1}/labcode/_child.py +0 -0
  51. {labcode-0.2.0 → labcode-0.3.1}/labcode/cli.py +0 -0
  52. {labcode-0.2.0 → labcode-0.3.1}/labcode/idgen.py +0 -0
  53. {labcode-0.2.0 → labcode-0.3.1}/labcode/objectid.py +0 -0
  54. {labcode-0.2.0 → labcode-0.3.1}/labcode/otel.py +0 -0
  55. {labcode-0.2.0 → labcode-0.3.1}/labcode/otel_sila2.py +0 -0
  56. {labcode-0.2.0 → labcode-0.3.1}/labcode/probe.py +0 -0
  57. {labcode-0.2.0 → labcode-0.3.1}/labcode/py.typed +0 -0
  58. {labcode-0.2.0 → labcode-0.3.1}/labcode/record.py +0 -0
  59. {labcode-0.2.0 → labcode-0.3.1}/labcode/run_cli.py +0 -0
  60. {labcode-0.2.0 → labcode-0.3.1}/labcode/runner.py +0 -0
  61. {labcode-0.2.0 → labcode-0.3.1}/labcode/sila2.py +0 -0
  62. {labcode-0.2.0 → labcode-0.3.1}/labcode/sila2_commands.py +0 -0
  63. {labcode-0.2.0 → labcode-0.3.1}/labcode/sila2_instrument.py +0 -0
  64. {labcode-0.2.0 → labcode-0.3.1}/labcode.egg-info/SOURCES.txt +0 -0
  65. {labcode-0.2.0 → labcode-0.3.1}/labcode.egg-info/dependency_links.txt +0 -0
  66. {labcode-0.2.0 → labcode-0.3.1}/labcode.egg-info/entry_points.txt +0 -0
  67. {labcode-0.2.0 → labcode-0.3.1}/labcode.egg-info/scm_file_list.json +0 -0
  68. {labcode-0.2.0 → labcode-0.3.1}/labcode.egg-info/top_level.txt +0 -0
  69. {labcode-0.2.0 → labcode-0.3.1}/setup.cfg +0 -0
  70. {labcode-0.2.0 → labcode-0.3.1}/tests/fixtures/device_script.env.yaml +0 -0
  71. {labcode-0.2.0 → labcode-0.3.1}/tests/fixtures/device_script.workflow.yaml +0 -0
  72. {labcode-0.2.0 → labcode-0.3.1}/tests/fixtures/replenishment.env.yaml +0 -0
  73. {labcode-0.2.0 → labcode-0.3.1}/tests/fixtures/reroute_device.env.yaml +0 -0
  74. {labcode-0.2.0 → labcode-0.3.1}/tests/fixtures/reroute_transporter.env.yaml +0 -0
  75. {labcode-0.2.0 → labcode-0.3.1}/tests/fixtures/transport.env.yaml +0 -0
  76. {labcode-0.2.0 → labcode-0.3.1}/tests/fixtures/transport.workflow.yaml +0 -0
  77. {labcode-0.2.0 → labcode-0.3.1}/tests/test_cli.py +0 -0
  78. {labcode-0.2.0 → labcode-0.3.1}/tests/test_objectid.py +0 -0
  79. {labcode-0.2.0 → labcode-0.3.1}/tests/test_otel.py +0 -0
  80. {labcode-0.2.0 → labcode-0.3.1}/tests/test_otel_child.py +0 -0
  81. {labcode-0.2.0 → labcode-0.3.1}/tests/test_otel_grpc.py +0 -0
  82. {labcode-0.2.0 → labcode-0.3.1}/tests/test_otel_sila2.py +0 -0
  83. {labcode-0.2.0 → labcode-0.3.1}/tests/test_probe.py +0 -0
  84. {labcode-0.2.0 → labcode-0.3.1}/tests/test_record.py +0 -0
  85. {labcode-0.2.0 → labcode-0.3.1}/tests/test_recording.py +0 -0
  86. {labcode-0.2.0 → labcode-0.3.1}/tests/test_run_cli.py +0 -0
  87. {labcode-0.2.0 → labcode-0.3.1}/tests/test_sila2.py +0 -0
  88. {labcode-0.2.0 → labcode-0.3.1}/tests/test_sila2_commands.py +0 -0
  89. {labcode-0.2.0 → labcode-0.3.1}/tests/test_sila2_instrument.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: labcode
3
- Version: 0.2.0
3
+ Version: 0.3.1
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
@@ -20,9 +20,9 @@ Classifier: Typing :: Typed
20
20
  Requires-Python: >=3.10
21
21
  Description-Content-Type: text/markdown
22
22
  License-File: LICENSE
23
- Requires-Dist: ofplang-validate>=0.1.3
24
- Requires-Dist: ofplang-schedule>=0.2.3
25
- Requires-Dist: ofplang-run>=0.3.0
23
+ Requires-Dist: ofplang-validate<0.3,>=0.2
24
+ Requires-Dist: ofplang-schedule<0.6,>=0.5
25
+ Requires-Dist: ofplang-run<0.7,>=0.6
26
26
  Provides-Extra: test
27
27
  Requires-Dist: pytest>=7.0; extra == "test"
28
28
  Requires-Dist: opentelemetry-sdk>=1.20; extra == "test"
@@ -98,19 +98,23 @@ repository, and what labcode adds to it in [`SPECIFICATIONS.md`](SPECIFICATIONS.
98
98
 
99
99
  What `lc run` brings of its own, beyond dispatching:
100
100
 
101
- - **the labcode backend** — each device operation's `x-labcode.script` runs
102
- out-of-process on a wall clock (§1.2, §1.3), so a real operation that takes minutes
103
- does not block the replan loop, and an operation that never returns is stopped by
104
- **`op_timeout`** (§1.8; `--op-timeout` / `--no-op-timeout`).
101
+ - **the labcode backend** — each operation's `x-labcode.script` runs out-of-process on a
102
+ wall clock (§1.2–§1.4), so a real operation that takes minutes does not block the
103
+ replan loop, and an operation that never returns is stopped by **`op_timeout`**
104
+ (§1.9; `--op-timeout` / `--no-op-timeout`).
105
+ - **refilling a stock** — where the environment says a replenisher can reach a device, a
106
+ stock that would run out is topped up rather than ending the run: the refill's own
107
+ script runs like any other, holding both machines while it works (§1.4). See
108
+ *Refilling a stock* below.
105
109
  - **the dialect front door** — the environment's `x-labcode` extension is validated
106
110
  before anything runs, on top of the portable-v0 check `lc validate` performs (§1, §2).
107
111
  - **availability probing** — each machine is checked as often as its `probe` policy says,
108
112
  and one that cannot be reached is taken out of the environment the scheduler plans
109
- against, so the run routes around it (§1.5; `--no-probe`).
113
+ against, so the run routes around it (§1.6; `--no-probe`).
110
114
  - **object identity** — the reserved `_id` view key is declared on Object types and minted
111
115
  per object, so a physical thing can be followed through a run (§4).
112
116
  - **`flavor: sila2`** — a script that speaks SiLA2 gets its clients opened around it
113
- (§1.6). The client library itself is the `sila2` extra: `pip install labcode[sila2]`,
117
+ (§1.7). The client library itself is the `sila2` extra: `pip install labcode[sila2]`,
114
118
  installed into whichever interpreter runs the scripts.
115
119
  - **recording a run** — with `--trace`, what the run did is recorded as OpenTelemetry
116
120
  traces: one trace per run, a span per operation, and — measured inside the process that
@@ -168,10 +172,10 @@ lc run <workflow> --env <env>
168
172
  completed reads as discrete, observable steps; a demo against a fast mock wants a small
169
173
  value.
170
174
  - `--op-timeout S` / `--no-op-timeout` — how long one operation may run before it is
171
- stopped and failed (§1.8). The default is the environment root's `x-labcode.op_timeout`,
175
+ stopped and failed (§1.9). The default is the environment root's `x-labcode.op_timeout`,
172
176
  else 7200 real seconds. The two forms exclude each other.
173
177
  - `--no-probe` — ignore the environment's `x-labcode.probe` policies and treat every
174
- machine as reachable (§1.5). The documents are still validated.
178
+ machine as reachable (§1.6). The documents are still validated.
175
179
  - `--ignore-resources` — switch the consumable model off. The environment's resource
176
180
  declarations are still checked for shape but none is applied, so a bench whose devices
177
181
  declare stocks nobody is tracking runs without the boundary saying what they held.
@@ -54,19 +54,23 @@ repository, and what labcode adds to it in [`SPECIFICATIONS.md`](SPECIFICATIONS.
54
54
 
55
55
  What `lc run` brings of its own, beyond dispatching:
56
56
 
57
- - **the labcode backend** — each device operation's `x-labcode.script` runs
58
- out-of-process on a wall clock (§1.2, §1.3), so a real operation that takes minutes
59
- does not block the replan loop, and an operation that never returns is stopped by
60
- **`op_timeout`** (§1.8; `--op-timeout` / `--no-op-timeout`).
57
+ - **the labcode backend** — each operation's `x-labcode.script` runs out-of-process on a
58
+ wall clock (§1.2–§1.4), so a real operation that takes minutes does not block the
59
+ replan loop, and an operation that never returns is stopped by **`op_timeout`**
60
+ (§1.9; `--op-timeout` / `--no-op-timeout`).
61
+ - **refilling a stock** — where the environment says a replenisher can reach a device, a
62
+ stock that would run out is topped up rather than ending the run: the refill's own
63
+ script runs like any other, holding both machines while it works (§1.4). See
64
+ *Refilling a stock* below.
61
65
  - **the dialect front door** — the environment's `x-labcode` extension is validated
62
66
  before anything runs, on top of the portable-v0 check `lc validate` performs (§1, §2).
63
67
  - **availability probing** — each machine is checked as often as its `probe` policy says,
64
68
  and one that cannot be reached is taken out of the environment the scheduler plans
65
- against, so the run routes around it (§1.5; `--no-probe`).
69
+ against, so the run routes around it (§1.6; `--no-probe`).
66
70
  - **object identity** — the reserved `_id` view key is declared on Object types and minted
67
71
  per object, so a physical thing can be followed through a run (§4).
68
72
  - **`flavor: sila2`** — a script that speaks SiLA2 gets its clients opened around it
69
- (§1.6). The client library itself is the `sila2` extra: `pip install labcode[sila2]`,
73
+ (§1.7). The client library itself is the `sila2` extra: `pip install labcode[sila2]`,
70
74
  installed into whichever interpreter runs the scripts.
71
75
  - **recording a run** — with `--trace`, what the run did is recorded as OpenTelemetry
72
76
  traces: one trace per run, a span per operation, and — measured inside the process that
@@ -124,10 +128,10 @@ lc run <workflow> --env <env>
124
128
  completed reads as discrete, observable steps; a demo against a fast mock wants a small
125
129
  value.
126
130
  - `--op-timeout S` / `--no-op-timeout` — how long one operation may run before it is
127
- stopped and failed (§1.8). The default is the environment root's `x-labcode.op_timeout`,
131
+ stopped and failed (§1.9). The default is the environment root's `x-labcode.op_timeout`,
128
132
  else 7200 real seconds. The two forms exclude each other.
129
133
  - `--no-probe` — ignore the environment's `x-labcode.probe` policies and treat every
130
- machine as reachable (§1.5). The documents are still validated.
134
+ machine as reachable (§1.6). The documents are still validated.
131
135
  - `--ignore-resources` — switch the consumable model off. The environment's resource
132
136
  declarations are still checked for shape but none is applied, so a bench whose devices
133
137
  declare stocks nobody is tracking runs without the boundary saying what they held.
@@ -12,10 +12,12 @@ conformance validator, run at the `lc run` front door.
12
12
 
13
13
  ## 1. `x-labcode` in an environment (P5)
14
14
 
15
- The extension appears in four places, each answering a different question: on a **process
16
- mode** and on a **transport route** it says *what to run* (a `script`, §1.1–§1.3); on a
17
- **device** and on a **transporter** it says *how to reach the machine* (a `connection`,
18
- §1.4), which is what lets a script be the commands alone (§1.6). Nowhere else — see §1.7.
15
+ The extension answers two different questions, in two kinds of place. On a **process
16
+ mode**, a **transport route** and a **replenishment route** it says *what to run* (a
17
+ `script`, §1.1–§1.4); on a **device**, a **transporter** and a **replenisher** it says
18
+ *how to reach the machine* (a `connection`, §1.5), which is what lets a script be the
19
+ commands alone (§1.7). The division is the same each time: the machine has an address,
20
+ and the thing it does to something else has a procedure. Nowhere else — see §1.8.
19
21
 
20
22
  An environment process mode (§5) may carry an `x-labcode` mapping holding a `script`: the
21
23
  Python that carries out that `(process, mode)`.
@@ -39,20 +41,23 @@ still validates and schedules as plain v0. Only labcode interprets it.
39
41
 
40
42
  ### 1.1 Shape
41
43
 
42
- - `x-labcode` MUST be a mapping. On a process mode or a transport route its only key is
43
- `script`.
44
+ - `x-labcode` MUST be a mapping. On a process mode, a transport route or a replenishment
45
+ route its only key is `script`.
44
46
  - `x-labcode.script`, if present, MUST be a mapping with:
45
47
  - `language`: MUST be `python`.
46
48
  - `code`: MUST be a string (an implementation-provided Python function body).
47
49
  - `flavor` (optional, default `raw`): MUST be `raw` or `sila2` — how `code` is meant to
48
- be run (§1.6). `sila2` is the **recommended** way to drive a SiLA2 lab: the code is the
50
+ be run (§1.7). `sila2` is the **recommended** way to drive a SiLA2 lab: the code is the
49
51
  commands alone and labcode supplies the clients. `raw` is the whole function body,
50
52
  written by its author — the general escape hatch, and what a script that connects for
51
- itself (or speaks something other than SiLA2) uses.
53
+ itself (or speaks something other than SiLA2) uses. On a **replenishment route**
54
+ `sila2` is an error in this version (§1.4).
52
55
  - `endpoints` (**transport routes only**, optional, default `false`): MUST be a boolean —
53
56
  whether this move is also given clients for the devices at either **end** of its route,
54
- not only its `transporter` (§1.6). A process mode may not declare it: a mode's machines
55
- are the ones it lists.
57
+ not only its `transporter` (§1.7). A process mode may not declare it: a mode's machines
58
+ are the ones it lists. A `sila2` script on a route with **no transporter** (§1.3) MUST
59
+ declare it `true`: the ends are then the only machines there are, and a script is never
60
+ given a machine it did not ask for.
56
61
 
57
62
  **Unknown keys are an error** — in `x-labcode` at every position, and in the mappings it
58
63
  holds. A key this version does not know is either a typo or a feature it does not have;
@@ -121,12 +126,76 @@ output is verified. Success is "it ran without raising"; an exception is a grace
121
126
  A route with no `x-labcode.script` runs as a plain timed move — the runner's material
122
127
  bookkeeping only, with no device command (a warned no-op for a real move, from != to).
123
128
 
124
- ### 1.4 `x-labcode` on a device or a transporter
129
+ **A route with no transporter.** An environment may declare a route that needs no
130
+ transporter at all, by writing `transporter: null` (ofplang-schedule §4.6 / §5.4) — a
131
+ device shifting material between its own spots, a chute. The plan then reports the move
132
+ with `transporter: null` too, and this dialect matches it like any other: routes are keyed
133
+ by `(transporter, from, to)`, so a null one matches a null one, and the script's
134
+ `transporter` local is `None`.
125
135
 
126
- An environment `devices[]` or `transporters[]` entry may carry an `x-labcode` with two
127
- keys: `connection` **where that machine is**, written once per physical machine rather
128
- than repeated in every script that drives it and `probe` (§1.5) — whether to check that
129
- it still answers.
136
+ Such a move is performed by the **devices at either end of the route** for a move within
137
+ one device, that device. So a `flavor: sila2` script on such a route must declare
138
+ **`endpoints: true`** (§1.6), and at least one of those devices must declare a
139
+ `connection`; either missing is a front-door error. The `endpoints` request is *required*
140
+ rather than inferred: it is the author's statement of which machines the script drives, and
141
+ reading it for them would decide that on exactly the routes where it matters most. A `raw`
142
+ script, or no script at all, is unaffected — neither is handed clients.
143
+
144
+ `sila2_client` is then the first machine connected to, which is the **source device**: the
145
+ one performing the move, exactly as it is the transporter on a carried route (§1.7).
146
+
147
+ ### 1.4 `x-labcode` on a replenishment route
148
+
149
+ An environment `replenishments[]` route may carry an `x-labcode` with a `script`: the
150
+ Python that physically refills that device from that replenisher (e.g. commanding a
151
+ dispenser). Same shape as §1.1 (`language: python`, string `code`).
152
+
153
+ The procedure lives on the **route**, not on the machine — the same division transports
154
+ and transporters have. A dispenser's address is a property of the dispenser (§1.5); how it
155
+ fills *this* device is a property of the pair.
156
+
157
+ ```yaml
158
+ replenishments:
159
+ - replenisher: dispenser
160
+ device: reader
161
+ duration: 4 # ticks: the scheduler's estimate of the visit
162
+ x-labcode:
163
+ script:
164
+ language: python
165
+ code: |
166
+ import time
167
+ time.sleep(80) # real seconds: what the visit actually takes
168
+ ```
169
+
170
+ **Calling convention (replenishment).** The script runs as a function body with these
171
+ locals: `replenisher`, `device` (the two machines the visit holds) and `amounts` — the
172
+ `{resource: amount}` the scheduler derived, which a planned refill fills to the device's
173
+ capacity. It is **not** given the duration: a real refill takes as long as it takes, and
174
+ the ticks the plan reserved are the scheduler's estimate rather than an instruction, so a
175
+ stand-in states its own time (which is why the two numbers above are written separately).
176
+
177
+ A replenishment script is **side-effect only**, as a transport's is: its return value is
178
+ ignored and no output is verified. An exception is a graceful failure — the refill ends
179
+ `failed` and the run stops, like any activity failure.
180
+
181
+ `flavor: sila2` is **an error** on a replenishment route in this version. A `sila2` script
182
+ is handed clients (§1.7), and which machine's clients a refill should receive — the
183
+ replenisher's, or both ends' as a transport route may ask for — is not settled. Refusing
184
+ says so; running the script without the clients it asked for would not. Use `raw` (the
185
+ default), which may of course connect for itself.
186
+
187
+ A route with no `x-labcode.script` runs as a plain timed visit: both machines are held for
188
+ the declared duration and nothing is commanded. That is a legitimate environment to write
189
+ — an operator tops the stock up while the schedule waits for them — and an easy one to
190
+ write by accident, so it is **warned** about, as a scriptless real move is.
191
+
192
+ ### 1.5 `x-labcode` on a device, a transporter or a replenisher
193
+
194
+ An environment `devices[]`, `transporters[]` or `replenishers[]` entry may carry an
195
+ `x-labcode` with two keys: `connection` — **where that machine is**, written once per
196
+ physical machine rather than repeated in every script that drives it — and `probe` (§1.6)
197
+ — whether to check that it still answers. The three kinds are treated alike because they
198
+ are alike: each is a machine with an address that a run may find unreachable.
130
199
 
131
200
  ```yaml
132
201
  devices:
@@ -162,21 +231,25 @@ a script uses it.
162
231
  - a mode script with `flavor: sila2` requires **at least one** of that mode's `devices[]`
163
232
  to declare a `connection`;
164
233
  - a transport script with `flavor: sila2` requires that route's `transporter` to declare
165
- one.
234
+ one — or, on a route with **no transporter** (§1.3), requires `endpoints: true` and at
235
+ least one of the devices at its ends to declare one, those being the machines that
236
+ perform such a move.
166
237
 
167
238
  A transport that declares `endpoints: true` is also handed the clients of the devices at
168
- either **end** of its route (§1.6), but those are *not* required to declare a `connection`:
239
+ either **end** of its route (§1.7), but those are *not* required to declare a `connection`:
169
240
  a route through a plain holding location is ordinary, and the end without an address is
170
241
  simply not connected to (a **warning** when *neither* end has one, since then the request
171
- does nothing). The transporter is the one that must be reachable, because it is the machine
172
- that does the moving — and the one `sila2_client` names. Asking a `raw` script for endpoint
242
+ does nothing an **error** on a route with no transporter, which has nothing else to
243
+ drive). The transporter is the one that must be reachable, because it is the machine that
244
+ does the moving — and the one `sila2_client` names; where there is none, that is the source
245
+ device, for the same reason. Asking a `raw` script for endpoint
173
246
  clients is an **error**: a raw script is handed no clients at all, so the request cannot be
174
247
  honoured.
175
248
 
176
249
  Declaring a `connection` on a device no script connects to is allowed — it is how an
177
250
  environment is prepared before the scripts that use it are written.
178
251
 
179
- ### 1.5 Availability — `probe`
252
+ ### 1.6 Availability — `probe`
180
253
 
181
254
  A machine that stops answering should not keep receiving work. A `probe` policy asks labcode
182
255
  to check the machines it knows how to reach, and to tell the scheduler about the ones it
@@ -263,7 +336,7 @@ belongs — as the operation that tried to command it failing.
263
336
  document is still validated, so an environment that is wrong about probing stays wrong).
264
337
  Each machine whose reachability changes is reported on stderr.
265
338
 
266
- ### 1.6 Calling convention (`flavor: sila2`)
339
+ ### 1.7 Calling convention (`flavor: sila2`)
267
340
 
268
341
  A `sila2` script is the **commands alone**: labcode opens a client to each of the
269
342
  operation's machines, runs the code with them in scope, and closes them afterwards. On top
@@ -271,8 +344,8 @@ of the input ports of §1.2 (or the transport locals of §1.3), the code sees:
271
344
 
272
345
  | name | meaning |
273
346
  |---|---|
274
- | `sila2_clients` | the clients by **machine id**, in the order the operation holds its machines: a mode's `devices[]` order, or — for a transport — its `transporter`, followed by the devices at either **end of the route** when it declares `endpoints: true` |
275
- | `sila2_client` | the first of them — for a transport always its `transporter`; the one name a single-machine operation needs |
347
+ | `sila2_clients` | the clients by **machine id**, in the order the operation names its machines: a mode's `devices[]` order, or — for a transport — its `transporter` (absent on a route that has none, §1.3), followed by the devices at either **end of the route** when it declares `endpoints: true`. Named, not held: a mode declaring `device_access: false` (ofplang-schedule §4.4.2) rests on its devices rather than occupying them, and its script is still handed their clients |
348
+ | `sila2_client` | the first of them — for a transport its `transporter`, or the **source device** on a route with none (§1.3); the one name a single-machine operation needs |
276
349
 
277
350
  ```yaml
278
351
  x-labcode:
@@ -301,6 +374,12 @@ x-labcode:
301
374
  property of the move, not of the lab. A route that does not ask still *holds* both ends, so
302
375
  reaching for one is answered with what to add rather than with silence.
303
376
 
377
+ On a route with **no transporter** (§1.3) a `sila2` script must ask: the ends are the only
378
+ machines there are, so not asking leaves nothing to open, and the front door says so rather
379
+ than letting the move fail when it runs. It is still asked for, not assumed — which
380
+ machines a script drives is the author's to state, and nowhere more so than where the
381
+ machine doing the moving is also the one holding the material.
382
+
304
383
  ```yaml
305
384
  transports:
306
385
  - transporter: arm
@@ -371,7 +450,7 @@ equally available to a `raw` script, and stays visible in the code that depends
371
450
  is to turn a hang into a diagnosable failure.
372
451
  - **It is the inner of two limits.** This one is per command, chosen by the script that
373
452
  knows what it is waiting for, and its failure can name the command that hung. The outer
374
- one (§1.8) is per operation and lab-wide, and catches the hangs no script is watching
453
+ one (§1.9) is per operation and lab-wide, and catches the hangs no script is watching
375
454
  for. The outer default is looser than this one, so where both apply this is what fires.
376
455
  - **Its timeout is in real seconds**, and is unrelated to the mode's `duration` — which is
377
456
  an *estimate*, in environment time, for scheduling. A schedule's estimate is not a
@@ -381,11 +460,11 @@ equally available to a `raw` script, and stays visible in the code that depends
381
460
  - A `sila2` script is only interpreted where the dialect is — in an environment
382
461
  `x-labcode`. A workflow's own `script` (v0 §22) has no `flavor`.
383
462
 
384
- ### 1.7 Where an `x-labcode` may appear
463
+ ### 1.8 Where an `x-labcode` may appear
385
464
 
386
465
  The positions of §1 are the only ones: the environment **root** (`probe` defaults and
387
- `op_timeout`), `processes.<p>.modes[]`, `transports[]`, `devices[]` and
388
- `transporters[]`. An `x-labcode`
466
+ `op_timeout`), `processes.<p>.modes[]`, `transports[]`, `replenishments[]`, `devices[]`,
467
+ `transporters[]` and `replenishers[]`. An `x-labcode`
389
468
  anywhere else in the environment — on a process, beside `time` — is an **error**, as is a
390
469
  key at a position that does not define it (a `connection` at the root, a `probe` on a mode).
391
470
  Nothing would read it, and `ofplang-schedule` tolerates an `x-` key at *every* position
@@ -395,7 +474,7 @@ This rule covers the environment only. An `x-labcode` in the **workflow** is not
395
474
  that document is portable v0, read by other implementations, and what extension keys it
396
475
  carries is not labcode's business.
397
476
 
398
- ### 1.8 Operation timeout — `op_timeout`
477
+ ### 1.9 Operation timeout — `op_timeout`
399
478
 
400
479
  How long **one operation** may run before labcode stops waiting for it, in **real
401
480
  seconds**. It lives at the environment root and nowhere else:
@@ -410,7 +489,7 @@ x-labcode:
410
489
  way to say "no limit" and is an error; a machine may not declare one (a per-machine key
411
490
  is an unknown key, §1.1).
412
491
  - **One value for the whole lab.** The fine-grained waits belong to the scripts, which know
413
- what they are waiting for (`settle`, §1.6.1); this value only has to clear the longest
492
+ what they are waiting for (`settle`, §1.7.1); this value only has to clear the longest
414
493
  operation the lab legitimately runs. Its default (7200 s) is twice the `settle` default,
415
494
  so where both apply the inner one — which can name the command — fires first.
416
495
  - **The clock is real seconds**, from the moment the operation starts, covering everything
@@ -422,13 +501,13 @@ x-labcode:
422
501
  failure — the run stops, the status document is written, the reason is reported, the exit
423
502
  code is 1 — which is the point: without a limit, an instrument that stops answering
424
503
  leaves a run polling with *no* status document and no reason at all.
425
- - **A timeout is not a cancel**, exactly as in §1.6.1: nothing here can stop a command the
504
+ - **A timeout is not a cancel**, exactly as in §1.7.1: nothing here can stop a command the
426
505
  instrument has already accepted. It keeps running, and the state that leaves behind —
427
506
  including material a transport was part way through moving — is the operator's to
428
507
  restore. The run stops there, so labcode's own picture of the lab is not relied on
429
508
  afterwards.
430
509
  - The machine that hung is **not** treated as unavailable: `op_timeout` does not add it to
431
- the down machines (§1.5), because "not answering" is not "not there", and re-routing work
510
+ the down machines (§1.6), because "not answering" is not "not there", and re-routing work
432
511
  onto other machines while this one is still physically running its command would make the
433
512
  lab less consistent, not more.
434
513
  - `lc run` overrides it for one run: `--op-timeout SECONDS`, or `--no-op-timeout` for no
@@ -450,6 +529,11 @@ For a dispatched `(process, mode)`, labcode resolves the code to run in this ord
450
529
  will run as a typed-default no-op. This is allowed — convenient while mocking a device —
451
530
  but `lc run` warns about it, so an unimplemented device is not silently a no-op.
452
531
 
532
+ **Transport and replenishment routes have no such chain.** There is nothing for them to
533
+ fall back to: a workflow describes neither a physical move nor a refill, so the route's
534
+ own `x-labcode.script` is the only source. A route without one runs as a plain timed
535
+ activity (§1.3, §1.4), warned about for the same reason as above.
536
+
453
537
  ## 3. Execution model
454
538
 
455
539
  Each dispatched operation runs in its own child process (real, wall-clock-paced); the
@@ -457,7 +541,7 @@ runner discovers completion by polling, so a multi-minute computation never bloc
457
541
  The advisory `duration` is the scheduler's estimate; the real duration is the script's.
458
542
  A script error (an exception, a wrong/ missing output name, a non-conformant value) is a
459
543
  graceful runtime failure (§22.2): the operation ends `failed` and the run stops. An
460
- operation that never finishes at all ends the same way once it passes `op_timeout` (§1.8) —
544
+ operation that never finishes at all ends the same way once it passes `op_timeout` (§1.9) —
461
545
  polling for completion is not the same as waiting forever for it.
462
546
 
463
547
  Cadence: the nominal poll period is `poll_interval × seconds_per_tick`. labcode defaults
@@ -503,7 +587,7 @@ statement that the cycle was cheap.
503
587
 
504
588
  So: keep the budget comfortably larger than the cycle cost. What the cycle costs is not
505
589
  fixed — replanning grows with the workflow, and a dialect step such as availability probing
506
- (§1.5) can add seconds — so the margin wants to be generous rather than exact. A run whose
590
+ (§1.6) can add seconds — so the margin wants to be generous rather than exact. A run whose
507
591
  recorded times matter (a checked-in example, a comparison against the plan's estimates)
508
592
  needs this to hold; a run that only has to *complete* does not.
509
593
 
@@ -562,9 +646,13 @@ ids per physical Object swaps in `RealUuid4Generator` (via
562
646
 
563
647
  ## 5. Not yet in this version (roadmap)
564
648
 
649
+ - **`flavor: sila2` on a replenishment route** — refused today (§1.4). What has to be
650
+ settled first is which machine's clients a refill script receives: the replenisher's
651
+ alone, or both ends' as a transport route may ask for with `endpoints`. Until then a
652
+ refill that must speak SiLA2 uses a `raw` script and connects for itself.
565
653
  - **A deeper probe** — asking a machine something (a SiLA2 property read) rather than only
566
654
  opening a connection to it, so "answering" can be checked and not just "listening"
567
- (§1.5). It would be an opt-in depth, since it costs a real exchange per check.
655
+ (§1.6). It would be an opt-in depth, since it costs a real exchange per check.
568
656
  - **Probing in parallel** — checking machines concurrently, so a lab with many unreachable
569
- machines does not pay for them one timeout at a time (§1.5).
570
- - **TLS** — the fields a secure connection needs, lifting the restriction in §1.4.
657
+ machines does not pay for them one timeout at a time (§1.6).
658
+ - **TLS** — the fields a secure connection needs, lifting the restriction in §1.5.
@@ -136,7 +136,7 @@ x-labcode:
136
136
 
137
137
  What the flavor supplies is connections, nothing more. Waiting for an *observable* command to
138
138
  finish stays in the script — but the loop itself does not have to be rewritten each time:
139
- labcode ships `settle` in **`labcode.sila2_commands`** (§1.6.1), reached by the ordinary import
139
+ labcode ships `settle` in **`labcode.sila2_commands`** (§1.7.1), reached by the ordinary import
140
140
  above. Nothing is injected, so a script that does not import it does not have it, and a `raw`
141
141
  script has exactly the same access as this one. A live connection is worth a name that appears
142
142
  out of nowhere; an import is not.
@@ -149,14 +149,14 @@ instrument carries on, leaving the lab for the operator to restore.
149
149
  The names a script hands a machine still have to be the ones that machine knows — for the arm
150
150
  those are its **station** names (`Base1`, `Base4`, …), not labcode's `device.spot`, and one it
151
151
  does not know fails with `InvalidStation` at the moment of use. See
152
- [`../SPECIFICATIONS.md`](../SPECIFICATIONS.md) §1.4 and §1.6, and the plate-cycle section below
152
+ [`../SPECIFICATIONS.md`](../SPECIFICATIONS.md) §1.5 and §1.7, and the plate-cycle section below
153
153
  for why the two vocabularies do not meet anywhere but in a transport script.
154
154
 
155
155
  ### If a machine stops answering
156
156
 
157
157
  Neither environment here asks for it, but labcode can **check that a machine is reachable**
158
158
  and schedule around the ones that are not — see
159
- [`../SPECIFICATIONS.md`](../SPECIFICATIONS.md) §1.5. Adding this to a device (or a
159
+ [`../SPECIFICATIONS.md`](../SPECIFICATIONS.md) §1.6. Adding this to a device (or a
160
160
  transporter) that declares a `connection`:
161
161
 
162
162
  ```yaml
@@ -175,7 +175,7 @@ run without editing the environment.
175
175
 
176
176
  Probing catches a machine that is not there; it does not catch one that accepted a command
177
177
  and never came back. That is what the **operation timeout** is for
178
- ([`../SPECIFICATIONS.md`](../SPECIFICATIONS.md) §1.8): every operation has a real-seconds
178
+ ([`../SPECIFICATIONS.md`](../SPECIFICATIONS.md) §1.9): every operation has a real-seconds
179
179
  deadline (7200 s by default, declared lab-wide at the environment root as
180
180
  `x-labcode.op_timeout`), and one that passes it is stopped and failed with the reason
181
181
  `op_timeout` — a run that ends with a status document and a reason instead of one that
@@ -324,7 +324,7 @@ dependency runs one way, and the lab's world model is not labcode's to edit.
324
324
  **Lids and doors.** In the lab's world model a closed lid or door makes that spot inaccessible,
325
325
  and an item cannot be moved into or out of an inaccessible spot. So the transport that delivers
326
326
  the plate is what opens the instrument: three of the five routes declare `endpoints: true` and
327
- are handed clients for the devices at either end as well as for the arm (§1.6). That is sound
327
+ are handed clients for the devices at either end as well as for the arm (§1.7). That is sound
328
328
  because the scheduler has already given the move both instruments for its whole duration —
329
329
  nothing else can be using them meanwhile.
330
330
 
@@ -325,6 +325,18 @@ def main(argv: list[str] | None = None) -> int:
325
325
  print(f"makespan : {status.get('now')}")
326
326
  print(f"result boundary : {result.result_boundary}")
327
327
 
328
+ # Why the run stopped, when it did. `RunResult.failure` (D36) is a machine-readable
329
+ # `kind` and a human-readable `detail` -- the reason the failing operation gave, which
330
+ # `lc run` prints and this script was discarding. The status document names the
331
+ # activities that did not complete but never says why any of them failed, so without
332
+ # this the operator is left to guess between an instrument that refused the command,
333
+ # one that stopped answering, and a station name the arm does not have.
334
+ if result.failed and result.failure is not None:
335
+ print(
336
+ f"run failure : {result.failure.kind}: {result.failure.detail}",
337
+ file=sys.stderr,
338
+ )
339
+
328
340
  try:
329
341
  check_outcome(status, result.result_boundary, observation)
330
342
  except CheckFailed as error:
@@ -281,6 +281,22 @@ def main(argv: list[str] | None = None) -> int:
281
281
  print(f"makespan : {status.get('now')}")
282
282
  print(f"result boundary : {result.result_boundary}")
283
283
 
284
+ # Why the run stopped, when it did. `RunResult.failure` (D36) is a machine-readable
285
+ # `kind` and a human-readable `detail` -- the reason the failing operation gave, which
286
+ # `lc run` prints and this script was discarding. The status document names the
287
+ # activities that did not complete but never says why any of them failed, so without
288
+ # this the operator is left to guess between an instrument that refused the command,
289
+ # one that stopped answering, and a station name the arm does not have.
290
+ #
291
+ # It matters most here. This example is also the one that walks through what a run does
292
+ # when a machine stops answering -- before an operation and in the middle of one -- so
293
+ # the failure it exists to demonstrate was the failure it was declining to name.
294
+ if result.failed and result.failure is not None:
295
+ print(
296
+ f"run failure : {result.failure.kind}: {result.failure.detail}",
297
+ file=sys.stderr,
298
+ )
299
+
284
300
  try:
285
301
  check_outcome(status, result.result_boundary, observation)
286
302
  except CheckFailed as error:
@@ -57,6 +57,7 @@ from labcode.extension import (
57
57
  script_endpoints,
58
58
  script_flavor,
59
59
  spot_device,
60
+ transport_label,
60
61
  transporter_connections,
61
62
  )
62
63
  from labcode.idgen import DEFAULT_ID_GENERATOR, IdGenerator
@@ -245,8 +246,7 @@ def make_transport_resolver(environment: dict) -> Callable:
245
246
  code = _flavored(
246
247
  code, script,
247
248
  _transport_machines(transport, transporters, devices, script),
248
- f"transport {transporter!r} "
249
- f"{transport.get('from')} -> {transport.get('to')}",
249
+ transport_label(transport),
250
250
  )
251
251
  routes[key] = code
252
252