labcode 0.6.0__tar.gz → 0.8.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.
Files changed (101) hide show
  1. {labcode-0.6.0 → labcode-0.8.0}/PKG-INFO +23 -3
  2. {labcode-0.6.0 → labcode-0.8.0}/README.md +20 -0
  3. {labcode-0.6.0 → labcode-0.8.0}/examples/README.md +102 -11
  4. labcode-0.8.0/examples/outputs/shared_bench.boundary.yaml +33 -0
  5. labcode-0.8.0/examples/outputs/shared_bench.observation.yaml +141 -0
  6. labcode-0.8.0/examples/outputs/shared_bench.plan.yaml +134 -0
  7. labcode-0.8.0/examples/outputs/shared_bench.svg +2 -0
  8. labcode-0.8.0/examples/render_shared_bench.py +109 -0
  9. labcode-0.8.0/examples/shared_bench.afternoon.boundary.yaml +15 -0
  10. labcode-0.8.0/examples/shared_bench.env.yaml +91 -0
  11. labcode-0.8.0/examples/shared_bench.morning.boundary.yaml +15 -0
  12. labcode-0.8.0/examples/shared_bench.run.yaml +25 -0
  13. labcode-0.8.0/examples/shared_bench.workflow.yaml +61 -0
  14. {labcode-0.6.0 → labcode-0.8.0}/labcode/backend.py +28 -9
  15. {labcode-0.6.0 → labcode-0.8.0}/labcode/objectid.py +41 -12
  16. {labcode-0.6.0 → labcode-0.8.0}/labcode/record.py +5 -0
  17. {labcode-0.6.0 → labcode-0.8.0}/labcode/run_cli.py +111 -24
  18. {labcode-0.6.0 → labcode-0.8.0}/labcode/runner.py +94 -16
  19. {labcode-0.6.0 → labcode-0.8.0}/labcode.egg-info/PKG-INFO +23 -3
  20. {labcode-0.6.0 → labcode-0.8.0}/labcode.egg-info/SOURCES.txt +12 -0
  21. {labcode-0.6.0 → labcode-0.8.0}/labcode.egg-info/requires.txt +2 -2
  22. {labcode-0.6.0 → labcode-0.8.0}/labcode.egg-info/scm_file_list.json +12 -0
  23. labcode-0.8.0/labcode.egg-info/scm_version.json +8 -0
  24. {labcode-0.6.0 → labcode-0.8.0}/pyproject.toml +12 -7
  25. labcode-0.8.0/tests/fixtures/joint.run.yaml +6 -0
  26. labcode-0.8.0/tests/test_joint.py +309 -0
  27. {labcode-0.6.0 → labcode-0.8.0}/tests/test_objectid.py +63 -0
  28. labcode-0.6.0/labcode.egg-info/scm_version.json +0 -8
  29. {labcode-0.6.0 → labcode-0.8.0}/.github/workflows/ci.yml +0 -0
  30. {labcode-0.6.0 → labcode-0.8.0}/.github/workflows/publish.yml +0 -0
  31. {labcode-0.6.0 → labcode-0.8.0}/.gitignore +0 -0
  32. {labcode-0.6.0 → labcode-0.8.0}/LICENSE +0 -0
  33. {labcode-0.6.0 → labcode-0.8.0}/MANIFEST.in +0 -0
  34. {labcode-0.6.0 → labcode-0.8.0}/SPECIFICATIONS.md +0 -0
  35. {labcode-0.6.0 → labcode-0.8.0}/examples/outputs/plate_line.boundary.yaml +0 -0
  36. {labcode-0.6.0 → labcode-0.8.0}/examples/outputs/plate_line.observation.yaml +0 -0
  37. {labcode-0.6.0 → labcode-0.8.0}/examples/outputs/plate_line.plan.yaml +0 -0
  38. {labcode-0.6.0 → labcode-0.8.0}/examples/outputs/plate_line.svg +0 -0
  39. {labcode-0.6.0 → labcode-0.8.0}/examples/outputs/sila2_plate_cycle.boundary.yaml +0 -0
  40. {labcode-0.6.0 → labcode-0.8.0}/examples/outputs/sila2_plate_cycle.observation.yaml +0 -0
  41. {labcode-0.6.0 → labcode-0.8.0}/examples/outputs/sila2_plate_cycle.plan.yaml +0 -0
  42. {labcode-0.6.0 → labcode-0.8.0}/examples/outputs/sila2_plate_cycle.svg +0 -0
  43. {labcode-0.6.0 → labcode-0.8.0}/examples/plate_line.boundary.yaml +0 -0
  44. {labcode-0.6.0 → labcode-0.8.0}/examples/plate_line.env.yaml +0 -0
  45. {labcode-0.6.0 → labcode-0.8.0}/examples/plate_line.workflow.yaml +0 -0
  46. {labcode-0.6.0 → labcode-0.8.0}/examples/preflight_sila2_env.py +0 -0
  47. {labcode-0.6.0 → labcode-0.8.0}/examples/render_plate_line.py +0 -0
  48. {labcode-0.6.0 → labcode-0.8.0}/examples/render_sila2_plate_cycle.py +0 -0
  49. {labcode-0.6.0 → labcode-0.8.0}/examples/run_all_sila2_examples.py +0 -0
  50. {labcode-0.6.0 → labcode-0.8.0}/examples/run_sila2_plate_cycle.py +0 -0
  51. {labcode-0.6.0 → labcode-0.8.0}/examples/run_sila2_plate_cycle_no_atc.py +0 -0
  52. {labcode-0.6.0 → labcode-0.8.0}/examples/run_sila2_seal.py +0 -0
  53. {labcode-0.6.0 → labcode-0.8.0}/examples/sila2_plate_cycle.boundary.yaml +0 -0
  54. {labcode-0.6.0 → labcode-0.8.0}/examples/sila2_plate_cycle.workflow.yaml +0 -0
  55. {labcode-0.6.0 → labcode-0.8.0}/examples/sila2_plate_cycle.wrapped.env.yaml +0 -0
  56. {labcode-0.6.0 → labcode-0.8.0}/examples/sila2_plate_cycle_no_atc.boundary.yaml +0 -0
  57. {labcode-0.6.0 → labcode-0.8.0}/examples/sila2_plate_cycle_no_atc.workflow.yaml +0 -0
  58. {labcode-0.6.0 → labcode-0.8.0}/examples/sila2_plate_cycle_no_atc.wrapped.env.yaml +0 -0
  59. {labcode-0.6.0 → labcode-0.8.0}/examples/sila2_seal.boundary.yaml +0 -0
  60. {labcode-0.6.0 → labcode-0.8.0}/examples/sila2_seal.env.yaml +0 -0
  61. {labcode-0.6.0 → labcode-0.8.0}/examples/sila2_seal.workflow.yaml +0 -0
  62. {labcode-0.6.0 → labcode-0.8.0}/examples/sila2_seal.wrapped.env.yaml +0 -0
  63. {labcode-0.6.0 → labcode-0.8.0}/labcode/__init__.py +0 -0
  64. {labcode-0.6.0 → labcode-0.8.0}/labcode/__main__.py +0 -0
  65. {labcode-0.6.0 → labcode-0.8.0}/labcode/_child.py +0 -0
  66. {labcode-0.6.0 → labcode-0.8.0}/labcode/cli.py +0 -0
  67. {labcode-0.6.0 → labcode-0.8.0}/labcode/dialect.py +0 -0
  68. {labcode-0.6.0 → labcode-0.8.0}/labcode/extension.py +0 -0
  69. {labcode-0.6.0 → labcode-0.8.0}/labcode/idgen.py +0 -0
  70. {labcode-0.6.0 → labcode-0.8.0}/labcode/otel.py +0 -0
  71. {labcode-0.6.0 → labcode-0.8.0}/labcode/otel_sila2.py +0 -0
  72. {labcode-0.6.0 → labcode-0.8.0}/labcode/probe.py +0 -0
  73. {labcode-0.6.0 → labcode-0.8.0}/labcode/py.typed +0 -0
  74. {labcode-0.6.0 → labcode-0.8.0}/labcode/sila2.py +0 -0
  75. {labcode-0.6.0 → labcode-0.8.0}/labcode/sila2_commands.py +0 -0
  76. {labcode-0.6.0 → labcode-0.8.0}/labcode/sila2_instrument.py +0 -0
  77. {labcode-0.6.0 → labcode-0.8.0}/labcode.egg-info/dependency_links.txt +0 -0
  78. {labcode-0.6.0 → labcode-0.8.0}/labcode.egg-info/entry_points.txt +0 -0
  79. {labcode-0.6.0 → labcode-0.8.0}/labcode.egg-info/top_level.txt +0 -0
  80. {labcode-0.6.0 → labcode-0.8.0}/setup.cfg +0 -0
  81. {labcode-0.6.0 → labcode-0.8.0}/tests/fixtures/device_script.env.yaml +0 -0
  82. {labcode-0.6.0 → labcode-0.8.0}/tests/fixtures/device_script.workflow.yaml +0 -0
  83. {labcode-0.6.0 → labcode-0.8.0}/tests/fixtures/replenishment.env.yaml +0 -0
  84. {labcode-0.6.0 → labcode-0.8.0}/tests/fixtures/reroute_device.env.yaml +0 -0
  85. {labcode-0.6.0 → labcode-0.8.0}/tests/fixtures/reroute_transporter.env.yaml +0 -0
  86. {labcode-0.6.0 → labcode-0.8.0}/tests/fixtures/transport.env.yaml +0 -0
  87. {labcode-0.6.0 → labcode-0.8.0}/tests/fixtures/transport.workflow.yaml +0 -0
  88. {labcode-0.6.0 → labcode-0.8.0}/tests/test_backend.py +0 -0
  89. {labcode-0.6.0 → labcode-0.8.0}/tests/test_cli.py +0 -0
  90. {labcode-0.6.0 → labcode-0.8.0}/tests/test_dialect.py +0 -0
  91. {labcode-0.6.0 → labcode-0.8.0}/tests/test_otel.py +0 -0
  92. {labcode-0.6.0 → labcode-0.8.0}/tests/test_otel_child.py +0 -0
  93. {labcode-0.6.0 → labcode-0.8.0}/tests/test_otel_grpc.py +0 -0
  94. {labcode-0.6.0 → labcode-0.8.0}/tests/test_otel_sila2.py +0 -0
  95. {labcode-0.6.0 → labcode-0.8.0}/tests/test_probe.py +0 -0
  96. {labcode-0.6.0 → labcode-0.8.0}/tests/test_record.py +0 -0
  97. {labcode-0.6.0 → labcode-0.8.0}/tests/test_recording.py +0 -0
  98. {labcode-0.6.0 → labcode-0.8.0}/tests/test_run_cli.py +0 -0
  99. {labcode-0.6.0 → labcode-0.8.0}/tests/test_sila2.py +0 -0
  100. {labcode-0.6.0 → labcode-0.8.0}/tests/test_sila2_commands.py +0 -0
  101. {labcode-0.6.0 → labcode-0.8.0}/tests/test_sila2_instrument.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: labcode
