labcode 0.9.0__tar.gz → 0.10.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.9.0 → labcode-0.10.0}/CHANGELOG.md +44 -0
- {labcode-0.9.0 → labcode-0.10.0}/PKG-INFO +4 -4
- {labcode-0.9.0 → labcode-0.10.0}/docs/SPECIFICATIONS.md +40 -28
- {labcode-0.9.0 → labcode-0.10.0}/examples/README.md +3 -3
- {labcode-0.9.0 → labcode-0.10.0}/labcode/_child.py +8 -9
- {labcode-0.9.0 → labcode-0.10.0}/labcode/backend.py +84 -35
- {labcode-0.9.0 → labcode-0.10.0}/labcode/dialect.py +18 -6
- {labcode-0.9.0 → labcode-0.10.0}/labcode/objectid.py +122 -33
- {labcode-0.9.0 → labcode-0.10.0}/labcode/run_cli.py +5 -0
- {labcode-0.9.0 → labcode-0.10.0}/labcode/runner.py +20 -3
- {labcode-0.9.0 → labcode-0.10.0}/labcode.egg-info/PKG-INFO +4 -4
- {labcode-0.9.0 → labcode-0.10.0}/labcode.egg-info/SOURCES.txt +2 -0
- {labcode-0.9.0 → labcode-0.10.0}/labcode.egg-info/requires.txt +3 -3
- {labcode-0.9.0 → labcode-0.10.0}/labcode.egg-info/scm_file_list.json +2 -0
- labcode-0.10.0/labcode.egg-info/scm_version.json +8 -0
- {labcode-0.9.0 → labcode-0.10.0}/pyproject.toml +11 -4
- labcode-0.10.0/tests/fixtures/dispense_read.workflow.yaml +81 -0
- {labcode-0.9.0 → labcode-0.10.0}/tests/test_backend.py +88 -4
- {labcode-0.9.0 → labcode-0.10.0}/tests/test_dialect.py +30 -3
- {labcode-0.9.0 → labcode-0.10.0}/tests/test_objectid.py +109 -0
- {labcode-0.9.0 → labcode-0.10.0}/tests/test_run_cli.py +32 -18
- labcode-0.10.0/tests/test_structured_labcode.py +135 -0
- labcode-0.9.0/labcode.egg-info/scm_version.json +0 -8
- {labcode-0.9.0 → labcode-0.10.0}/.github/workflows/ci.yml +0 -0
- {labcode-0.9.0 → labcode-0.10.0}/.github/workflows/publish.yml +0 -0
- {labcode-0.9.0 → labcode-0.10.0}/.gitignore +0 -0
- {labcode-0.9.0 → labcode-0.10.0}/LICENSE +0 -0
- {labcode-0.9.0 → labcode-0.10.0}/MANIFEST.in +0 -0
- {labcode-0.9.0 → labcode-0.10.0}/README.md +0 -0
- {labcode-0.9.0 → labcode-0.10.0}/examples/lads_plate_cycle.env.yaml +0 -0
- {labcode-0.9.0 → labcode-0.10.0}/examples/lads_seal.env.yaml +0 -0
- {labcode-0.9.0 → labcode-0.10.0}/examples/outputs/plate_line.boundary.yaml +0 -0
- {labcode-0.9.0 → labcode-0.10.0}/examples/outputs/plate_line.observation.yaml +0 -0
- {labcode-0.9.0 → labcode-0.10.0}/examples/outputs/plate_line.plan.yaml +0 -0
- {labcode-0.9.0 → labcode-0.10.0}/examples/outputs/plate_line.svg +0 -0
- {labcode-0.9.0 → labcode-0.10.0}/examples/outputs/shared_bench.boundary.yaml +0 -0
- {labcode-0.9.0 → labcode-0.10.0}/examples/outputs/shared_bench.observation.yaml +0 -0
- {labcode-0.9.0 → labcode-0.10.0}/examples/outputs/shared_bench.plan.yaml +0 -0
- {labcode-0.9.0 → labcode-0.10.0}/examples/outputs/shared_bench.svg +0 -0
- {labcode-0.9.0 → labcode-0.10.0}/examples/outputs/sila2_plate_cycle.boundary.yaml +0 -0
- {labcode-0.9.0 → labcode-0.10.0}/examples/outputs/sila2_plate_cycle.observation.yaml +0 -0
- {labcode-0.9.0 → labcode-0.10.0}/examples/outputs/sila2_plate_cycle.plan.yaml +0 -0
- {labcode-0.9.0 → labcode-0.10.0}/examples/outputs/sila2_plate_cycle.svg +0 -0
- {labcode-0.9.0 → labcode-0.10.0}/examples/plate_line.boundary.yaml +0 -0
- {labcode-0.9.0 → labcode-0.10.0}/examples/plate_line.env.yaml +0 -0
- {labcode-0.9.0 → labcode-0.10.0}/examples/plate_line.workflow.yaml +0 -0
- {labcode-0.9.0 → labcode-0.10.0}/examples/preflight_sila2_env.py +0 -0
- {labcode-0.9.0 → labcode-0.10.0}/examples/render_plate_line.py +0 -0
- {labcode-0.9.0 → labcode-0.10.0}/examples/render_shared_bench.py +0 -0
- {labcode-0.9.0 → labcode-0.10.0}/examples/render_sila2_plate_cycle.py +0 -0
- {labcode-0.9.0 → labcode-0.10.0}/examples/run_all_lads_examples.py +0 -0
- {labcode-0.9.0 → labcode-0.10.0}/examples/run_all_sila2_examples.py +0 -0
- {labcode-0.9.0 → labcode-0.10.0}/examples/run_sila2_plate_cycle.py +0 -0
- {labcode-0.9.0 → labcode-0.10.0}/examples/run_sila2_plate_cycle_no_atc.py +0 -0
- {labcode-0.9.0 → labcode-0.10.0}/examples/run_sila2_seal.py +0 -0
- {labcode-0.9.0 → labcode-0.10.0}/examples/shared_bench.afternoon.boundary.yaml +0 -0
- {labcode-0.9.0 → labcode-0.10.0}/examples/shared_bench.env.yaml +0 -0
- {labcode-0.9.0 → labcode-0.10.0}/examples/shared_bench.morning.boundary.yaml +0 -0
- {labcode-0.9.0 → labcode-0.10.0}/examples/shared_bench.run.yaml +0 -0
- {labcode-0.9.0 → labcode-0.10.0}/examples/shared_bench.workflow.yaml +0 -0
- {labcode-0.9.0 → labcode-0.10.0}/examples/sila2_plate_cycle.boundary.yaml +0 -0
- {labcode-0.9.0 → labcode-0.10.0}/examples/sila2_plate_cycle.workflow.yaml +0 -0
- {labcode-0.9.0 → labcode-0.10.0}/examples/sila2_plate_cycle.wrapped.env.yaml +0 -0
- {labcode-0.9.0 → labcode-0.10.0}/examples/sila2_plate_cycle_no_atc.boundary.yaml +0 -0
- {labcode-0.9.0 → labcode-0.10.0}/examples/sila2_plate_cycle_no_atc.workflow.yaml +0 -0
- {labcode-0.9.0 → labcode-0.10.0}/examples/sila2_plate_cycle_no_atc.wrapped.env.yaml +0 -0
- {labcode-0.9.0 → labcode-0.10.0}/examples/sila2_seal.boundary.yaml +0 -0
- {labcode-0.9.0 → labcode-0.10.0}/examples/sila2_seal.env.yaml +0 -0
- {labcode-0.9.0 → labcode-0.10.0}/examples/sila2_seal.workflow.yaml +0 -0
- {labcode-0.9.0 → labcode-0.10.0}/examples/sila2_seal.wrapped.env.yaml +0 -0
- {labcode-0.9.0 → labcode-0.10.0}/labcode/__init__.py +0 -0
- {labcode-0.9.0 → labcode-0.10.0}/labcode/__main__.py +0 -0
- {labcode-0.9.0 → labcode-0.10.0}/labcode/cli.py +0 -0
- {labcode-0.9.0 → labcode-0.10.0}/labcode/connections.py +0 -0
- {labcode-0.9.0 → labcode-0.10.0}/labcode/extension.py +0 -0
- {labcode-0.9.0 → labcode-0.10.0}/labcode/idgen.py +0 -0
- {labcode-0.9.0 → labcode-0.10.0}/labcode/lads.py +0 -0
- {labcode-0.9.0 → labcode-0.10.0}/labcode/lads_commands.py +0 -0
- {labcode-0.9.0 → labcode-0.10.0}/labcode/lads_unit.py +0 -0
- {labcode-0.9.0 → labcode-0.10.0}/labcode/otel.py +0 -0
- {labcode-0.9.0 → labcode-0.10.0}/labcode/otel_sila2.py +0 -0
- {labcode-0.9.0 → labcode-0.10.0}/labcode/probe.py +0 -0
- {labcode-0.9.0 → labcode-0.10.0}/labcode/py.typed +0 -0
- {labcode-0.9.0 → labcode-0.10.0}/labcode/record.py +0 -0
- {labcode-0.9.0 → labcode-0.10.0}/labcode/sila2.py +0 -0
- {labcode-0.9.0 → labcode-0.10.0}/labcode/sila2_commands.py +0 -0
- {labcode-0.9.0 → labcode-0.10.0}/labcode/sila2_instrument.py +0 -0
- {labcode-0.9.0 → labcode-0.10.0}/labcode.egg-info/dependency_links.txt +0 -0
- {labcode-0.9.0 → labcode-0.10.0}/labcode.egg-info/entry_points.txt +0 -0
- {labcode-0.9.0 → labcode-0.10.0}/labcode.egg-info/top_level.txt +0 -0
- {labcode-0.9.0 → labcode-0.10.0}/setup.cfg +0 -0
- {labcode-0.9.0 → labcode-0.10.0}/tests/fixtures/device_script.env.yaml +0 -0
- {labcode-0.9.0 → labcode-0.10.0}/tests/fixtures/device_script.workflow.yaml +0 -0
- {labcode-0.9.0 → labcode-0.10.0}/tests/fixtures/joint.run.yaml +0 -0
- {labcode-0.9.0 → labcode-0.10.0}/tests/fixtures/replenishment.env.yaml +0 -0
- {labcode-0.9.0 → labcode-0.10.0}/tests/fixtures/reroute_device.env.yaml +0 -0
- {labcode-0.9.0 → labcode-0.10.0}/tests/fixtures/reroute_transporter.env.yaml +0 -0
- {labcode-0.9.0 → labcode-0.10.0}/tests/fixtures/transport.env.yaml +0 -0
- {labcode-0.9.0 → labcode-0.10.0}/tests/fixtures/transport.workflow.yaml +0 -0
- {labcode-0.9.0 → labcode-0.10.0}/tests/test_cli.py +0 -0
- {labcode-0.9.0 → labcode-0.10.0}/tests/test_joint.py +0 -0
- {labcode-0.9.0 → labcode-0.10.0}/tests/test_lads.py +0 -0
- {labcode-0.9.0 → labcode-0.10.0}/tests/test_lads_unit.py +0 -0
- {labcode-0.9.0 → labcode-0.10.0}/tests/test_otel.py +0 -0
- {labcode-0.9.0 → labcode-0.10.0}/tests/test_otel_child.py +0 -0
- {labcode-0.9.0 → labcode-0.10.0}/tests/test_otel_grpc.py +0 -0
- {labcode-0.9.0 → labcode-0.10.0}/tests/test_otel_sila2.py +0 -0
- {labcode-0.9.0 → labcode-0.10.0}/tests/test_probe.py +0 -0
- {labcode-0.9.0 → labcode-0.10.0}/tests/test_record.py +0 -0
- {labcode-0.9.0 → labcode-0.10.0}/tests/test_recording.py +0 -0
- {labcode-0.9.0 → labcode-0.10.0}/tests/test_sila2.py +0 -0
- {labcode-0.9.0 → labcode-0.10.0}/tests/test_sila2_commands.py +0 -0
- {labcode-0.9.0 → labcode-0.10.0}/tests/test_sila2_instrument.py +0 -0
|
@@ -6,6 +6,50 @@ specification the section numbers refer to is [`docs/SPECIFICATIONS.md`](docs/SP
|
|
|
6
6
|
Releases before 0.9.0 are described in the messages of their `Release vX.Y.Z` commits
|
|
7
7
|
(`git log --grep '^Release v'`).
|
|
8
8
|
|
|
9
|
+
## 0.10.0 — 2026-10-05
|
|
10
|
+
|
|
11
|
+
labcode runs a protocol repeated over every plate (`map` / `fold`), and stops making up
|
|
12
|
+
a value a script did not compute.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- **`map` and `fold`** run, as ofplang-schedule 0.13 expands them and ofplang-run 0.13
|
|
17
|
+
executes the expansion. An **Array of Objects** at the run boundary is one Object per
|
|
18
|
+
element: its `view` is a list of views, one per spot, and each element gets its own
|
|
19
|
+
`_id`, keyed by the element (`plates[1]`, §4.3). An invocation's node path carries its
|
|
20
|
+
iteration index, and so do the keys of what it creates.
|
|
21
|
+
- **What a run made up, it says** (`lc run` prints each as `lc run: warning: ...`):
|
|
22
|
+
- `entry_input_defaulted` — a boundary view written in part, or not at all: the
|
|
23
|
+
declared fields it left out run on their type's default (§4.2). Once per Object.
|
|
24
|
+
- `output_view_defaulted` — a script that did not return an Object it created: the
|
|
25
|
+
view runs on its type's default (§1.2). Once per process and port.
|
|
26
|
+
- `_id` is never reported; minting it is labcode's identity, not a default.
|
|
27
|
+
|
|
28
|
+
### Changed — may need your attention
|
|
29
|
+
|
|
30
|
+
- **A script must return every Pure Data output it declares** (§1.2). Until now one left
|
|
31
|
+
out took its type's default — `return {}` from a `read` gave `od: 0.0`, a reading
|
|
32
|
+
nothing took. It now fails the operation (`script_output_names`), as v0 §22.2 does. An
|
|
33
|
+
Object output may still be left out: a mapped one is carried, a created one gets its
|
|
34
|
+
default view (reported).
|
|
35
|
+
- **A process with no script anywhere that declares a Pure Data output is refused** by
|
|
36
|
+
`lc run` before anything runs (§2): a no-op can compute nothing, so every run of it
|
|
37
|
+
would fail. One with only Object outputs still runs as a no-op, with the warning it
|
|
38
|
+
always had. A process with a script on some modes only fails if a mode without one is
|
|
39
|
+
chosen.
|
|
40
|
+
- **A boundary view that is not a mapping** (or, for an Array, not a list of them) is
|
|
41
|
+
refused. It used to be replaced by an empty view without a word.
|
|
42
|
+
- **An Object mapped from an input with no `_id`** fails the operation
|
|
43
|
+
(`missing_object_id`). No run can reach this; it is there to catch a broken invariant
|
|
44
|
+
early.
|
|
45
|
+
- A v0 §22 workflow script in a language other than Python fails the operation
|
|
46
|
+
(`script_language`) instead of running as a no-op.
|
|
47
|
+
- Requires ofplang-validate 0.3, ofplang-schedule 0.13 and ofplang-run 0.13
|
|
48
|
+
(specification revision 0.4).
|
|
49
|
+
|
|
50
|
+
`lc export` is unchanged; drawing a run that has `map` / `fold` invocations in it is not
|
|
51
|
+
yet supported there.
|
|
52
|
+
|
|
9
53
|
## 0.9.0 — 2026-09-28
|
|
10
54
|
|
|
11
55
|
labcode can now drive a lab over **LADS OPC UA** as well as SiLA2, and gains `lc export`.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: labcode
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.10.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
|
|
@@ -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.
|
|
24
|
-
Requires-Dist: ofplang-schedule<0.
|
|
25
|
-
Requires-Dist: ofplang-run<0.
|
|
23
|
+
Requires-Dist: ofplang-validate<0.4,>=0.3
|
|
24
|
+
Requires-Dist: ofplang-schedule<0.14,>=0.13
|
|
25
|
+
Requires-Dist: ofplang-run<0.14,>=0.13
|
|
26
26
|
Provides-Extra: test
|
|
27
27
|
Requires-Dist: pytest>=7.0; extra == "test"
|
|
28
28
|
Requires-Dist: opentelemetry-sdk>=1.20; extra == "test"
|
|
@@ -74,26 +74,29 @@ port names**, each bound to that port's view value (Pure Data or an Object's vie
|
|
|
74
74
|
— as in a v0 §22 `python_script_processes` script. External `import` is allowed; there is
|
|
75
75
|
no sandbox.
|
|
76
76
|
|
|
77
|
-
**
|
|
78
|
-
|
|
79
|
-
|
|
77
|
+
**Object outputs need not be returned.** As in v0 §22.2, a labcode process script
|
|
78
|
+
returns its outputs as a mapping, and **every Pure Data output it declares must be in
|
|
79
|
+
it**: a measurement nothing computed has no value to give, and a default would be a
|
|
80
|
+
made-up reading. One left out fails the operation (`script_output_names`), as does a
|
|
81
|
+
returned name that is not a declared output (this catches a typo'd name). Each returned
|
|
82
|
+
value must conform to its port's type.
|
|
83
|
+
|
|
84
|
+
An **Object** output may be left out, because labcode can supply it without making a
|
|
85
|
+
value up:
|
|
80
86
|
|
|
81
87
|
- an Object output declared in `objects.map` is **carried from its input** (the same
|
|
82
88
|
Object, its view unchanged) — so a pass-through need not restate it;
|
|
83
|
-
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
typo'd output name). A §22.2-strict script — one that returns every output explicitly —
|
|
88
|
-
works unchanged.
|
|
89
|
+
- an Object output declared in `objects.create` gets its type's **default view** (and a
|
|
90
|
+
freshly minted `_id`, §4.2). That view is a value labcode made up, so the run reports it
|
|
91
|
+
(`output_view_defaulted`, once per process and port); returning the view says what it
|
|
92
|
+
is, and the warning goes away. `_id` is never part of what is reported (§4.2).
|
|
89
93
|
|
|
90
|
-
So for `read` (input `plate`, outputs `plate` via `objects.map` + `od`)
|
|
91
|
-
equivalent to returning `{"plate": plate, "od": 0.42}`… except the defaulted forms:
|
|
94
|
+
So for `read` (input `plate`, outputs `plate` via `objects.map` + `od`):
|
|
92
95
|
|
|
93
96
|
```python
|
|
94
97
|
return {"plate": plate, "od": 0.42} # explicit
|
|
95
98
|
return {"od": 0.42} # plate carried by objects.map
|
|
96
|
-
return {} #
|
|
99
|
+
return {} # fails: od is not returned
|
|
97
100
|
```
|
|
98
101
|
|
|
99
102
|
### 1.3 `x-labcode` on a transport route
|
|
@@ -645,15 +648,20 @@ For a dispatched `(process, mode)`, labcode resolves the code to run in this ord
|
|
|
645
648
|
|
|
646
649
|
1. the mode's `x-labcode.script.code` (2 — the labcode device script), else
|
|
647
650
|
2. the workflow process's own `script.code` (1 — a v0 §22 script process), else
|
|
648
|
-
3. none — the operation runs as a **
|
|
649
|
-
|
|
651
|
+
3. none — the operation runs as a **no-op** (a device not yet scripted): its Object
|
|
652
|
+
outputs are supplied as above (§1.2), and a Pure Data output, which only a script
|
|
653
|
+
could compute, fails the operation (`script_output_names`).
|
|
650
654
|
|
|
651
655
|
**Exclusivity (error).** A process MUST NOT carry both a workflow `script` (1) and an env
|
|
652
656
|
`x-labcode.script` (2) on any of its modes; that is ambiguous and is rejected.
|
|
653
657
|
|
|
654
|
-
**
|
|
655
|
-
will run as a
|
|
656
|
-
|
|
658
|
+
**No-op reachability (warning, or error).** A process with neither (1) nor (2) on any
|
|
659
|
+
mode will run as a no-op. This is allowed — convenient while mocking a device — but `lc
|
|
660
|
+
run` warns about it, so an unimplemented device is not silently a no-op. A process that
|
|
661
|
+
also declares a **Pure Data output** is refused instead: every run of it would fail, so
|
|
662
|
+
it fails before anything runs. A process with a script on some modes and not others is
|
|
663
|
+
not refused — which mode runs is the scheduler's choice — and fails only if a mode
|
|
664
|
+
without one is chosen.
|
|
657
665
|
|
|
658
666
|
**Transport and replenishment routes have no such chain.** There is nothing for them to
|
|
659
667
|
fall back to: a workflow describes neither a physical move nor a refill, so the route's
|
|
@@ -744,13 +752,17 @@ An Object's `_id` is set at its two points of origin, then **carried** everywher
|
|
|
744
752
|
`objects.map` and transport copy the whole view, so `_id` propagates for free:
|
|
745
753
|
|
|
746
754
|
- **`objects.create`** — a newly created Object's `_id` is minted when the operation
|
|
747
|
-
produces it
|
|
748
|
-
|
|
749
|
-
output, §1.2).
|
|
755
|
+
produces it. A device script need not know about `_id`: it returns the view (or leaves
|
|
756
|
+
the Object out, §1.2), and labcode supplies `_id`.
|
|
750
757
|
- **run boundary** — a whole-workflow Object *input* enters at the boundary; `lc run`
|
|
751
|
-
mints its `_id
|
|
752
|
-
|
|
753
|
-
|
|
758
|
+
mints its `_id`, **unless the boundary already carries one** — so a result boundary fed
|
|
759
|
+
back in round-trips its ids. An **Array of Objects** is one Object per element: its
|
|
760
|
+
`view`, if given, is a list of views, one per spot, and each element gets its own `_id`.
|
|
761
|
+
A view may be written in part: a declared field it leaves out takes its type's default,
|
|
762
|
+
and the run reports which (`entry_input_defaulted`, once per element). A view that is
|
|
763
|
+
not a mapping is refused, not replaced.
|
|
764
|
+
- **What is reported.** `_id` is never in a warning: minting it is this feature, not a
|
|
765
|
+
default. Only a declared view field labcode had to fill is.
|
|
754
766
|
- **`objects.map`** — a mapped Object output carries its input's `_id` unchanged
|
|
755
767
|
(identity preserved), even if a §22.2-strict script returned the port explicitly.
|
|
756
768
|
|
|
@@ -759,16 +771,16 @@ An Object's `_id` is set at its two points of origin, then **carried** everywher
|
|
|
759
771
|
Ids come from a swappable generator (`labcode.idgen.IdGenerator`). The default
|
|
760
772
|
(`SeededUuid4Generator`) mints **reproducible** uuid4-shaped ids from a seed and a
|
|
761
773
|
*provenance key* — the node instance + output port for a create, the port name for a
|
|
762
|
-
boundary input — **not** draw order. So the same workflow yields the same ids on every
|
|
774
|
+
boundary input (`plates[1]` for an element of an Array) — **not** draw order. So the same workflow yields the same ids on every
|
|
763
775
|
run, and the wall-clock backend's jittering completion order cannot change them (which is
|
|
764
776
|
what keeps checked-in example observations stable). A real run wanting globally-unique
|
|
765
777
|
ids per physical Object swaps in `RealUuid4Generator` (via
|
|
766
778
|
`labcode_backend_factory(id_generator=...)`).
|
|
767
779
|
|
|
768
|
-
> The provenance key is the runner's node-instance identity + port.
|
|
769
|
-
>
|
|
770
|
-
>
|
|
771
|
-
>
|
|
780
|
+
> The provenance key is the runner's node-instance identity + port. An invocation of an
|
|
781
|
+
> expanded `map` / `fold` has its iteration index in its node path (`Load/2`), so each
|
|
782
|
+
> invocation's creates key apart and stay reproducible. Dynamic control flow
|
|
783
|
+
> (`do_while`) is not run.
|
|
772
784
|
|
|
773
785
|
## 5. Not yet in this version (roadmap)
|
|
774
786
|
|
|
@@ -29,8 +29,8 @@ load ──[move]──> dispense ──[move]──> read ──[move]──> s
|
|
|
29
29
|
**Every process mode and every transport route carries a script**: `load` makes the
|
|
30
30
|
Plate, `dispense` dispenses the Tube into the Plate (both Objects are carried through by
|
|
31
31
|
`objects.map`, so its script returns nothing), `read` measures `od` (the Plate is
|
|
32
|
-
carried, so the script returns only
|
|
33
|
-
the spec), `store` takes it; the transport scripts perform the move and may read the
|
|
32
|
+
carried, so the script returns only the reading — an Object output need not be
|
|
33
|
+
returned, see the spec), `store` takes it; the transport scripts perform the move and may read the
|
|
34
34
|
moved Plate's `view` (its barcode).
|
|
35
35
|
|
|
36
36
|
labcode runs each script **out-of-process** on a wall clock, discovering completion by
|
|
@@ -189,7 +189,7 @@ result boundary does not, carrying none.
|
|
|
189
189
|
Where `plate_line`'s scripts are mocks that only return values, this example's scripts open
|
|
190
190
|
SiLA2 connections and issue real commands. It is the integration check for labcode's SiLA2
|
|
191
191
|
story: labcode schedules, dispatches out-of-process, a script talks SiLA2, an instrument acts,
|
|
192
|
-
and the produced value comes back
|
|
192
|
+
and the produced value comes back as the script's return, the plate carried by labcode.
|
|
193
193
|
|
|
194
194
|
```
|
|
195
195
|
(in) plate ──[move]──▶ seal ──[move]──▶ (out) plate
|
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
"""labcode's out-of-process child harness
|
|
1
|
+
"""labcode's out-of-process child harness.
|
|
2
2
|
|
|
3
|
-
Like ``ofplang.run.simulator._child``, but labcode process
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
output
|
|
8
|
-
|
|
9
|
-
`labcode.backend.LabcodeBackend`.
|
|
3
|
+
Like ``ofplang.run.simulator._child``, but a labcode process script may leave out an
|
|
4
|
+
Object output (SPECIFICATIONS.md §1.2), which the backend supplies -- carried through
|
|
5
|
+
``objects.map``, or a created Object's default view. So this child does **no** output
|
|
6
|
+
verification; it just returns the script's raw result mapping. The check that every Pure
|
|
7
|
+
Data output is there, the rejection of any *extra* (undeclared) output name, the Object
|
|
8
|
+
fill and the conformance check all happen in `labcode.backend.LabcodeBackend`.
|
|
10
9
|
|
|
11
10
|
A transport or refill script is side-effect only (its return is ignored). Protocol (JSON on
|
|
12
11
|
stdin, outcome to ``result_path``) is identical to the upstream child; only the process
|
|
@@ -61,7 +60,7 @@ def _execute(job: dict, result_path: str) -> int:
|
|
|
61
60
|
f"{type(raw).__name__}, not a mapping",
|
|
62
61
|
}}
|
|
63
62
|
else:
|
|
64
|
-
#
|
|
63
|
+
# Object outputs may be missing: the backend supplies and verifies (§1.2).
|
|
65
64
|
payload = {"outputs": raw}
|
|
66
65
|
except DeviceComputationError as exc:
|
|
67
66
|
payload = {"error": {"code": exc.code, "message": str(exc)}}
|
|
@@ -66,7 +66,7 @@ from labcode.extension import (
|
|
|
66
66
|
transporter_connections,
|
|
67
67
|
)
|
|
68
68
|
from labcode.idgen import DEFAULT_ID_GENERATOR, IdGenerator
|
|
69
|
-
from labcode.objectid import RESERVED_ID, stamp_object_ids
|
|
69
|
+
from labcode.objectid import RESERVED_ID, object_output_ports, stamp_object_ids
|
|
70
70
|
from labcode.probe import (
|
|
71
71
|
Availability,
|
|
72
72
|
CadenceReporter,
|
|
@@ -289,8 +289,8 @@ def _labcode_child_spawn(job: dict, env: Mapping[str, str] | None = None):
|
|
|
289
289
|
|
|
290
290
|
|
|
291
291
|
class LabcodeBackend(SubprocessBackend):
|
|
292
|
-
"""A `SubprocessBackend` for the labcode dialect:
|
|
293
|
-
execution.
|
|
292
|
+
"""A `SubprocessBackend` for the labcode dialect: Object outputs filled for a script +
|
|
293
|
+
transport execution.
|
|
294
294
|
|
|
295
295
|
Two dialect behaviours on top of the base backend:
|
|
296
296
|
|
|
@@ -299,11 +299,12 @@ class LabcodeBackend(SubprocessBackend):
|
|
|
299
299
|
moved Object's view bound as locals (``from_spot`` / ``to_spot`` / ``transporter`` /
|
|
300
300
|
``view``). It reuses the upstream machinery -- `_start_child_op` launches it and the
|
|
301
301
|
settle loop completes / fails it (ofplang-run >= 0.1.6) -- so only dispatch is added.
|
|
302
|
-
* **
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
302
|
+
* **Object outputs need not be returned** (SPECIFICATIONS.md §1.2). A process script
|
|
303
|
+
returns every Pure Data output it declares, and may leave out an Object output:
|
|
304
|
+
`_resolve_model` carries a mapped one (``objects.map``) and gives a created one a
|
|
305
|
+
default view, reported in `warnings`. A Pure Data output left out, or a returned
|
|
306
|
+
name that is not a declared output, is an error -- nothing makes a measurement up.
|
|
307
|
+
This needs the Object-tolerant `labcode._child` (the default `spawn`), since the
|
|
307
308
|
upstream child verifies outputs exactly."""
|
|
308
309
|
|
|
309
310
|
def __init__(
|
|
@@ -343,6 +344,11 @@ class LabcodeBackend(SubprocessBackend):
|
|
|
343
344
|
# with the run boundary's minting so a run's ids are consistent; default is the
|
|
344
345
|
# reproducible seeded generator.
|
|
345
346
|
self._id_gen = id_generator or DEFAULT_ID_GENERATOR
|
|
347
|
+
# Values this backend made up and said so (`ofplang.run` `RunWarning`): the view
|
|
348
|
+
# of a created Object its script did not return. One per (job, process, port),
|
|
349
|
+
# however many operations of it there were.
|
|
350
|
+
self.warnings: list = []
|
|
351
|
+
self._warned: set = set()
|
|
346
352
|
|
|
347
353
|
# -- recording what the run did (`labcode.record`) -------------------------------
|
|
348
354
|
|
|
@@ -422,46 +428,89 @@ class LabcodeBackend(SubprocessBackend):
|
|
|
422
428
|
|
|
423
429
|
def _resolve_model(self, process, mode, inputs, output_schema, definition,
|
|
424
430
|
node=None, job=None):
|
|
425
|
-
"""The value model `_complete` calls at completion:
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
`
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
431
|
+
"""The value model `_complete` calls at completion: the script's outputs, with any
|
|
432
|
+
Object output it left out filled (§1.2), then Object identities (`_id`) stamped.
|
|
433
|
+
|
|
434
|
+
Every Pure Data output must come from the script: one it leaves out -- or a
|
|
435
|
+
process with no script at all that declares one (§2) -- is a runtime failure
|
|
436
|
+
(`script_output_names`), since nothing else could compute it and a default would
|
|
437
|
+
be a made-up measurement. An Object output may be left out: `default_device_model`
|
|
438
|
+
carries a mapped one, and gives a created one a default view, which is reported
|
|
439
|
+
(`warnings`). A returned name outside `output_schema` is a runtime failure.
|
|
440
|
+
`stamp_object_ids` then mints ``_id`` for a created Object and carries it for a
|
|
441
|
+
mapped one (keyed by `node`, the workflow provenance, **and by `job`** -- two jobs
|
|
442
|
+
of one workflow render the same node path, so the node alone would mint one
|
|
443
|
+
identity for two plates). A child error is handled as in the base backend.
|
|
435
444
|
|
|
436
445
|
🔴 Both halves arrive here rather than at dispatch because this is where an
|
|
437
446
|
identity is *made*: the runner offers provenance to a model that declares it
|
|
438
447
|
(`ofplang-run` >= 0.11), and the simulator carries it on the operation from the
|
|
439
448
|
dispatch that started it."""
|
|
440
449
|
pending = self._pending
|
|
450
|
+
created, mapped = object_output_ports(definition)
|
|
451
|
+
object_ports = set(created) | set(mapped)
|
|
441
452
|
if not isinstance(pending, dict): # the _TIMED sentinel: no child ran
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
453
|
+
# A workflow script in a language no child runs is a failure, as in the base
|
|
454
|
+
# backend -- not a process without a script.
|
|
455
|
+
script = (definition or {}).get("script")
|
|
456
|
+
if script is not None and python_code(script) is None:
|
|
457
|
+
language = script.get("language") if isinstance(script, dict) else None
|
|
458
|
+
raise DeviceComputationError(
|
|
459
|
+
f"script process {process!r} declares unsupported script language "
|
|
460
|
+
f"{language!r}",
|
|
461
|
+
code="script_language",
|
|
462
|
+
)
|
|
463
|
+
raw: dict = {}
|
|
464
|
+
what = f"process {process!r} mode {mode!r} has no script, and nothing computes"
|
|
465
|
+
else:
|
|
466
|
+
if "error" in pending:
|
|
467
|
+
err = pending["error"]
|
|
468
|
+
raise DeviceComputationError(
|
|
469
|
+
err.get("message", "child failed"), code=err.get("code", "child_error")
|
|
470
|
+
)
|
|
471
|
+
raw = pending.get("outputs") or {}
|
|
472
|
+
extra = set(raw) - set(output_schema or {})
|
|
473
|
+
if extra:
|
|
474
|
+
raise DeviceComputationError(
|
|
475
|
+
f"script process {process!r} returned undeclared output names "
|
|
476
|
+
f"{sorted(extra)}",
|
|
477
|
+
code="script_output_names",
|
|
478
|
+
)
|
|
479
|
+
what = f"script process {process!r} did not return"
|
|
480
|
+
# Every Pure Data output has to be computed: there is no default for one (D59).
|
|
481
|
+
missing = sorted(set(output_schema or {}) - object_ports - set(raw))
|
|
482
|
+
if missing:
|
|
483
|
+
raise DeviceComputationError(f"{what} output(s) {missing}", code="script_output_names")
|
|
459
484
|
base = default_device_model(process, mode, inputs, output_schema, definition)
|
|
485
|
+
# A created Object the script did not return starts with a default view: say so.
|
|
486
|
+
for port in created:
|
|
487
|
+
if port not in raw:
|
|
488
|
+
self._warn_defaulted_view(process, port, node, job, output_schema)
|
|
460
489
|
return stamp_object_ids(
|
|
461
490
|
{**base, **raw}, definition, inputs or {}, node, self._id_gen, output_schema,
|
|
462
491
|
job=job,
|
|
463
492
|
)
|
|
464
493
|
|
|
494
|
+
def _warn_defaulted_view(self, process, port, node, job, output_schema) -> None:
|
|
495
|
+
"""Report a created Object whose view runs on its type's defaults, once per job,
|
|
496
|
+
process and port. ``_id`` is not a default -- it is minted -- so a type with no
|
|
497
|
+
other view field gives nothing to report."""
|
|
498
|
+
from ofplang.run.runner.job import RunWarning
|
|
499
|
+
|
|
500
|
+
fields = (output_schema or {}).get(port, {}).get("fields") or {}
|
|
501
|
+
defaulted = [field for field in fields if field != RESERVED_ID]
|
|
502
|
+
key = (job or "", process, port)
|
|
503
|
+
if not defaulted or key in self._warned:
|
|
504
|
+
return
|
|
505
|
+
self._warned.add(key)
|
|
506
|
+
where = "/".join(str(step) for step in node) if node else "?"
|
|
507
|
+
self.warnings.append(RunWarning(
|
|
508
|
+
"output_view_defaulted",
|
|
509
|
+
f"process {process!r} did not return its created Object {port!r} (first at "
|
|
510
|
+
f"{where}), so view field(s) {defaulted} run on their type's default",
|
|
511
|
+
job or "",
|
|
512
|
+
))
|
|
513
|
+
|
|
465
514
|
def advance(self, until: int) -> int:
|
|
466
515
|
"""Pace the wall clock as the base backend does, and say so once if this run
|
|
467
516
|
cannot keep the cadence it was asked for.
|
|
@@ -70,7 +70,7 @@ from labcode.extension import (
|
|
|
70
70
|
transport_label,
|
|
71
71
|
unknown_key_messages,
|
|
72
72
|
)
|
|
73
|
-
from labcode.objectid import RESERVED_ID, reserved_collisions
|
|
73
|
+
from labcode.objectid import RESERVED_ID, object_output_ports, reserved_collisions
|
|
74
74
|
|
|
75
75
|
|
|
76
76
|
@dataclass
|
|
@@ -375,12 +375,24 @@ def _validate_processes(
|
|
|
375
375
|
f"process {name!r} has both a workflow script (1) and an env "
|
|
376
376
|
f"x-labcode.script (2); they are mutually exclusive"
|
|
377
377
|
)
|
|
378
|
-
#
|
|
378
|
+
# A process with no script anywhere runs as a no-op. That is allowed -- a device not
|
|
379
|
+
# scripted yet, while the rest is mocked -- and warned about. But a no-op computes
|
|
380
|
+
# nothing, so a Pure Data output it declares could only be made up: such a process
|
|
381
|
+
# is certain to fail every time it runs, and is refused here, before it does (D59).
|
|
379
382
|
if not wf_has_script and not modes_with_script:
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
383
|
+
created, mapped = object_output_ports(wf_procs.get(name))
|
|
384
|
+
declared = (wf_procs.get(name) or {}).get("outputs") or {}
|
|
385
|
+
computed = sorted(set(declared) - set(created) - set(mapped))
|
|
386
|
+
if computed:
|
|
387
|
+
errors.append(
|
|
388
|
+
f"process {name!r} has no script (workflow or x-labcode) but declares "
|
|
389
|
+
f"output(s) {computed}, which only a script can compute"
|
|
390
|
+
)
|
|
391
|
+
else:
|
|
392
|
+
warnings.append(
|
|
393
|
+
f"process {name!r} has no script (workflow or x-labcode); its operations "
|
|
394
|
+
f"will run as a no-op"
|
|
395
|
+
)
|
|
384
396
|
|
|
385
397
|
|
|
386
398
|
def _check_reserved_locals(wf_process: Any, label: str, errors: list, flavor: str) -> None:
|