labcode 0.0.1__tar.gz → 0.1.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.0.1 → labcode-0.1.0}/PKG-INFO +25 -8
- {labcode-0.0.1 → labcode-0.1.0}/README.md +22 -5
- labcode-0.1.0/SPECIFICATIONS.md +135 -0
- labcode-0.1.0/examples/README.md +40 -0
- labcode-0.1.0/examples/plate_line.env.yaml +86 -0
- labcode-0.1.0/examples/plate_line.workflow.yaml +71 -0
- labcode-0.1.0/labcode/_child.py +62 -0
- labcode-0.1.0/labcode/backend.py +211 -0
- {labcode-0.0.1 → labcode-0.1.0}/labcode/cli.py +7 -5
- labcode-0.1.0/labcode/dialect.py +140 -0
- labcode-0.1.0/labcode/run_cli.py +186 -0
- {labcode-0.0.1 → labcode-0.1.0}/labcode.egg-info/PKG-INFO +25 -8
- {labcode-0.0.1 → labcode-0.1.0}/labcode.egg-info/SOURCES.txt +16 -1
- {labcode-0.0.1 → labcode-0.1.0}/labcode.egg-info/requires.txt +2 -2
- labcode-0.1.0/labcode.egg-info/scm_file_list.json +30 -0
- labcode-0.1.0/labcode.egg-info/scm_version.json +8 -0
- {labcode-0.0.1 → labcode-0.1.0}/pyproject.toml +8 -6
- labcode-0.1.0/tests/fixtures/device_script.env.yaml +28 -0
- labcode-0.1.0/tests/fixtures/device_script.workflow.yaml +26 -0
- labcode-0.1.0/tests/fixtures/transport.env.yaml +42 -0
- labcode-0.1.0/tests/fixtures/transport.workflow.yaml +41 -0
- labcode-0.1.0/tests/test_backend.py +196 -0
- labcode-0.1.0/tests/test_dialect.py +93 -0
- labcode-0.1.0/tests/test_run_cli.py +161 -0
- labcode-0.0.1/labcode.egg-info/scm_file_list.json +0 -15
- labcode-0.0.1/labcode.egg-info/scm_version.json +0 -8
- {labcode-0.0.1 → labcode-0.1.0}/.github/workflows/ci.yml +0 -0
- {labcode-0.0.1 → labcode-0.1.0}/.github/workflows/publish.yml +0 -0
- {labcode-0.0.1 → labcode-0.1.0}/.gitignore +0 -0
- {labcode-0.0.1 → labcode-0.1.0}/LICENSE +0 -0
- {labcode-0.0.1 → labcode-0.1.0}/labcode/__init__.py +0 -0
- {labcode-0.0.1 → labcode-0.1.0}/labcode/__main__.py +0 -0
- {labcode-0.0.1 → labcode-0.1.0}/labcode/py.typed +0 -0
- {labcode-0.0.1 → labcode-0.1.0}/labcode.egg-info/dependency_links.txt +0 -0
- {labcode-0.0.1 → labcode-0.1.0}/labcode.egg-info/entry_points.txt +0 -0
- {labcode-0.0.1 → labcode-0.1.0}/labcode.egg-info/top_level.txt +0 -0
- {labcode-0.0.1 → labcode-0.1.0}/setup.cfg +0 -0
- {labcode-0.0.1 → labcode-0.1.0}/tests/test_cli.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: labcode
|
|
3
|
-
Version: 0.0
|
|
3
|
+
Version: 0.1.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.0
|
|
24
|
-
Requires-Dist: ofplang-schedule>=0.1.
|
|
25
|
-
Requires-Dist: ofplang-run>=0.1.
|
|
24
|
+
Requires-Dist: ofplang-schedule>=0.1.2
|
|
25
|
+
Requires-Dist: ofplang-run>=0.1.6
|
|
26
26
|
Provides-Extra: test
|
|
27
27
|
Requires-Dist: pytest>=7.0; extra == "test"
|
|
28
28
|
Provides-Extra: dev
|
|
@@ -43,14 +43,31 @@ ofplang toolchain and exposes it under a single command:
|
|
|
43
43
|
```sh
|
|
44
44
|
lc validate ... # check a workflow is well-formed portable v0
|
|
45
45
|
lc schedule ... # compute a schedule for a workflow
|
|
46
|
-
lc run ... # execute a workflow
|
|
46
|
+
lc run ... # execute a workflow on the labcode backend
|
|
47
47
|
```
|
|
48
48
|
|
|
49
49
|
labcode is where a site-specific dialect and a custom runner (real lab hardware)
|
|
50
|
-
are developed on top of the ofplang toolchain.
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
50
|
+
are developed on top of the ofplang toolchain. `lc validate` and `lc schedule`
|
|
51
|
+
forward to the ofplang siblings unchanged; **`lc run` is the labcode dialect's own
|
|
52
|
+
runner**: it drives the workflow on the labcode backend, running each device
|
|
53
|
+
operation's script — supplied in the environment as an `x-labcode.script` extension
|
|
54
|
+
on a process mode — out-of-process on a wall clock, so a long-running real operation
|
|
55
|
+
never blocks the replan loop. See [`SPECIFICATIONS.md`](SPECIFICATIONS.md) for the
|
|
56
|
+
`x-labcode` extension.
|
|
57
|
+
|
|
58
|
+
```yaml
|
|
59
|
+
# in the execution environment: how a (process, mode) is carried out
|
|
60
|
+
processes:
|
|
61
|
+
measure_od:
|
|
62
|
+
modes:
|
|
63
|
+
- id: v0
|
|
64
|
+
duration: 45
|
|
65
|
+
x-labcode:
|
|
66
|
+
script:
|
|
67
|
+
language: python
|
|
68
|
+
code: |
|
|
69
|
+
return {"od": read_plate(plate)}
|
|
70
|
+
```
|
|
54
71
|
|
|
55
72
|
## Install
|
|
56
73
|
|
|
@@ -10,14 +10,31 @@ ofplang toolchain and exposes it under a single command:
|
|
|
10
10
|
```sh
|
|
11
11
|
lc validate ... # check a workflow is well-formed portable v0
|
|
12
12
|
lc schedule ... # compute a schedule for a workflow
|
|
13
|
-
lc run ... # execute a workflow
|
|
13
|
+
lc run ... # execute a workflow on the labcode backend
|
|
14
14
|
```
|
|
15
15
|
|
|
16
16
|
labcode is where a site-specific dialect and a custom runner (real lab hardware)
|
|
17
|
-
are developed on top of the ofplang toolchain.
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
are developed on top of the ofplang toolchain. `lc validate` and `lc schedule`
|
|
18
|
+
forward to the ofplang siblings unchanged; **`lc run` is the labcode dialect's own
|
|
19
|
+
runner**: it drives the workflow on the labcode backend, running each device
|
|
20
|
+
operation's script — supplied in the environment as an `x-labcode.script` extension
|
|
21
|
+
on a process mode — out-of-process on a wall clock, so a long-running real operation
|
|
22
|
+
never blocks the replan loop. See [`SPECIFICATIONS.md`](SPECIFICATIONS.md) for the
|
|
23
|
+
`x-labcode` extension.
|
|
24
|
+
|
|
25
|
+
```yaml
|
|
26
|
+
# in the execution environment: how a (process, mode) is carried out
|
|
27
|
+
processes:
|
|
28
|
+
measure_od:
|
|
29
|
+
modes:
|
|
30
|
+
- id: v0
|
|
31
|
+
duration: 45
|
|
32
|
+
x-labcode:
|
|
33
|
+
script:
|
|
34
|
+
language: python
|
|
35
|
+
code: |
|
|
36
|
+
return {"od": read_plate(plate)}
|
|
37
|
+
```
|
|
21
38
|
|
|
22
39
|
## Install
|
|
23
40
|
|
|
@@ -0,0 +1,135 @@
|
|
|
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` on a process mode (P5)
|
|
14
|
+
|
|
15
|
+
An environment process mode (§5) may carry an `x-labcode` mapping. In this version it
|
|
16
|
+
holds a single key, `script`: the Python that carries out that `(process, mode)`.
|
|
17
|
+
|
|
18
|
+
```yaml
|
|
19
|
+
processes:
|
|
20
|
+
measure_od:
|
|
21
|
+
modes:
|
|
22
|
+
- id: v0
|
|
23
|
+
devices: [reader]
|
|
24
|
+
duration: 45 # the scheduler's estimate; real time is the script's own
|
|
25
|
+
x-labcode:
|
|
26
|
+
script:
|
|
27
|
+
language: python # the only supported language
|
|
28
|
+
code: |
|
|
29
|
+
return {"od": read_plate(plate)}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
`x-labcode` is tolerated (and ignored) by `ofplang-schedule` (>= 0.1.2): the environment
|
|
33
|
+
still validates and schedules as plain v0. Only labcode interprets it.
|
|
34
|
+
|
|
35
|
+
### 1.1 Shape
|
|
36
|
+
|
|
37
|
+
- `x-labcode` MUST be a mapping.
|
|
38
|
+
- `x-labcode.script`, if present, MUST be a mapping with:
|
|
39
|
+
- `language`: MUST be `python`.
|
|
40
|
+
- `code`: MUST be a string (an implementation-provided Python function body).
|
|
41
|
+
|
|
42
|
+
### 1.2 Calling convention (process)
|
|
43
|
+
|
|
44
|
+
The script runs as the body of a function whose parameters are the operation's **input
|
|
45
|
+
port names**, each bound to that port's view value (Pure Data or an Object's view record)
|
|
46
|
+
— as in a v0 §22 `python_script_processes` script. External `import` is allowed; there is
|
|
47
|
+
no sandbox.
|
|
48
|
+
|
|
49
|
+
**Partial outputs.** Unlike v0 §22.2 (which requires the script to return *every* output
|
|
50
|
+
exactly), a labcode process script `return`s only the outputs it **computes** — a subset.
|
|
51
|
+
The backend fills the rest:
|
|
52
|
+
|
|
53
|
+
- an Object output declared in `objects.map` is **carried from its input** (the same
|
|
54
|
+
Object, its view unchanged) — so a pass-through need not restate it;
|
|
55
|
+
- any other unset output gets a **typed default** for its type.
|
|
56
|
+
|
|
57
|
+
The script's returned values override these. Each returned value must conform to its port's
|
|
58
|
+
type, and **returning a name that is not a declared output is an error** (this catches a
|
|
59
|
+
typo'd output name). A §22.2-strict script — one that returns every output explicitly —
|
|
60
|
+
works unchanged.
|
|
61
|
+
|
|
62
|
+
So for `read` (input `plate`, outputs `plate` via `objects.map` + `od`), all three are
|
|
63
|
+
equivalent to returning `{"plate": plate, "od": 0.42}`… except the defaulted forms:
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
return {"plate": plate, "od": 0.42} # explicit
|
|
67
|
+
return {"od": 0.42} # plate carried by objects.map
|
|
68
|
+
return {} # plate carried; od defaults to 0.0
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### 1.3 `x-labcode` on a transport route
|
|
72
|
+
|
|
73
|
+
An environment `transports[]` route may carry an `x-labcode` with a `script`: the Python
|
|
74
|
+
that physically carries out that move (e.g. commanding a robot arm). Same shape as §1.1
|
|
75
|
+
(`language: python`, string `code`).
|
|
76
|
+
|
|
77
|
+
```yaml
|
|
78
|
+
transports:
|
|
79
|
+
- transporter: arm
|
|
80
|
+
from: reader.stage
|
|
81
|
+
to: sealer.stage
|
|
82
|
+
duration: 3
|
|
83
|
+
x-labcode:
|
|
84
|
+
script:
|
|
85
|
+
language: python
|
|
86
|
+
code: |
|
|
87
|
+
grip = "gentle" if (view or {}).get("fragile") else "firm"
|
|
88
|
+
move_plate(from_spot, to_spot, grip=grip)
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
**Calling convention (transport).** The script runs as a function body with these locals:
|
|
92
|
+
`from_spot`, `to_spot`, `transporter` (the physical route), and `view` — the view value of
|
|
93
|
+
the moved Object. `view` is **best-effort and MAY be `None`** (the runner resolves it from
|
|
94
|
+
the producing arc; when it cannot, it is `None`), so a script that reads it should tolerate
|
|
95
|
+
`None`. A transport script is **side-effect only**: its return value is ignored and no
|
|
96
|
+
output is verified. Success is "it ran without raising"; an exception is a graceful failure
|
|
97
|
+
(the move ends `failed`, no material is moved — the run stops).
|
|
98
|
+
|
|
99
|
+
A route with no `x-labcode.script` runs as a plain timed move — the runner's material
|
|
100
|
+
bookkeeping only, with no device command (a warned no-op for a real move, from != to).
|
|
101
|
+
|
|
102
|
+
## 2. Code source resolution and exclusivity
|
|
103
|
+
|
|
104
|
+
For a dispatched `(process, mode)`, labcode resolves the code to run in this order:
|
|
105
|
+
|
|
106
|
+
1. the mode's `x-labcode.script.code` (2 — the labcode device script), else
|
|
107
|
+
2. the workflow process's own `script.code` (1 — a v0 §22 script process), else
|
|
108
|
+
3. none — the operation runs as a **typed-default no-op** (its outputs are typed
|
|
109
|
+
defaults; a device not yet scripted).
|
|
110
|
+
|
|
111
|
+
**Exclusivity (error).** A process MUST NOT carry both a workflow `script` (1) and an env
|
|
112
|
+
`x-labcode.script` (2) on any of its modes; that is ambiguous and is rejected.
|
|
113
|
+
|
|
114
|
+
**Typed-default reachability (warning).** A process with neither (1) nor (2) on any mode
|
|
115
|
+
will run as a typed-default no-op. This is allowed — convenient while mocking a device —
|
|
116
|
+
but `lc run` warns about it, so an unimplemented device is not silently a no-op.
|
|
117
|
+
|
|
118
|
+
## 3. Execution model
|
|
119
|
+
|
|
120
|
+
Each dispatched operation runs in its own child process (real, wall-clock-paced); the
|
|
121
|
+
runner discovers completion by polling, so a multi-minute computation never blocks it.
|
|
122
|
+
The advisory `duration` is the scheduler's estimate; the real duration is the script's.
|
|
123
|
+
A script error (an exception, a wrong/ missing output name, a non-conformant value) is a
|
|
124
|
+
graceful runtime failure (§22.2): the operation ends `failed` and the run stops.
|
|
125
|
+
|
|
126
|
+
Cadence: the effective poll period is `poll_interval × seconds_per_tick`. labcode defaults
|
|
127
|
+
`seconds_per_tick` to ~20 s (so a real op is polled at an observable cadence, not
|
|
128
|
+
sub-second, which would flood the replan loop); `lc run --seconds-per-tick/--speed/
|
|
129
|
+
--poll-interval/--margin` override it.
|
|
130
|
+
|
|
131
|
+
## 4. Not yet in this version (roadmap)
|
|
132
|
+
|
|
133
|
+
- **Device / transporter `x-labcode`** — connection and availability information
|
|
134
|
+
(e.g. SiLA2 address) consolidated on `devices[]` / `transporters[]`, used for a
|
|
135
|
+
connect/command/disconnect wrapper and for `down_devices` availability probing.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# labcode examples
|
|
2
|
+
|
|
3
|
+
## `plate_line` — an Object-bearing line, driven by environment scripts
|
|
4
|
+
|
|
5
|
+
A `Plate` flows down a three-station line and a measurement is read off it:
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
load ──[move]──> read ──[move]──> store
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
- [`plate_line.workflow.yaml`](plate_line.workflow.yaml) is **portable ofplang v0** — it
|
|
12
|
+
says only *what* happens. `read` is **Object-bearing**: the Plate goes in and the *same*
|
|
13
|
+
Plate comes out (`objects.map` — identity preserved), together with a Pure Data
|
|
14
|
+
measurement `od`. The Plate is created inside the workflow, so no run boundary is needed.
|
|
15
|
+
- [`plate_line.env.yaml`](plate_line.env.yaml) is the **labcode environment** — it says
|
|
16
|
+
*how* each step is carried out, as an `x-labcode.script` (see [`../SPECIFICATIONS.md`](../SPECIFICATIONS.md)).
|
|
17
|
+
**Every process mode and every transport route carries a script**: `load` makes the
|
|
18
|
+
Plate, `read` measures `od` (the Plate is carried through automatically by `objects.map`,
|
|
19
|
+
so the script returns only what it computes — labcode's *partial outputs*, see the spec),
|
|
20
|
+
`store` takes it; the transport scripts perform the move and may read the moved Plate's
|
|
21
|
+
`view` (its barcode).
|
|
22
|
+
|
|
23
|
+
labcode runs each script **out-of-process** on a wall clock, discovering completion by
|
|
24
|
+
polling — so a real, slow device operation never blocks the runner.
|
|
25
|
+
|
|
26
|
+
### Run it
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
lc run examples/plate_line.workflow.yaml --env examples/plate_line.env.yaml --seconds-per-tick 0.2
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
`--seconds-per-tick` sets the real seconds per environment time tick; it defaults to a
|
|
33
|
+
coarse value suited to real hardware, so a small value here keeps the demo quick. The run
|
|
34
|
+
completes with every activity `completed`; the produced measurement (`od = 0.42`) can be
|
|
35
|
+
written out with `--boundary-out result.yaml`.
|
|
36
|
+
|
|
37
|
+
> The scripts here are mocks (they just return values / reference their locals). Replace a
|
|
38
|
+
> script's body with real device calls — e.g. `robot.move(from_spot, to_spot)` in a
|
|
39
|
+
> transport, or an instrument read in `read` — to drive real hardware; the code may
|
|
40
|
+
> `import` anything the host Python can.
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# The labcode environment for plate_line.workflow.yaml: it says *how* each step and
|
|
2
|
+
# each move is carried out, as an `x-labcode.script` (labcode runs these out-of-process).
|
|
3
|
+
# The workflow stays portable v0 (`ofplang-schedule` just tolerates the x- keys).
|
|
4
|
+
#
|
|
5
|
+
# Every process mode AND every transport route carries a script here. The process
|
|
6
|
+
# scripts are Object-bearing: `load` returns the new Plate's view; `read` returns the
|
|
7
|
+
# *same* plate it was given plus the measured `od`; `store` takes it (side-effect only).
|
|
8
|
+
# The transport scripts move the plate and may read its view (the barcode).
|
|
9
|
+
time:
|
|
10
|
+
unit: second
|
|
11
|
+
|
|
12
|
+
devices:
|
|
13
|
+
- { id: loader, spots: [stage] }
|
|
14
|
+
- { id: reader, spots: [stage] }
|
|
15
|
+
- { id: sealer, spots: [stage] }
|
|
16
|
+
|
|
17
|
+
transporters:
|
|
18
|
+
- { id: arm }
|
|
19
|
+
|
|
20
|
+
transports:
|
|
21
|
+
- transporter: arm
|
|
22
|
+
from: loader.stage
|
|
23
|
+
to: reader.stage
|
|
24
|
+
duration: 2
|
|
25
|
+
x-labcode:
|
|
26
|
+
script:
|
|
27
|
+
language: python
|
|
28
|
+
code: |
|
|
29
|
+
# from_spot / to_spot / transporter are the physical route; `view` is the
|
|
30
|
+
# moved Plate's view (barcode). Side-effect only -- no return.
|
|
31
|
+
barcode = (view or {}).get("barcode")
|
|
32
|
+
# a real arm would move the plate here; e.g. robot.move(from_spot, to_spot)
|
|
33
|
+
|
|
34
|
+
- transporter: arm
|
|
35
|
+
from: reader.stage
|
|
36
|
+
to: sealer.stage
|
|
37
|
+
duration: 2
|
|
38
|
+
x-labcode:
|
|
39
|
+
script:
|
|
40
|
+
language: python
|
|
41
|
+
code: |
|
|
42
|
+
barcode = (view or {}).get("barcode")
|
|
43
|
+
|
|
44
|
+
processes:
|
|
45
|
+
|
|
46
|
+
load:
|
|
47
|
+
modes:
|
|
48
|
+
- devices: [loader]
|
|
49
|
+
duration: 3
|
|
50
|
+
output_spots: { plate: loader.stage }
|
|
51
|
+
x-labcode:
|
|
52
|
+
script:
|
|
53
|
+
language: python
|
|
54
|
+
code: |
|
|
55
|
+
# Create the Plate's view (its observable data). Returns the one output port.
|
|
56
|
+
return {"plate": {"barcode": "P001"}}
|
|
57
|
+
|
|
58
|
+
read:
|
|
59
|
+
modes:
|
|
60
|
+
- devices: [reader]
|
|
61
|
+
duration: 3
|
|
62
|
+
input_spots: { plate: reader.stage }
|
|
63
|
+
output_spots: { plate: reader.stage } # same spot: the plate is read in place
|
|
64
|
+
x-labcode:
|
|
65
|
+
script:
|
|
66
|
+
language: python
|
|
67
|
+
code: |
|
|
68
|
+
# Measure the optical density. `plate` is the incoming Plate's view; it is
|
|
69
|
+
# carried through automatically (objects.map), so the script returns only
|
|
70
|
+
# what it computes -- `od` (labcode's partial outputs, SPECIFICATIONS.md §1.2).
|
|
71
|
+
return {"od": 0.42}
|
|
72
|
+
|
|
73
|
+
store:
|
|
74
|
+
modes:
|
|
75
|
+
- devices: [sealer]
|
|
76
|
+
duration: 3
|
|
77
|
+
input_spots: { plate: sealer.stage }
|
|
78
|
+
x-labcode:
|
|
79
|
+
script:
|
|
80
|
+
language: python
|
|
81
|
+
code: |
|
|
82
|
+
# `plate` is the incoming Plate's view; storing is side-effect only.
|
|
83
|
+
return {}
|
|
84
|
+
|
|
85
|
+
objective:
|
|
86
|
+
kind: makespan
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# A small labcode example: a Plate flows down a three-station line and a measurement
|
|
2
|
+
# is read off it. Every step's *how* -- both the device operations and the moves --
|
|
3
|
+
# is supplied by the environment as an `x-labcode.script` (see plate_line.env.yaml);
|
|
4
|
+
# this file is portable ofplang v0 (it says only *what* happens).
|
|
5
|
+
#
|
|
6
|
+
# load ──[move]──> read ──[move]──> store
|
|
7
|
+
#
|
|
8
|
+
# `read` is Object-bearing: it takes the Plate in and passes the *same* Plate out
|
|
9
|
+
# (objects.map -- identity preserved), while also producing a Pure Data measurement
|
|
10
|
+
# `od`. So an Object and a data value flow together (cf. examples/plate_chain in
|
|
11
|
+
# ofplang-run). The Plate is created inside the workflow, so no run boundary is needed.
|
|
12
|
+
spec_version: "0.0"
|
|
13
|
+
|
|
14
|
+
types:
|
|
15
|
+
Plate:
|
|
16
|
+
domain: object
|
|
17
|
+
view:
|
|
18
|
+
barcode: { type: String }
|
|
19
|
+
|
|
20
|
+
processes:
|
|
21
|
+
|
|
22
|
+
# A source device: put a fresh Plate on the line.
|
|
23
|
+
load:
|
|
24
|
+
kind: atomic
|
|
25
|
+
outputs:
|
|
26
|
+
plate: { type: Plate, phase: data }
|
|
27
|
+
objects:
|
|
28
|
+
create: [outputs.plate]
|
|
29
|
+
|
|
30
|
+
# Object-bearing measurement: the Plate goes in and the *same* Plate comes out
|
|
31
|
+
# (objects.map), and its optical density `od` is measured as Pure Data.
|
|
32
|
+
read:
|
|
33
|
+
kind: atomic
|
|
34
|
+
inputs:
|
|
35
|
+
plate: { type: Plate, phase: data }
|
|
36
|
+
outputs:
|
|
37
|
+
plate: { type: Plate, phase: data } # same name as the input: the same plate
|
|
38
|
+
od: { type: Float, phase: data }
|
|
39
|
+
objects:
|
|
40
|
+
map:
|
|
41
|
+
outputs.plate: inputs.plate
|
|
42
|
+
|
|
43
|
+
# A sink device: take the Plate off the line.
|
|
44
|
+
store:
|
|
45
|
+
kind: atomic
|
|
46
|
+
inputs:
|
|
47
|
+
plate: { type: Plate, phase: data }
|
|
48
|
+
objects:
|
|
49
|
+
consume: [inputs.plate]
|
|
50
|
+
|
|
51
|
+
main:
|
|
52
|
+
kind: composite
|
|
53
|
+
inputs: {}
|
|
54
|
+
outputs:
|
|
55
|
+
od: { type: Float, phase: data }
|
|
56
|
+
body:
|
|
57
|
+
nodes:
|
|
58
|
+
- id: Load
|
|
59
|
+
process: load
|
|
60
|
+
- id: Read
|
|
61
|
+
process: read
|
|
62
|
+
state:
|
|
63
|
+
plate: { from: Load.plate } # Object arc (a move is scheduled for it)
|
|
64
|
+
- id: Store
|
|
65
|
+
process: store
|
|
66
|
+
state:
|
|
67
|
+
plate: { from: Read.plate }
|
|
68
|
+
returns:
|
|
69
|
+
od: { from: Read.od }
|
|
70
|
+
|
|
71
|
+
entry: main
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""labcode's out-of-process child harness (the partial-output dialect).
|
|
2
|
+
|
|
3
|
+
Like ``ofplang.run.simulator._child``, but labcode process scripts follow the labcode
|
|
4
|
+
dialect's **partial** output convention (SPECIFICATIONS.md §1.2): a script returns only the
|
|
5
|
+
outputs it computes, and the backend fills the rest -- carrying an Object output through
|
|
6
|
+
``objects.map`` (identity preserved) and defaulting the others. So this child does **no**
|
|
7
|
+
output verification; it just returns the script's raw result mapping. The merge, the
|
|
8
|
+
conformance check, and the rejection of any *extra* (undeclared) output name all happen in
|
|
9
|
+
`labcode.backend.LabcodeBackend`.
|
|
10
|
+
|
|
11
|
+
A transport script is side-effect only (its return is ignored). Protocol (JSON on stdin,
|
|
12
|
+
outcome to ``result_path``) is identical to the upstream child; only the process branch
|
|
13
|
+
differs (raw, unverified, vs verified-exactly)."""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
import sys
|
|
19
|
+
import traceback
|
|
20
|
+
|
|
21
|
+
from ofplang.run.simulator import DeviceComputationError, run_python_script
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def main() -> int:
|
|
25
|
+
try:
|
|
26
|
+
job = json.load(sys.stdin)
|
|
27
|
+
result_path = job["result_path"]
|
|
28
|
+
except Exception as exc: # cannot even read the job -- a harness-level failure
|
|
29
|
+
print(f"lc child: cannot read job: {exc}", file=sys.stderr)
|
|
30
|
+
traceback.print_exc()
|
|
31
|
+
return 1
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
raw = run_python_script(job.get("code") or "", job.get("inputs") or {})
|
|
35
|
+
if job.get("kind") == "transport":
|
|
36
|
+
payload: dict = {"outputs": {}} # side-effect only; the return is ignored
|
|
37
|
+
elif not isinstance(raw, dict):
|
|
38
|
+
payload = {"error": {
|
|
39
|
+
"code": "script_output_names",
|
|
40
|
+
"message": f"script process {job.get('process')!r} returned "
|
|
41
|
+
f"{type(raw).__name__}, not a mapping",
|
|
42
|
+
}}
|
|
43
|
+
else:
|
|
44
|
+
# A partial mapping is allowed: the backend merges and verifies it (§1.2).
|
|
45
|
+
payload = {"outputs": raw}
|
|
46
|
+
except DeviceComputationError as exc:
|
|
47
|
+
payload = {"error": {"code": exc.code, "message": str(exc)}}
|
|
48
|
+
except Exception as exc: # a script error run_python_script did not wrap
|
|
49
|
+
payload = {"error": {"code": "script_error", "message": f"{type(exc).__name__}: {exc}"}}
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
with open(result_path, "w", encoding="utf-8") as fh:
|
|
53
|
+
json.dump(payload, fh)
|
|
54
|
+
except Exception as exc: # cannot deliver the result -- a harness-level failure
|
|
55
|
+
print(f"lc child: cannot write result: {exc}", file=sys.stderr)
|
|
56
|
+
traceback.print_exc()
|
|
57
|
+
return 1
|
|
58
|
+
return 0
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
if __name__ == "__main__": # pragma: no cover - module entry
|
|
62
|
+
raise SystemExit(main())
|