3
- Version: 0.6.0
3
+ Version: 0.8.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.3,>=0.2
24
- Requires-Dist: ofplang-schedule<0.11,>=0.9
25
- Requires-Dist: ofplang-run<0.10,>=0.8
24
+ Requires-Dist: ofplang-schedule<0.12,>=0.9
25
+ Requires-Dist: ofplang-run<0.13,>=0.11
26
26
  Provides-Extra: test
27
27
  Requires-Dist: pytest>=7.0; extra == "test"
28
28
  Requires-Dist: opentelemetry-sdk>=1.20; extra == "test"
@@ -145,9 +145,29 @@ lc run <workflow> --env <env>
145
145
  [--seconds-per-tick S] [--op-timeout S | --no-op-timeout] [--no-probe]
146
146
  [--max-transport-legs N] [--ignore-resources] [--trace] [--mission-id ID]
147
147
  [--object-ids seeded|real]
148
+
149
+ lc run --jobs <run doc> --env <env> [--on-job-failure continue|stop] [...]
148
150
  ```
149
151
 
150
152
  - `<workflow>` — the portable v0 workflow: *what* happens.
153
+ - `--jobs RUNDOC` — run **several jobs together** in one laboratory (schedule SPEC
154
+ §6.11) in place of the single `<workflow>`. The run document names each job — an `id`,
155
+ its workflow, its own boundary, and when it may start — plus what the laboratory's own
156
+ stocks hold to begin with and which spots it is already holding. They are planned
157
+ *together*, so they compete for the same machines and share a refill neither needs
158
+ alone. Each job's workflow goes through both front doors separately and a rejection
159
+ names the job it came from; a boundary belongs to a job, so `--boundary` is not used
160
+ with `--jobs`.
161
+ 🔴 Object identities are minted **per job**: two jobs of one workflow bind the same
162
+ port names and render the same node paths, so a reproducible generator keyed on those
163
+ alone would give one job's plate the other's `_id`. `--trace` records the job on every
164
+ operation (`ofp.job`) for the same reason — the node alone does not say whose work it
165
+ was. A run of a single workflow names no job and is recorded exactly as it always was.
166
+ - `--on-job-failure continue|stop` — what one job's failure does to the rest of a
167
+ `--jobs` run. `continue` (the default) stops that job alone and lets the others
168
+ finish — which is why they were planned together — while `stop` stops the whole run.
169
+ A stopped job's plate stays where it is, and the plan is made around it rather than
170
+ onto it. A single workflow is a single job, so this makes no difference to it.
151
171
  - `--env` (required) — the labcode environment: the execution environment (spec §5) plus
152
172
  the `x-labcode` extension saying *how* each operation is carried out.
153
173
  - `--boundary DOC` — the whole-workflow I/O as one document: a `boundary:` mapping with a
@@ -101,9 +101,29 @@ lc run <workflow> --env <env>
101
101
  [--seconds-per-tick S] [--op-timeout S | --no-op-timeout] [--no-probe]
102
102
  [--max-transport-legs N] [--ignore-resources] [--trace] [--mission-id ID]
103
103
  [--object-ids seeded|real]
104
+
105
+ lc run --jobs <run doc> --env <env> [--on-job-failure continue|stop] [...]
104
106
  ```
