labcode 0.1.2__tar.gz → 0.1.4__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.2 → labcode-0.1.4}/.github/workflows/ci.yml +14 -0
- labcode-0.1.4/MANIFEST.in +6 -0
- {labcode-0.1.2 → labcode-0.1.4}/PKG-INFO +4 -2
- labcode-0.1.4/SPECIFICATIONS.md +432 -0
- labcode-0.1.4/examples/README.md +227 -0
- labcode-0.1.4/examples/run_all_sila2_examples.py +84 -0
- labcode-0.1.4/examples/run_sila2_seal.py +300 -0
- labcode-0.1.4/examples/sila2_seal.boundary.yaml +19 -0
- labcode-0.1.4/examples/sila2_seal.env.yaml +130 -0
- labcode-0.1.4/examples/sila2_seal.workflow.yaml +60 -0
- labcode-0.1.4/examples/sila2_seal.wrapped.env.yaml +127 -0
- {labcode-0.1.2 → labcode-0.1.4}/labcode/backend.py +131 -18
- labcode-0.1.4/labcode/dialect.py +444 -0
- labcode-0.1.4/labcode/extension.py +353 -0
- labcode-0.1.4/labcode/probe.py +192 -0
- {labcode-0.1.2 → labcode-0.1.4}/labcode/run_cli.py +63 -7
- {labcode-0.1.2 → labcode-0.1.4}/labcode/runner.py +46 -9
- labcode-0.1.4/labcode/sila2.py +153 -0
- {labcode-0.1.2 → labcode-0.1.4}/labcode.egg-info/PKG-INFO +4 -2
- {labcode-0.1.2 → labcode-0.1.4}/labcode.egg-info/SOURCES.txt +14 -0
- {labcode-0.1.2 → labcode-0.1.4}/labcode.egg-info/requires.txt +4 -1
- {labcode-0.1.2 → labcode-0.1.4}/labcode.egg-info/scm_file_list.json +16 -1
- labcode-0.1.4/labcode.egg-info/scm_version.json +8 -0
- {labcode-0.1.2 → labcode-0.1.4}/pyproject.toml +12 -7
- labcode-0.1.4/tests/fixtures/reroute_device.env.yaml +60 -0
- labcode-0.1.4/tests/fixtures/reroute_transporter.env.yaml +57 -0
- {labcode-0.1.2 → labcode-0.1.4}/tests/test_backend.py +63 -0
- labcode-0.1.4/tests/test_dialect.py +501 -0
- labcode-0.1.4/tests/test_probe.py +438 -0
- {labcode-0.1.2 → labcode-0.1.4}/tests/test_run_cli.py +65 -0
- labcode-0.1.4/tests/test_sila2.py +201 -0
- labcode-0.1.2/SPECIFICATIONS.md +0 -188
- labcode-0.1.2/examples/README.md +0 -85
- labcode-0.1.2/labcode/dialect.py +0 -151
- labcode-0.1.2/labcode.egg-info/scm_version.json +0 -8
- labcode-0.1.2/tests/test_dialect.py +0 -104
- {labcode-0.1.2 → labcode-0.1.4}/.github/workflows/publish.yml +0 -0
- {labcode-0.1.2 → labcode-0.1.4}/.gitignore +0 -0
- {labcode-0.1.2 → labcode-0.1.4}/LICENSE +0 -0
- {labcode-0.1.2 → labcode-0.1.4}/README.md +0 -0
- {labcode-0.1.2 → labcode-0.1.4}/examples/outputs/plate_line.boundary.yaml +0 -0
- {labcode-0.1.2 → labcode-0.1.4}/examples/outputs/plate_line.observation.yaml +0 -0
- {labcode-0.1.2 → labcode-0.1.4}/examples/outputs/plate_line.plan.yaml +0 -0
- {labcode-0.1.2 → labcode-0.1.4}/examples/outputs/plate_line.svg +0 -0
- {labcode-0.1.2 → labcode-0.1.4}/examples/plate_line.boundary.yaml +0 -0
- {labcode-0.1.2 → labcode-0.1.4}/examples/plate_line.env.yaml +0 -0
- {labcode-0.1.2 → labcode-0.1.4}/examples/plate_line.workflow.yaml +0 -0
- {labcode-0.1.2 → labcode-0.1.4}/examples/render_plate_line.py +0 -0
- {labcode-0.1.2 → labcode-0.1.4}/labcode/__init__.py +0 -0
- {labcode-0.1.2 → labcode-0.1.4}/labcode/__main__.py +0 -0
- {labcode-0.1.2 → labcode-0.1.4}/labcode/_child.py +0 -0
- {labcode-0.1.2 → labcode-0.1.4}/labcode/cli.py +0 -0
- {labcode-0.1.2 → labcode-0.1.4}/labcode/idgen.py +0 -0
- {labcode-0.1.2 → labcode-0.1.4}/labcode/objectid.py +0 -0
- {labcode-0.1.2 → labcode-0.1.4}/labcode/py.typed +0 -0
- {labcode-0.1.2 → labcode-0.1.4}/labcode.egg-info/dependency_links.txt +0 -0
- {labcode-0.1.2 → labcode-0.1.4}/labcode.egg-info/entry_points.txt +0 -0
- {labcode-0.1.2 → labcode-0.1.4}/labcode.egg-info/top_level.txt +0 -0
- {labcode-0.1.2 → labcode-0.1.4}/setup.cfg +0 -0
- {labcode-0.1.2 → labcode-0.1.4}/tests/fixtures/device_script.env.yaml +0 -0
- {labcode-0.1.2 → labcode-0.1.4}/tests/fixtures/device_script.workflow.yaml +0 -0
- {labcode-0.1.2 → labcode-0.1.4}/tests/fixtures/transport.env.yaml +0 -0
- {labcode-0.1.2 → labcode-0.1.4}/tests/fixtures/transport.workflow.yaml +0 -0
- {labcode-0.1.2 → labcode-0.1.4}/tests/test_cli.py +0 -0
- {labcode-0.1.2 → labcode-0.1.4}/tests/test_objectid.py +0 -0
|
@@ -43,3 +43,17 @@ jobs:
|
|
|
43
43
|
cache: pip
|
|
44
44
|
- run: pip install -e ".[dev]"
|
|
45
45
|
- run: mypy
|
|
46
|
+
|
|
47
|
+
# `uv sync --extra sila2` is how a development environment gets built here, and
|
|
48
|
+
# uv.lock records what it produces. This asserts the lock still agrees with
|
|
49
|
+
# pyproject.toml, so a raised dependency floor or a new extra cannot leave the
|
|
50
|
+
# environment pinned to versions the project no longer accepts. It deliberately
|
|
51
|
+
# does not ask whether newer releases exist -- that is `uv lock --upgrade`, run
|
|
52
|
+
# when someone means to move. The jobs above stay on pip so they keep testing
|
|
53
|
+
# against the latest released siblings.
|
|
54
|
+
lock:
|
|
55
|
+
runs-on: ubuntu-latest
|
|
56
|
+
steps:
|
|
57
|
+
- uses: actions/checkout@v4
|
|
58
|
+
- uses: astral-sh/setup-uv@v7
|
|
59
|
+
- run: uv lock --check
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# setuptools-scm hands the sdist every file git tracks, and uv.lock is tracked so
|
|
2
|
+
# that the versions a developer actually gets stay reviewable. That lock describes
|
|
3
|
+
# the development environment only -- an installer resolves the wheel's dependency
|
|
4
|
+
# metadata and never reads it -- so it has no business in the distribution, where
|
|
5
|
+
# it would roughly double the sdist. Excluded here; still in the repository.
|
|
6
|
+
exclude uv.lock
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: labcode
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
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
|
|
@@ -22,13 +22,15 @@ Description-Content-Type: text/markdown
|
|
|
22
22
|
License-File: LICENSE
|
|
23
23
|
Requires-Dist: ofplang-validate>=0.1.3
|
|
24
24
|
Requires-Dist: ofplang-schedule>=0.1.3
|
|
25
|
-
Requires-Dist: ofplang-run>=0.1.
|
|
25
|
+
Requires-Dist: ofplang-run>=0.1.11
|
|
26
26
|
Provides-Extra: test
|
|
27
27
|
Requires-Dist: pytest>=7.0; extra == "test"
|
|
28
28
|
Provides-Extra: dev
|
|
29
29
|
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
30
30
|
Requires-Dist: ruff>=0.16; extra == "dev"
|
|
31
31
|
Requires-Dist: mypy>=1.11; extra == "dev"
|
|
32
|
+
Provides-Extra: sila2
|
|
33
|
+
Requires-Dist: sila2>=0.14; extra == "sila2"
|
|
32
34
|
Dynamic: license-file
|
|
33
35
|
|
|
34
36
|
# labcode
|
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
# labcode dialect specification
|
|
2
|
+
|
|
3
|
+
labcode is a dialect of the Object-Flow Programming Language (ofplang). A labcode
|
|
4
|
+
workflow **is** a portable v0 ofplang workflow; the dialect lives entirely in the
|
|
5
|
+
**execution environment** (§5), as an `x-labcode` extension that says *how* each device
|
|
6
|
+
operation is physically carried out. `lc run` drives the workflow on the labcode backend
|
|
7
|
+
(`ofplang.run.SubprocessBackend`), sourcing each operation's script from `x-labcode` and
|
|
8
|
+
running it out-of-process on a wall clock.
|
|
9
|
+
|
|
10
|
+
This document is the reference for the `x-labcode` extension; `labcode.dialect` is its
|
|
11
|
+
conformance validator, run at the `lc run` front door.
|
|
12
|
+
|
|
13
|
+
## 1. `x-labcode` in an environment (P5)
|
|
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.
|
|
19
|
+
|
|
20
|
+
An environment process mode (§5) may carry an `x-labcode` mapping holding a `script`: the
|
|
21
|
+
Python that carries out that `(process, mode)`.
|
|
22
|
+
|
|
23
|
+
```yaml
|
|
24
|
+
processes:
|
|
25
|
+
measure_od:
|
|
26
|
+
modes:
|
|
27
|
+
- id: v0
|
|
28
|
+
devices: [reader]
|
|
29
|
+
duration: 45 # the scheduler's estimate; real time is the script's own
|
|
30
|
+
x-labcode:
|
|
31
|
+
script:
|
|
32
|
+
language: python # the only supported language
|
|
33
|
+
code: |
|
|
34
|
+
return {"od": read_plate(plate)}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`x-labcode` is tolerated (and ignored) by `ofplang-schedule` (>= 0.1.2): the environment
|
|
38
|
+
still validates and schedules as plain v0. Only labcode interprets it.
|
|
39
|
+
|
|
40
|
+
### 1.1 Shape
|
|
41
|
+
|
|
42
|
+
- `x-labcode` MUST be a mapping. On a process mode or a transport route its only key is
|
|
43
|
+
`script`.
|
|
44
|
+
- `x-labcode.script`, if present, MUST be a mapping with:
|
|
45
|
+
- `language`: MUST be `python`.
|
|
46
|
+
- `code`: MUST be a string (an implementation-provided Python function body).
|
|
47
|
+
- `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
|
|
49
|
+
commands alone and labcode supplies the clients. `raw` is the whole function body,
|
|
50
|
+
written by its author — the general escape hatch, and what a script that connects for
|
|
51
|
+
itself (or speaks something other than SiLA2) uses.
|
|
52
|
+
|
|
53
|
+
**Unknown keys are an error** — in `x-labcode` at every position, and in the mappings it
|
|
54
|
+
holds. A key this version does not know is either a typo or a feature it does not have;
|
|
55
|
+
either way, ignoring it would mean a document that says one thing and a run that does
|
|
56
|
+
another (a misspelled `flavour:` running unwrapped, a `probe:` on a mode monitoring
|
|
57
|
+
nothing). This applies only inside `x-labcode` — the workflow's own `script` (v0 §22)
|
|
58
|
+
belongs to `ofplang-validate`.
|
|
59
|
+
|
|
60
|
+
### 1.2 Calling convention (process)
|
|
61
|
+
|
|
62
|
+
The script runs as the body of a function whose parameters are the operation's **input
|
|
63
|
+
port names**, each bound to that port's view value (Pure Data or an Object's view record)
|
|
64
|
+
— as in a v0 §22 `python_script_processes` script. External `import` is allowed; there is
|
|
65
|
+
no sandbox.
|
|
66
|
+
|
|
67
|
+
**Partial outputs.** Unlike v0 §22.2 (which requires the script to return *every* output
|
|
68
|
+
exactly), a labcode process script `return`s only the outputs it **computes** — a subset.
|
|
69
|
+
The backend fills the rest:
|
|
70
|
+
|
|
71
|
+
- an Object output declared in `objects.map` is **carried from its input** (the same
|
|
72
|
+
Object, its view unchanged) — so a pass-through need not restate it;
|
|
73
|
+
- any other unset output gets a **typed default** for its type.
|
|
74
|
+
|
|
75
|
+
The script's returned values override these. Each returned value must conform to its port's
|
|
76
|
+
type, and **returning a name that is not a declared output is an error** (this catches a
|
|
77
|
+
typo'd output name). A §22.2-strict script — one that returns every output explicitly —
|
|
78
|
+
works unchanged.
|
|
79
|
+
|
|
80
|
+
So for `read` (input `plate`, outputs `plate` via `objects.map` + `od`), all three are
|
|
81
|
+
equivalent to returning `{"plate": plate, "od": 0.42}`… except the defaulted forms:
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
return {"plate": plate, "od": 0.42} # explicit
|
|
85
|
+
return {"od": 0.42} # plate carried by objects.map
|
|
86
|
+
return {} # plate carried; od defaults to 0.0
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### 1.3 `x-labcode` on a transport route
|
|
90
|
+
|
|
91
|
+
An environment `transports[]` route may carry an `x-labcode` with a `script`: the Python
|
|
92
|
+
that physically carries out that move (e.g. commanding a robot arm). Same shape as §1.1
|
|
93
|
+
(`language: python`, string `code`).
|
|
94
|
+
|
|
95
|
+
```yaml
|
|
96
|
+
transports:
|
|
97
|
+
- transporter: arm
|
|
98
|
+
from: reader.stage
|
|
99
|
+
to: sealer.stage
|
|
100
|
+
duration: 3
|
|
101
|
+
x-labcode:
|
|
102
|
+
script:
|
|
103
|
+
language: python
|
|
104
|
+
code: |
|
|
105
|
+
grip = "gentle" if (view or {}).get("fragile") else "firm"
|
|
106
|
+
move_plate(from_spot, to_spot, grip=grip)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
**Calling convention (transport).** The script runs as a function body with these locals:
|
|
110
|
+
`from_spot`, `to_spot`, `transporter` (the physical route), and `view` — the view value of
|
|
111
|
+
the moved Object. `view` is **best-effort and MAY be `None`** (the runner resolves it from
|
|
112
|
+
the producing arc; when it cannot, it is `None`), so a script that reads it should tolerate
|
|
113
|
+
`None`. A transport script is **side-effect only**: its return value is ignored and no
|
|
114
|
+
output is verified. Success is "it ran without raising"; an exception is a graceful failure
|
|
115
|
+
(the move ends `failed`, no material is moved — the run stops).
|
|
116
|
+
|
|
117
|
+
A route with no `x-labcode.script` runs as a plain timed move — the runner's material
|
|
118
|
+
bookkeeping only, with no device command (a warned no-op for a real move, from != to).
|
|
119
|
+
|
|
120
|
+
### 1.4 `x-labcode` on a device or a transporter
|
|
121
|
+
|
|
122
|
+
An environment `devices[]` or `transporters[]` entry may carry an `x-labcode` with two
|
|
123
|
+
keys: `connection` — **where that machine is**, written once per physical machine rather
|
|
124
|
+
than repeated in every script that drives it — and `probe` (§1.5) — whether to check that
|
|
125
|
+
it still answers.
|
|
126
|
+
|
|
127
|
+
```yaml
|
|
128
|
+
devices:
|
|
129
|
+
- id: plateloc
|
|
130
|
+
spots: [stage]
|
|
131
|
+
x-labcode:
|
|
132
|
+
connection: { kind: sila2, host: 127.0.0.1, port: 50053, insecure: true }
|
|
133
|
+
|
|
134
|
+
transporters:
|
|
135
|
+
- id: arm
|
|
136
|
+
x-labcode:
|
|
137
|
+
connection: { kind: sila2, host: 127.0.0.1, port: 50057, insecure: true }
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
| field | required | default | meaning |
|
|
141
|
+
|---|---|---|---|
|
|
142
|
+
| `kind` | no | `sila2` | the protocol; `sila2` is the only value this version defines |
|
|
143
|
+
| `host` | **yes** | — | a non-empty string |
|
|
144
|
+
| `port` | **yes** | — | an integer in 1..65535 |
|
|
145
|
+
| `insecure` | no | `false` | connect without TLS |
|
|
146
|
+
|
|
147
|
+
**TLS is not supported in this version.** There is nowhere in the schema to put the
|
|
148
|
+
credentials it needs (a root certificate, at least), so a `connection` whose effective
|
|
149
|
+
`insecure` is false is rejected at the front door — including one that simply omits the
|
|
150
|
+
key and takes the default — and refused again if a script reaches the connect helper
|
|
151
|
+
directly. Every connection must therefore say `insecure: true` today.
|
|
152
|
+
The default stays `false` so that supporting TLS later is a pure addition: new fields,
|
|
153
|
+
and the error goes away. The check applies to every declared `connection`, whether or not
|
|
154
|
+
a script uses it.
|
|
155
|
+
|
|
156
|
+
**A `sila2` script needs somewhere to connect** (checked at the front door):
|
|
157
|
+
|
|
158
|
+
- a mode script with `flavor: sila2` requires **at least one** of that mode's `devices[]`
|
|
159
|
+
to declare a `connection`;
|
|
160
|
+
- a transport script with `flavor: sila2` requires that route's `transporter` to declare
|
|
161
|
+
one.
|
|
162
|
+
|
|
163
|
+
Declaring a `connection` on a device no script connects to is allowed — it is how an
|
|
164
|
+
environment is prepared before the scripts that use it are written.
|
|
165
|
+
|
|
166
|
+
### 1.5 Availability — `probe`
|
|
167
|
+
|
|
168
|
+
A machine that stops answering should not keep receiving work. A `probe` policy asks labcode
|
|
169
|
+
to check the machines it knows how to reach, and to tell the scheduler about the ones it
|
|
170
|
+
cannot: their process modes, and the transports they carry or touch, are dropped from the
|
|
171
|
+
environment the scheduler sees, so the run **routes around them**.
|
|
172
|
+
|
|
173
|
+
`probe` may be written on a device or a transporter, and at the **environment root** as a
|
|
174
|
+
document-wide default. The root's fields sit under a machine's own, **field by field**, so
|
|
175
|
+
a machine can change one thing without restating the rest.
|
|
176
|
+
|
|
177
|
+
| field | default | meaning |
|
|
178
|
+
|---|---|---|
|
|
179
|
+
| `enabled` | `false` | whether this machine is probed at all |
|
|
180
|
+
| `timeout` | `5` | how long one check may take, in **real seconds** |
|
|
181
|
+
| `interval` | `once` | `once` (check at the start of the run and keep that answer), a number of **real seconds** to re-check on, or `0` to re-check on every replan |
|
|
182
|
+
|
|
183
|
+
`timeout` and `interval` are real seconds — probing is work done against the real world, so
|
|
184
|
+
it has nothing to do with the environment's time unit or the run's wall-clock pacing.
|
|
185
|
+
|
|
186
|
+
**Writing a policy does not enable it.** `enabled` defaults to false wherever it is not
|
|
187
|
+
said, so adding an `interval` to an environment cannot start probing something that was not
|
|
188
|
+
being probed before; an environment with no `probe` at all behaves exactly as it did before
|
|
189
|
+
this version. A policy that nothing enables is a **warning** — it does nothing, which is
|
|
190
|
+
unlikely to be what its author meant.
|
|
191
|
+
|
|
192
|
+
**A probed machine needs an address.** A machine whose effective policy is enabled must
|
|
193
|
+
declare a `connection` — otherwise there is nothing to probe, and that is an error. This
|
|
194
|
+
matters when enabling probing document-wide, because the root reaches *every* machine: a
|
|
195
|
+
plain holding device with no connection then has to be excluded on purpose.
|
|
196
|
+
|
|
197
|
+
```yaml
|
|
198
|
+
# Per machine: enable only the ones with an address (nothing to write for a holding device)
|
|
199
|
+
devices:
|
|
200
|
+
- id: plateloc
|
|
201
|
+
spots: [stage]
|
|
202
|
+
x-labcode:
|
|
203
|
+
connection: { kind: sila2, host: 127.0.0.1, port: 50053, insecure: true }
|
|
204
|
+
probe: { enabled: true, interval: 60 }
|
|
205
|
+
- { id: station, spots: [slot1] }
|
|
206
|
+
|
|
207
|
+
# Document-wide: enable once, and exclude what cannot be reached
|
|
208
|
+
x-labcode:
|
|
209
|
+
probe: { enabled: true, interval: 60 }
|
|
210
|
+
devices:
|
|
211
|
+
- id: plateloc
|
|
212
|
+
spots: [stage]
|
|
213
|
+
x-labcode:
|
|
214
|
+
connection: { kind: sila2, host: 127.0.0.1, port: 50053, insecure: true }
|
|
215
|
+
- id: station
|
|
216
|
+
spots: [slot1]
|
|
217
|
+
x-labcode:
|
|
218
|
+
probe: { enabled: false }
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
**What a probe is.** Opening a TCP connection to the declared address, and nothing more. It
|
|
222
|
+
needs no client library, and it establishes **reachability, not readiness**: a machine whose
|
|
223
|
+
port is open but whose software is wedged reads as up here, and that case surfaces where it
|
|
224
|
+
belongs — as the operation that tried to command it failing.
|
|
225
|
+
|
|
226
|
+
**What it does to a run.**
|
|
227
|
+
|
|
228
|
+
- Only **new scheduling** is affected. An operation already running on a machine that has
|
|
229
|
+
just gone down is not touched.
|
|
230
|
+
- **If there is no other way, the run fails.** A workflow that needs a machine nothing can
|
|
231
|
+
replace stops with the scheduler's "no route" error rather than dispatching onto it. That
|
|
232
|
+
error names an arc or a mode, not a machine, so `lc run` appends the machines the probe
|
|
233
|
+
found unreachable — otherwise the answer to "why is there no route" is not in the message.
|
|
234
|
+
- **Recovery is automatic** — for a policy that re-checks. With `once` (the default) the
|
|
235
|
+
first answer stands for the whole run; with an `interval`, a machine that comes back
|
|
236
|
+
returns to the plan.
|
|
237
|
+
- **A check costs run-loop time**, and that cost is subject to §3.1 below: probing happens
|
|
238
|
+
in the process driving the run, one machine at a time, on the replan that asks for it.
|
|
239
|
+
This is the loop's most expensive optional step, so it is the likeliest thing to make a
|
|
240
|
+
cycle outgrow its poll period — which is why `interval: 0` is a setting for a diagnosis
|
|
241
|
+
rather than for operating a lab.
|
|
242
|
+
- **What costs is the machine that is *not* answering.** A reachable machine answers in well
|
|
243
|
+
under a millisecond on a local network. An unreachable one is only cheap when something
|
|
244
|
+
actively refuses the connection; a machine that was switched off, that left the network, or
|
|
245
|
+
that sits behind a host holding the port open while nothing serves it takes up to its
|
|
246
|
+
`timeout` to read as down. The cost of a round therefore follows the machines that are
|
|
247
|
+
down, not the ones that are up — so the round to size the poll period against (§3.1) is
|
|
248
|
+
the one in which the most of them are.
|
|
249
|
+
- `lc run --no-probe` ignores the policies and treats every machine as reachable (the
|
|
250
|
+
document is still validated, so an environment that is wrong about probing stays wrong).
|
|
251
|
+
Each machine whose reachability changes is reported on stderr.
|
|
252
|
+
|
|
253
|
+
### 1.6 Calling convention (`flavor: sila2`)
|
|
254
|
+
|
|
255
|
+
A `sila2` script is the **commands alone**: labcode opens a client to each of the
|
|
256
|
+
operation's machines, runs the code with them in scope, and closes them afterwards. On top
|
|
257
|
+
of the input ports of §1.2 (or the transport locals of §1.3), the code sees:
|
|
258
|
+
|
|
259
|
+
| name | meaning |
|
|
260
|
+
|---|---|
|
|
261
|
+
| `sila2_clients` | the clients by **device id** (`transporter id` for a transport), in `devices[]` order, holding **only** the machines that declared a `connection` |
|
|
262
|
+
| `sila2_client` | the first of them — the one name a single-machine operation needs |
|
|
263
|
+
|
|
264
|
+
```yaml
|
|
265
|
+
x-labcode:
|
|
266
|
+
script:
|
|
267
|
+
language: python
|
|
268
|
+
flavor: sila2
|
|
269
|
+
code: |
|
|
270
|
+
# `sila2_client` is already connected to this mode's device.
|
|
271
|
+
return {"od": sila2_client.OpticalDensityProvider.MeasureOD().OD}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
- **These two names are reserved.** A script's inputs are bound as its function's
|
|
275
|
+
parameters, so an input port of the same name would be silently overwritten by a client;
|
|
276
|
+
a process that declares one is rejected at the front door (as a `_id` view field is,
|
|
277
|
+
§4.1).
|
|
278
|
+
- **One connection per operation**, opened before the code runs and closed after it — on
|
|
279
|
+
any exit, including a `return` or an exception, and including a *later* connection
|
|
280
|
+
failing after an earlier one opened. There is no pooling and no reconnection: reaching an
|
|
281
|
+
instrument is assumed, and failing to is an ordinary operation failure naming the machine.
|
|
282
|
+
- **Everything else is still the script's own.** The flavor supplies connections, nothing
|
|
283
|
+
more: waiting for an observable command to finish (the standard `sila2` polling pattern)
|
|
284
|
+
belongs in the code, as it does in a `raw` script.
|
|
285
|
+
- A `sila2` script is only interpreted where the dialect is — in an environment
|
|
286
|
+
`x-labcode`. A workflow's own `script` (v0 §22) has no `flavor`.
|
|
287
|
+
|
|
288
|
+
### 1.7 Where an `x-labcode` may appear
|
|
289
|
+
|
|
290
|
+
The positions of §1 are the only ones: the environment **root** (`probe` defaults only),
|
|
291
|
+
`processes.<p>.modes[]`, `transports[]`, `devices[]` and `transporters[]`. An `x-labcode`
|
|
292
|
+
anywhere else in the environment — on a process, beside `time` — is an **error**, as is a
|
|
293
|
+
key at a position that does not define it (a `connection` at the root, a `probe` on a mode).
|
|
294
|
+
Nothing would read it, and `ofplang-schedule` tolerates an `x-` key at *every* position
|
|
295
|
+
without interpreting it, so a misplaced block would otherwise stay silent forever.
|
|
296
|
+
|
|
297
|
+
This rule covers the environment only. An `x-labcode` in the **workflow** is not reported:
|
|
298
|
+
that document is portable v0, read by other implementations, and what extension keys it
|
|
299
|
+
carries is not labcode's business.
|
|
300
|
+
|
|
301
|
+
## 2. Code source resolution and exclusivity
|
|
302
|
+
|
|
303
|
+
For a dispatched `(process, mode)`, labcode resolves the code to run in this order:
|
|
304
|
+
|
|
305
|
+
1. the mode's `x-labcode.script.code` (2 — the labcode device script), else
|
|
306
|
+
2. the workflow process's own `script.code` (1 — a v0 §22 script process), else
|
|
307
|
+
3. none — the operation runs as a **typed-default no-op** (its outputs are typed
|
|
308
|
+
defaults; a device not yet scripted).
|
|
309
|
+
|
|
310
|
+
**Exclusivity (error).** A process MUST NOT carry both a workflow `script` (1) and an env
|
|
311
|
+
`x-labcode.script` (2) on any of its modes; that is ambiguous and is rejected.
|
|
312
|
+
|
|
313
|
+
**Typed-default reachability (warning).** A process with neither (1) nor (2) on any mode
|
|
314
|
+
will run as a typed-default no-op. This is allowed — convenient while mocking a device —
|
|
315
|
+
but `lc run` warns about it, so an unimplemented device is not silently a no-op.
|
|
316
|
+
|
|
317
|
+
## 3. Execution model
|
|
318
|
+
|
|
319
|
+
Each dispatched operation runs in its own child process (real, wall-clock-paced); the
|
|
320
|
+
runner discovers completion by polling, so a multi-minute computation never blocks it.
|
|
321
|
+
The advisory `duration` is the scheduler's estimate; the real duration is the script's.
|
|
322
|
+
A script error (an exception, a wrong/ missing output name, a non-conformant value) is a
|
|
323
|
+
graceful runtime failure (§22.2): the operation ends `failed` and the run stops.
|
|
324
|
+
|
|
325
|
+
Cadence: the nominal poll period is `poll_interval × seconds_per_tick`. labcode defaults
|
|
326
|
+
`seconds_per_tick` to ~20 s (so a real op is polled at an observable cadence, not
|
|
327
|
+
sub-second, which would flood the replan loop); `lc run --seconds-per-tick/--speed/
|
|
328
|
+
--poll-interval/--margin` override it.
|
|
329
|
+
|
|
330
|
+
**The running-task margin defaults to the poll interval.** When the scheduler replans, a
|
|
331
|
+
still-running operation is pinned to end at `max(reported end, now + margin)` — the margin
|
|
332
|
+
is how far ahead of *now* an operation that has not finished is assumed to run for. A
|
|
333
|
+
positive margin is therefore what keeps that operation's successor from being planned at
|
|
334
|
+
`now` and dispatched onto a resource it has not released; and a real operation overruns its
|
|
335
|
+
estimate as a matter of course, so labcode defaults the margin to one poll interval rather
|
|
336
|
+
than to 0. Note that this does not depend on the cadence holding (§3.1): the pin moves with
|
|
337
|
+
`now`, so skipped ticks cannot erode the protection.
|
|
338
|
+
|
|
339
|
+
### 3.1 A poll cycle has to fit its poll period
|
|
340
|
+
|
|
341
|
+
One turn of the loop costs the driving process real time: replanning, dispatching, and
|
|
342
|
+
whatever else the dialect does before it waits again. Call that the **cycle cost** and the
|
|
343
|
+
nominal poll period the **budget**. The relation between them decides how the run behaves,
|
|
344
|
+
and there is no third case:
|
|
345
|
+
|
|
346
|
+
- **cost < budget** — the loop waits out the difference, so a turn takes exactly the budget
|
|
347
|
+
and the clock lands on the next tick. The cost is *absorbed*: the run keeps the cadence it
|
|
348
|
+
was asked for and each operation's recorded duration reflects the lab. This is the case
|
|
349
|
+
the defaults are chosen for, and the case a lab should run in.
|
|
350
|
+
- **cost > budget** — there is nothing left to wait for. The loop stops waiting, the ticks
|
|
351
|
+
it could not observe are **skipped**, and the clock jumps to the tick real time has
|
|
352
|
+
reached. Nothing is falsified by that: the clock still tells real time, and the lab really
|
|
353
|
+
did keep running while the loop was busy. But the *effective* period becomes the cycle
|
|
354
|
+
cost, so `poll_interval` and `seconds_per_tick` no longer set the cadence, and every
|
|
355
|
+
operation's recorded duration is rounded up to that coarser grid — a fast operation can be
|
|
356
|
+
recorded as having taken a whole cycle. **`lc run` reports the first slip** (how long the
|
|
357
|
+
cycle took, what the period was, how many ticks went unobserved), because the fix is a
|
|
358
|
+
setting only the caller can change.
|
|
359
|
+
|
|
360
|
+
The report comes from the wait, so a cycle that never reaches it says nothing. When the
|
|
361
|
+
replan at the top of a cycle fails outright — there is no route, because the work needs a
|
|
362
|
+
machine that is gone — the run ends there, and no slip is reported however long that cycle
|
|
363
|
+
took. The absence of the message means the loop never got as far as waiting; it is not a
|
|
364
|
+
statement that the cycle was cheap.
|
|
365
|
+
|
|
366
|
+
So: keep the budget comfortably larger than the cycle cost. What the cycle costs is not
|
|
367
|
+
fixed — replanning grows with the workflow, and a dialect step such as availability probing
|
|
368
|
+
(§1.5) can add seconds — so the margin wants to be generous rather than exact. A run whose
|
|
369
|
+
recorded times matter (a checked-in example, a comparison against the plan's estimates)
|
|
370
|
+
needs this to hold; a run that only has to *complete* does not.
|
|
371
|
+
|
|
372
|
+
## 4. Object identity — the reserved `_id` view key
|
|
373
|
+
|
|
374
|
+
labcode gives every Object a stable, value-layer identity so it can be traced across
|
|
375
|
+
steps and in the observation document. The identity lives in the Object's **view** under
|
|
376
|
+
the reserved key **`_id`** (a `String`). This is a dialect feature layered on portable
|
|
377
|
+
v0: the workflow the user writes carries no `_id`; `lc run` injects and mints it.
|
|
378
|
+
|
|
379
|
+
### 4.1 Type rewrite
|
|
380
|
+
|
|
381
|
+
Before running, `lc run` rewrites the workflow in memory: it adds `_id: { type: String }`
|
|
382
|
+
to the `view` of **every `domain: object` type** (creating `view` if the type had none).
|
|
383
|
+
`_id` is an ordinary legal v0 view field (a leading-underscore identifier, not reserved
|
|
384
|
+
in core, and a primitive `String`), so the rewritten document validates and schedules
|
|
385
|
+
unchanged, and the runner's closed-shape view conformance treats `_id` as a normal
|
|
386
|
+
declared field. labcode runs this rewritten document directly (no temp file:
|
|
387
|
+
`ofplang.run.run_workflow` accepts an in-memory document).
|
|
388
|
+
|
|
389
|
+
**Reserved (error).** A user type that itself declares a `_id` view field is rejected at
|
|
390
|
+
the dialect front door — labcode owns `_id`, and silently clobbering the field would be
|
|
391
|
+
worse than a clear error.
|
|
392
|
+
|
|
393
|
+
### 4.2 Where an id comes from
|
|
394
|
+
|
|
395
|
+
An Object's `_id` is set at its two points of origin, then **carried** everywhere else —
|
|
396
|
+
`objects.map` and transport copy the whole view, so `_id` propagates for free:
|
|
397
|
+
|
|
398
|
+
- **`objects.create`** — a newly created Object's `_id` is minted when the operation
|
|
399
|
+
produces it (in the backend's output fill). A device script need not know about `_id`:
|
|
400
|
+
it returns only what it computes, and the fill supplies `_id` (like any other unset
|
|
401
|
+
output, §1.2).
|
|
402
|
+
- **run boundary** — a whole-workflow Object *input* enters at the boundary; `lc run`
|
|
403
|
+
mints its `_id` (filling any other declared view field with a typed default so the
|
|
404
|
+
seeded value conforms), **unless the boundary already carries one** — so a result
|
|
405
|
+
boundary fed back in round-trips its ids.
|
|
406
|
+
- **`objects.map`** — a mapped Object output carries its input's `_id` unchanged
|
|
407
|
+
(identity preserved), even if a §22.2-strict script returned the port explicitly.
|
|
408
|
+
|
|
409
|
+
### 4.3 Reproducibility
|
|
410
|
+
|
|
411
|
+
Ids come from a swappable generator (`labcode.idgen.IdGenerator`). The default
|
|
412
|
+
(`SeededUuid4Generator`) mints **reproducible** uuid4-shaped ids from a seed and a
|
|
413
|
+
*provenance key* — the node instance + output port for a create, the port name for a
|
|
414
|
+
boundary input — **not** draw order. So the same workflow yields the same ids on every
|
|
415
|
+
run, and the wall-clock backend's jittering completion order cannot change them (which is
|
|
416
|
+
what keeps checked-in example observations stable). A real run wanting globally-unique
|
|
417
|
+
ids per physical Object swaps in `RealUuid4Generator` (via
|
|
418
|
+
`labcode_backend_factory(id_generator=...)`).
|
|
419
|
+
|
|
420
|
+
> The provenance key is the runner's node-instance identity + port. Today each create
|
|
421
|
+
> node runs once, so node-path + port is unique; when dynamic control flow (e.g.
|
|
422
|
+
> `do_while`) is added, that node-instance identity must include the iteration index so
|
|
423
|
+
> ids stay unique and reproducible.
|
|
424
|
+
|
|
425
|
+
## 5. Not yet in this version (roadmap)
|
|
426
|
+
|
|
427
|
+
- **A deeper probe** — asking a machine something (a SiLA2 property read) rather than only
|
|
428
|
+
opening a connection to it, so "answering" can be checked and not just "listening"
|
|
429
|
+
(§1.5). It would be an opt-in depth, since it costs a real exchange per check.
|
|
430
|
+
- **Probing in parallel** — checking machines concurrently, so a lab with many unreachable
|
|
431
|
+
machines does not pay for them one timeout at a time (§1.5).
|
|
432
|
+
- **TLS** — the fields a secure connection needs, lifting the restriction in §1.4.
|