105
107
 
106
108
  - `<workflow>` — the portable v0 workflow: *what* happens.
109
+ - `--jobs RUNDOC` — run **several jobs together** in one laboratory (schedule SPEC
110
+ §6.11) in place of the single `<workflow>`. The run document names each job — an `id`,
111
+ its workflow, its own boundary, and when it may start — plus what the laboratory's own
112
+ stocks hold to begin with and which spots it is already holding. They are planned
113
+ *together*, so they compete for the same machines and share a refill neither needs
114
+ alone. Each job's workflow goes through both front doors separately and a rejection
115
+ names the job it came from; a boundary belongs to a job, so `--boundary` is not used
116
+ with `--jobs`.
117
+ 🔴 Object identities are minted **per job**: two jobs of one workflow bind the same
118
+ port names and render the same node paths, so a reproducible generator keyed on those
119
+ alone would give one job's plate the other's `_id`. `--trace` records the job on every
120
+ operation (`ofp.job`) for the same reason — the node alone does not say whose work it
121
+ was. A run of a single workflow names no job and is recorded exactly as it always was.
122
+ - `--on-job-failure continue|stop` — what one job's failure does to the rest of a
123
+ `--jobs` run. `continue` (the default) stops that job alone and lets the others
124
+ finish — which is why they were planned together — while `stop` stops the whole run.
125
+ A stopped job's plate stays where it is, and the plan is made around it rather than
126
+ onto it. A single workflow is a single job, so this makes no difference to it.
107
127
  - `--env` (required) — the labcode environment: the execution environment (spec §5) plus
108
128
  the `x-labcode` extension saying *how* each operation is carried out.
109
129
  - `--boundary DOC` — the whole-workflow I/O as one document: a `boundary:` mapping with a
@@ -59,12 +59,24 @@ python examples/render_plate_line.py
59
59
 
60
60
  🔴 **These are a record of one run, not a reproducible golden.** The labcode backend
61
61
  runs its operations out of process against a **wall clock** (`seconds_per_tick`), so
62
- which tick a subprocess finishes on varies between runs: re-rendering the same commit
63
- gives a final `now` of 10 one time and 13 the next. Do not treat a difference here as a
64
- regression, and do not gate a change on these files being byte-identical — the way the
65
- scheduler's and the runner's own artifacts can be, both of those being driven by a
66
- virtual clock. What *is* stable is the plan the scheduler produces from a given input,
67
- which the tests cover.
62
+ which tick an operation finishes on varies between runs. Do not treat a difference here
63
+ as a regression, and do not gate a change on these files being byte-identical — the way
64
+ the scheduler's and the runner's own artifacts can be, both of those being driven by a
65
+ virtual clock.
66
+
67
+ Measured on one machine, three runs of the same commit: **the same nine activities
68
+ every time**, and a makespan of 144, 128 and 137. What moves is *when* each activity
69
+ happened and, because a rolling run replans against what it has observed, *the
70
+ arrangement* the plan settles on — one run loads the plate first, the next moves the
71
+ tube first. What does not move is **what ran**, the `_id` each Object carries, and the
72
+ values the scripts produced: `outputs/plate_line.boundary.yaml` came back **byte-identical
73
+ across all three**, being the one artifact here that carries no times at all.
74
+
75
+ 🔴 It is not only the subprocesses. `LabcodeRunner` accepts an injected `monotonic` /
76
+ `sleep`, and a **scriptless** run driven by a fake clock still varied run to run
77
+ (measured) — so "inject a clock" is not the fix, and making labcode's execution output
78
+ deterministic is an open problem rather than an oversight. The plan the *scheduler*
79
+ produces from a given input is reproducible, and the tests are where that is pinned.
68
80
 
69
81
  - [`outputs/plate_line.plan.yaml`](outputs/plate_line.plan.yaml) — the **final execution
70
82
  schedule** (the §6/§7 status document: every activity, `completed`).
@@ -78,21 +90,100 @@ which the tests cover.
78
90
  boundary**, echoing the produced `od` and the returned `tube` (as `--boundary-out`
79
91
  writes it).
80
92
 
81
- Because the labcode backend runs each op out-of-process on a wall clock, the exact times
82
- (and makespan) may vary slightly between runs; the sequence and produced values do not.
83
-
84
93
  Every Object's view carries a reserved **`_id`** — labcode's implicit, value-layer Object
85
94
  identity (see [`../SPECIFICATIONS.md`](../SPECIFICATIONS.md) §4). In the observation you can
86
95
  follow the *same* Plate (one `_id`) from `load` through `dispense`/`read` to `store`, and
87
96
  the Tube's `_id` round-trips from the input boundary to the output. The ids are
88
- reproducible (a seeded, provenance-keyed generator), so these outputs are stable to
89
- re-generate.
97
+ reproducible (a seeded, provenance-keyed generator, keyed **per job** in a `--jobs`
98
+ run), so the ids in these outputs are stable to re-generate even though their times are
99
+ not.
90
100
 
91
101
  > The scripts here are mocks (they just return values / reference their locals). Replace a
92
102
  > script's body with real device calls — e.g. `robot.move(from_spot, to_spot)` in a
93
103
  > transport, or an instrument read in `read` — to drive real hardware; the code may
94
104
  > `import` anything the host Python can.
95
105
 
106
+ ## `shared_bench` — two jobs, one laboratory
107
+
108
+ `plate_line` runs one workflow. A bench does not: the morning's tubes and the
109
+ afternoon's are the same work on the same machines, and running them one after the other
110
+ leaves the reader idle while somebody fetches the next rack. `lc run --jobs` plans them
111
+ **together** (schedule SPEC §6.11), so the plan decides who gets the bench.
112
+
113
+ ```
114
+ rack.slot_a ─┐ ┌─ rack.slot_a morning
115
+ ├─ arm ─▶ reader.stage ┤
116
+ rack.slot_b ─┘ └─ rack.slot_b afternoon
117
+ ```
118
+
119
+ A slot per job, and **one reader and one arm** — the contention is the subject. Both jobs
120
+ run the *same* workflow file and differ only in their boundary: which tube, and which
121
+ slot it lives on.
122
+
123
+ - [`shared_bench.workflow.yaml`](shared_bench.workflow.yaml) — fetch a Tube, measure it,
124
+ put it back. Nothing about it is joint, which is the point: a workflow says what
125
+ happens to one piece of work.
126
+ - [`shared_bench.env.yaml`](shared_bench.env.yaml) — the laboratory, with an
127
+ `x-labcode.script` on the measure mode and on every route.
128
+ - [`shared_bench.run.yaml`](shared_bench.run.yaml) — the **run document**: the two jobs,
129
+ each naming its workflow and its boundary.
130
+ - [`shared_bench.morning.boundary.yaml`](shared_bench.morning.boundary.yaml) /
131
+ [`.afternoon.`](shared_bench.afternoon.boundary.yaml) — a boundary belongs to a *job*,
132
+ which is why the run document names one per job rather than the command line carrying
133
+ one for all of them.
134
+
135
+ ### Run it
136
+
137
+ ```sh
138
+ lc run --jobs examples/shared_bench.run.yaml --env examples/shared_bench.env.yaml --seed 0 --seconds-per-tick 0.2 --boundary-out /tmp/shared_bench.boundary.yaml
139
+ ```
140
+
141
+ `--on-job-failure continue` (the default) stops a failing job alone and lets the other
142
+ finish — which is why they were planned together; `stop` stops the whole run.
143
+
144
+ ### Produce the outputs
145
+
146
+ ```sh
147
+ python examples/render_shared_bench.py
148
+ ```
149
+
150
+ - [`outputs/shared_bench.plan.yaml`](outputs/shared_bench.plan.yaml) — the final
151
+ schedule. Every activity carries the `job` it belongs to, and the roster at the top
152
+ carries each job's promised completion.
153
+ - [`outputs/shared_bench.svg`](outputs/shared_bench.svg) — the Gantt chart. The reader's
154
+ lane is where the joint plan shows: the two jobs take turns on it rather than one run
155
+ waiting for the other to end.
156
+ - [`outputs/shared_bench.observation.yaml`](outputs/shared_bench.observation.yaml) — the
157
+ observation document, each record naming its job.
158
+ - [`outputs/shared_bench.boundary.yaml`](outputs/shared_bench.boundary.yaml) — the result
159
+ boundary, **per job**.
160
+
161
+ 🔴 **The result boundary is where to look.** Both jobs render the same node paths and
162
+ bind the same port names, so what tells their work apart is the job:
163
+
164
+ ```yaml
165
+ jobs:
166
+ morning:
167
+ boundary:
168
+ outputs:
169
+ sample: { spot: rack.slot_a, view: { label: morning, _id: d3eb614f-… } }
170
+ od: { view: 0.7 }
171
+ afternoon:
172
+ boundary:
173
+ outputs:
174
+ sample: { spot: rack.slot_b, view: { label: afternoon, _id: 9393759b-… } }
175
+ od: { view: 0.9 }
176
+ ```
177
+
178
+ Two tubes, two identities. labcode mints each Object's `_id` **per job** for exactly this
179
+ reason — a reproducible generator keyed on the node path alone would hand both tubes one
180
+ identity, silently and the same way on every run — and `--trace` records the job on every
181
+ operation (`ofp.job`) so a record says whose work it was. A run of a single workflow names
182
+ no job and is minted and recorded exactly as it always was.
183
+
184
+ The times here move between runs like every other labcode execution output (above); the
185
+ result boundary does not, carrying none.
186
+
96
187
  ## `sila2_seal` — the same idea, driven by real SiLA2 servers
97
188
 
98
189
  Where `plate_line`'s scripts are mocks that only return values, this example's scripts open
@@ -0,0 +1,33 @@
1
+ jobs:
2
+ morning:
3
+ boundary:
4
+ inputs:
5
+ sample:
6
+ spot: rack.slot_a
7
+ view:
8
+ label: morning
9
+ _id: d3eb614f-1607-4f72-946c-4e7b4ff459b9
10
+ outputs:
11
+ sample:
12
+ spot: rack.slot_a
13
+ view:
14
+ label: morning
15
+ _id: d3eb614f-1607-4f72-946c-4e7b4ff459b9
16
+ od:
17
+ view: 0.7
18
+ afternoon:
19
+ boundary:
20
+ inputs:
21
+ sample:
22
+ spot: rack.slot_b
23
+ view:
24
+ label: afternoon
25
+ _id: 9393759b-b778-4196-8be5-392dc38f1f97
26
+ outputs:
27
+ sample:
28
+ spot: rack.slot_b
29
+ view:
30
+ label: afternoon
31
+ _id: 9393759b-b778-4196-8be5-392dc38f1f97
32
+ od:
33
+ view: 0.9
@@ -0,0 +1,141 @@
1
+ schema: ofplang-observation/v0
2
+ time:
3
+ unit: second
4
+ ---
5
+ kind: transport
6
+ job: morning
7
+ from_spot: rack.slot_a
8
+ to_spot: reader.stage
9
+ transporter: arm
10
+ arc:
11
+ from:
12
+ node: []
13
+ port: sample
14
+ to:
15
+ node:
16
+ - Measure
17
+ port: sample
18
+ start: 0
19
+ end: 1
20
+ moved:
21
+ view:
22
+ label: morning
23
+ _id: d3eb614f-1607-4f72-946c-4e7b4ff459b9
24
+ ---
25
+ kind: processing
26
+ job: morning
27
+ process: measure
28
+ mode: m0
29
+ node:
30
+ - Measure
31
+ devices:
32
+ - reader
33
+ input_spots:
34
+ sample: reader.stage
35
+ output_spots:
36
+ measured: reader.stage
37
+ start: 1
38
+ end: 3
39
+ inputs:
40
+ sample:
41
+ view:
42
+ label: morning
43
+ _id: d3eb614f-1607-4f72-946c-4e7b4ff459b9
44
+ outputs:
45
+ measured:
46
+ view:
47
+ label: morning
48
+ _id: d3eb614f-1607-4f72-946c-4e7b4ff459b9
49
+ od:
50
+ view: 0.7
51
+ ---
52
+ kind: transport
53
+ job: morning
54
+ from_spot: reader.stage
55
+ to_spot: rack.slot_a
56
+ transporter: arm
57
+ arc:
58
+ from:
59
+ node:
60
+ - Measure
61
+ port: measured
62
+ to:
63
+ node: []
64
+ port: sample
65
+ start: 3
66
+ end: 5
67
+ moved:
68
+ view:
69
+ label: morning
70
+ _id: d3eb614f-1607-4f72-946c-4e7b4ff459b9
71
+ ---
72
+ kind: transport
73
+ job: afternoon
74
+ from_spot: rack.slot_b
75
+ to_spot: reader.stage
76
+ transporter: arm
77
+ arc:
78
+ from:
79
+ node: []
80
+ port: sample
81
+ to:
82
+ node:
83
+ - Measure
84
+ port: sample
85
+ start: 5
86
+ end: 7
87
+ moved:
88
+ view:
89
+ label: afternoon
90
+ _id: 9393759b-b778-4196-8be5-392dc38f1f97
91
+ ---
92
+ kind: processing
93
+ job: afternoon
94
+ process: measure
95
+ mode: m0
96
+ node:
97
+ - Measure
98
+ devices:
99
+ - reader
100
+ input_spots:
101
+ sample: reader.stage
102
+ output_spots:
103
+ measured: reader.stage
104
+ start: 7
105
+ end: 9
106
+ inputs:
107
+ sample:
108
+ view:
109
+ label: afternoon
110
+ _id: 9393759b-b778-4196-8be5-392dc38f1f97
111
+ outputs:
112
+ measured:
113
+ view:
114
+ label: afternoon
115
+ _id: 9393759b-b778-4196-8be5-392dc38f1f97
116
+ od:
117
+ view: 0.9
118
+ ---
119
+ kind: transport
120
+ job: afternoon
121
+ from_spot: reader.stage
122
+ to_spot: rack.slot_b
123
+ transporter: arm
124
+ arc:
125
+ from:
126
+ node:
127
+ - Measure
128
+ port: measured
129
+ to:
130
+ node: []
131
+ port: sample
132
+ start: 9
133
+ end: 10
134
+ moved:
135
+ view:
136
+ label: afternoon
137
+ _id: 9393759b-b778-4196-8be5-392dc38f1f97
138
+ ---
139
+ final: true
140
+ now: 10
141
+ outcome: completed
@@ -0,0 +1,134 @@
1
+ time:
2
+ unit: second
3
+ now: 10
4
+ jobs:
5
+ - id: morning
6
+ release: 0
7
+ bound: 10
8
+ fingerprint: ec69dad347c6ba08
9
+ interface:
10
+ inputs:
11
+ sample: rack.slot_a
12
+ outputs:
13
+ sample: rack.slot_a
14
+ - id: afternoon
15
+ release: 0
16
+ bound: 20
17
+ fingerprint: ec69dad347c6ba08
18
+ interface:
19
+ inputs:
20
+ sample: rack.slot_b
21
+ outputs:
22
+ sample: rack.slot_b
23
+ objective:
24
+ kind:
25
+ - completion_time_sum
26
+ - makespan
27
+ value:
28
+ - 15
29
+ - 10
30
+ activities:
31
+ - kind: transport
32
+ job: morning
33
+ status: completed
34
+ start: 0
35
+ end: 1
36
+ from_spot: rack.slot_a
37
+ to_spot: reader.stage
38
+ transporter: arm
39
+ arc:
40
+ from:
41
+ node: []
42
+ port: sample
43
+ to:
44
+ node:
45
+ - Measure
46
+ port: sample
47
+ seq: 0
48
+ - kind: processing
49
+ job: morning
50
+ status: completed
51
+ start: 1
52
+ end: 3
53
+ process: measure
54
+ mode: m0
55
+ node:
56
+ - Measure
57
+ devices:
58
+ - reader
59
+ input_spots:
60
+ sample: reader.stage
61
+ output_spots:
62
+ measured: reader.stage
63
+ - kind: transport
64
+ job: morning
65
+ status: completed
66
+ start: 3
67
+ end: 5
68
+ from_spot: reader.stage
69
+ to_spot: rack.slot_a
70
+ transporter: arm
71
+ arc:
72
+ from:
73
+ node:
74
+ - Measure
75
+ port: measured
76
+ to:
77
+ node: []
78
+ port: sample
79
+ seq: 0
80
+ - kind: transport
81
+ job: afternoon
82
+ status: completed
83
+ start: 5
84
+ end: 7
85
+ from_spot: rack.slot_b
86
+ to_spot: reader.stage
87
+ transporter: arm
88
+ arc:
89
+ from:
90
+ node: []
91
+ port: sample
92
+ to:
93
+ node:
94
+ - Measure
95
+ port: sample
96
+ seq: 0
97
+ - kind: processing
98
+ job: afternoon
99
+ status: completed
100
+ start: 7
101
+ end: 9
102
+ process: measure
103
+ mode: m0
104
+ node:
105
+ - Measure
106
+ devices:
107
+ - reader
108
+ input_spots:
109
+ sample: reader.stage
110
+ output_spots:
111
+ measured: reader.stage
112
+ - kind: transport
113
+ job: afternoon
114
+ status: completed
115
+ start: 9
116
+ end: 10
117
+ from_spot: reader.stage
118
+ to_spot: rack.slot_b
119
+ transporter: arm
120
+ arc:
121
+ from:
122
+ node:
123
+ - Measure
124
+ port: measured
125
+ to:
126
+ node: []
127
+ port: sample
128
+ seq: 0
129
+ meta:
130
+ workflow:
131
+ - <in-memory>
132
+ - <in-memory>
133
+ environment: C:\Users\kaizu\Documents\Python Scripts\ofplang-labcode\examples\shared_bench.env.yaml
134
+ status: <in-memory>
@@ -0,0 +1,2 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 784 206" width="784" height="206" font-family="system-ui, sans-serif" font-size="12"><text fill="#1a1a1a" font-size="15" font-weight="600" x="16" y="24">Schedule &#8212; device view</text><text fill="#666666" x="16" y="40">makespan 10 second</text><defs><marker id="ah" markerWidth="8" markerHeight="8" refX="7" refY="3" orient="auto"><path d="M0,0 L7,3 L0,6 Z" fill="#9333ea"/></marker></defs><g transform="translate(0,48)"><line stroke="#b3b3b3" stroke-width="1" x1="200.0" y1="34.0" x2="200.0" y2="130.0"/><text fill="#666666" x="200.0" y="28.0" text-anchor="middle">0</text><line stroke="#b3b3b3" stroke-width="1" x1="256.0" y1="34.0" x2="256.0" y2="130.0"/><text fill="#666666" x="256.0" y="28.0" text-anchor="middle">1</text><line stroke="#b3b3b3" stroke-width="1" x1="312.0" y1="34.0" x2="312.0" y2="130.0"/><text fill="#666666" x="312.0" y="28.0" text-anchor="middle">2</text><line stroke="#b3b3b3" stroke-width="1" x1="368.0" y1="34.0" x2="368.0" y2="130.0"/><text fill="#666666" x="368.0" y="28.0" text-anchor="middle">3</text><line stroke="#b3b3b3" stroke-width="1" x1="424.0" y1="34.0" x2="424.0" y2="130.0"/><text fill="#666666" x="424.0" y="28.0" text-anchor="middle">4</text><line stroke="#b3b3b3" stroke-width="1" x1="480.0" y1="34.0" x2="480.0" y2="130.0"/><text fill="#666666" x="480.0" y="28.0" text-anchor="middle">5</text><line stroke="#b3b3b3" stroke-width="1" x1="536.0" y1="34.0" x2="536.0" y2="130.0"/><text fill="#666666" x="536.0" y="28.0" text-anchor="middle">6</text><line stroke="#b3b3b3" stroke-width="1" x1="592.0" y1="34.0" x2="592.0" y2="130.0"/><text fill="#666666" x="592.0" y="28.0" text-anchor="middle">7</text><line stroke="#b3b3b3" stroke-width="1" x1="648.0" y1="34.0" x2="648.0" y2="130.0"/><text fill="#666666" x="648.0" y="28.0" text-anchor="middle">8</text><line stroke="#b3b3b3" stroke-width="1" x1="704.0" y1="34.0" x2="704.0" y2="130.0"/><text fill="#666666" x="704.0" y="28.0" text-anchor="middle">9</text><line stroke="#b3b3b3" stroke-width="1" x1="760.0" y1="34.0" x2="760.0" y2="130.0"/><text fill="#666666" x="760.0" y="28.0" text-anchor="middle">10</text><text fill="#666666" x="200.0" y="150.0">time (second)</text><line stroke="#d9d9d9" stroke-width="1" x1="0" y1="70.0" x2="784.0" y2="70.0"/><text fill="#1a1a1a" x="190.0" y="59.0" text-anchor="end">rack</text><line stroke="#d9d9d9" stroke-width="1" x1="0" y1="100.0" x2="784.0" y2="100.0"/><text fill="#1a1a1a" x="190.0" y="89.0" text-anchor="end">reader</text><line stroke="#d9d9d9" stroke-width="1" x1="0" y1="130.0" x2="784.0" y2="130.0"/><text fill="#1a1a1a" x="190.0" y="119.0" text-anchor="end">arm (transporter)</text><line stroke="#dc2626" stroke-width="1.5" stroke-dasharray="4 3" x1="760.0" y1="34.0" x2="760.0" y2="130.0"/><text fill="#dc2626" x="763.0" y="28.0">now=10</text><rect fill="#f59e0b" x="200.0" y="105.0" width="56.0" height="20" rx="3"/><text fill="#3a2a00" x="204.0" y="119.0">&gt; morn…</text><rect fill="#f59e0b" fill-opacity="0.2" x="200.0" y="45.0" width="56.0" height="20" rx="3"/><rect fill="#f59e0b" fill-opacity="0.2" x="200.0" y="75.0" width="56.0" height="20" rx="3"/><rect fill="#3b82f6" x="256.0" y="75.0" width="112.0" height="20" rx="3"/><text fill="#ffffff" x="260.0" y="89.0">morning:Measure</text><rect fill="#f59e0b" x="368.0" y="105.0" width="112.0" height="20" rx="3"/><text fill="#3a2a00" x="372.0" y="119.0">&gt; morning:trans…</text><rect fill="#f59e0b" fill-opacity="0.2" x="368.0" y="75.0" width="112.0" height="20" rx="3"/><rect fill="#f59e0b" fill-opacity="0.2" x="368.0" y="45.0" width="112.0" height="20" rx="3"/><rect fill="#f59e0b" x="480.0" y="105.0" width="112.0" height="20" rx="3"/><text fill="#3a2a00" x="484.0" y="119.0">&gt; afternoon:Mea…</text><rect fill="#f59e0b" fill-opacity="0.2" x="480.0" y="45.0" width="112.0" height="20" rx="3"/><rect fill="#f59e0b" fill-opacity="0.2" x="480.0" y="75.0" width="112.0" height="20" rx="3"/><rect fill="#3b82f6" x="592.0" y="75.0" width="112.0" height="20" rx="3"/><text fill="#ffffff" x="596.0" y="89.0">afternoon:Measu…</text><rect fill="#f59e0b" x="704.0" y="105.0" width="56.0" height="20" rx="3"/><text fill="#3a2a00" x="708.0" y="119.0">&gt; afte…</text><rect fill="#f59e0b" fill-opacity="0.2" x="704.0" y="75.0" width="56.0" height="20" rx="3"/><rect fill="#f59e0b" fill-opacity="0.2" x="704.0" y="45.0" width="56.0" height="20" rx="3"/></g></svg>
@@ -0,0 +1,109 @@
1
+ """Run the `shared_bench` example -- two jobs in one laboratory -- and write its outputs.
2
+
3
+ The companion of `render_plate_line.py`, driving `labcode.run_labcode` exactly as
4
+ `lc run --jobs examples/shared_bench.run.yaml --env examples/shared_bench.env.yaml`
5
+ does. Where that example runs one workflow down a line, this one runs the *same* workflow
6
+ twice at once and lets the plan sort out who gets the bench:
7
+
8
+ rack.slot_a ─┐ ┌─ rack.slot_a morning
9
+ ├─ arm ─▶ reader.stage ┤
10
+ rack.slot_b ─┘ └─ rack.slot_b afternoon
11
+
12
+ One reader and one arm, a slot per job. The two jobs are planned **together** (schedule
13
+ SPEC §6.11), so they take turns on the reader instead of one run waiting for the other
14
+ to finish -- which is what planning them together is for.
15
+
16
+ 🔴 What to look at: the two jobs run the same file, so every node path and port name
17
+ they render is identical. What tells their work apart is the **job** -- each Object's
18
+ `_id` is minted per job (see the printed result boundary: two different tubes), and
19
+ `--trace` records the job on every operation. Without that a reproducible generator
20
+ would hand both tubes one identity.
21
+
22
+ Run it:
23
+
24
+ python examples/render_shared_bench.py
25
+
26
+ It writes four artifacts under examples/outputs/:
27
+ - shared_bench.plan.yaml -- the final execution schedule (§6/§7), every activity
28
+ carrying the `job` it belongs to
29
+ - shared_bench.observation.yaml -- the observation document (D38), likewise per job
30
+ - shared_bench.svg -- a Gantt chart (device view): the reader's lane shows
31
+ the two jobs queueing for it
32
+ - shared_bench.boundary.yaml -- the result boundary, **per job**: each tube back on
33
+ its own slot, with its own `_id` and its own `od`
34
+
35
+ 🔴 These are a record of one run, not a golden: labcode runs out of process on a wall
36
+ clock, so the times and the arrangement move between runs (examples/README.md). The one
37
+ artifact that does not is the result boundary, which carries no times.
38
+
39
+ Requires the sibling `ofplang-schedule` (the runner replans through it, and its
40
+ visualizer draws the SVG).
41
+ """
42
+
43
+ from __future__ import annotations
44
+
45
+ from pathlib import Path
46
+
47
+ import yaml
48
+ from ofplang.run.runner import load_document, parse_run_document, serialize_document
49
+ from ofplang.schedule.scheduler.visualize import render_svg
50
+
51
+ from labcode.runner import run_labcode
52
+
53
+ HERE = Path(__file__).parent
54
+ OUT = HERE / "outputs"
55
+ RUN_DOCUMENT = HERE / "shared_bench.run.yaml"
56
+ ENVIRONMENT = HERE / "shared_bench.env.yaml"
57
+
58
+ # As in render_plate_line.py: a small real-seconds-per-tick keeps the out-of-process demo
59
+ # quick. The reader takes 6 ticks and each move 2, so two jobs sharing one reader is a
60
+ # few seconds of wall clock.
61
+ SECONDS_PER_TICK = 0.2
62
+
63
+
64
+ def main() -> None:
65
+ OUT.mkdir(exist_ok=True)
66
+
67
+ # The run document is the joint entry: which jobs, each one's workflow and boundary.
68
+ # Parsed the way `lc run --jobs` parses it, relative to this directory.
69
+ run_doc = parse_run_document(
70
+ load_document(str(RUN_DOCUMENT)), RUN_DOCUMENT.parent
71
+ )
72
+
73
+ # `run_labcode` takes the roster in place of a workflow. It does per job what it does
74
+ # for a lone workflow -- inject `_id` into the Object types, mint the boundary's ids,
75
+ # share one IdGenerator with the backend -- and keys the minting by the job, so the
76
+ # two tubes are two tubes.
77
+ result = run_labcode(
78
+ list(run_doc.jobs),
79
+ str(ENVIRONMENT),
80
+ # No boundary for the run: a boundary belongs to a job, and each carries its own.
81
+ poll_interval=1,
82
+ running_task_margin=1,
83
+ random_seed=0,
84
+ seconds_per_tick=SECONDS_PER_TICK,
85
+ observation_out=str(OUT / "shared_bench.observation.yaml"),
86
+ # The default, spelled out because it is the interesting one here: if the morning
87
+ # tube is dropped, the afternoon's still gets measured.
88
+ on_job_failure="continue",
89
+ )
90
+
91
+ status = result.status
92
+ status.setdefault("objective", {"kind": "makespan", "value": status.get("now")})
93
+
94
+ (OUT / "shared_bench.plan.yaml").write_text(
95
+ serialize_document(status), encoding="utf-8"
96
+ )
97
+ (OUT / "shared_bench.svg").write_text(
98
+ render_svg(status, view="device"), encoding="utf-8"
99
+ )
100
+ (OUT / "shared_bench.boundary.yaml").write_text(
101
+ serialize_document(result.result_boundary), encoding="utf-8"
102
+ )
103
+
104
+ print(f"makespan: {status.get('now')}")
105
+ print(yaml.safe_dump(result.result_boundary, sort_keys=False).rstrip())
106
+
107
+
108
+ if __name__ == "__main__":
109
+ main()
@@ -0,0 +1,15 @@
1
+ # The afternoon job's boundary: its tube starts on `slot_b` and goes back there.
2
+ #
3
+ # A boundary belongs to a *job*, not to the run -- which is why a run document names one
4
+ # per job rather than the command line carrying one for all of them. The two jobs here
5
+ # differ in exactly this file: same workflow, same laboratory, different slot and
6
+ # different tube.
7
+ boundary:
8
+ inputs:
9
+ sample:
10
+ spot: rack.slot_b
11
+ view:
12
+ label: afternoon
13
+ outputs:
14
+ sample: { spot: rack.slot_b }
15
+ od: {}