labcode 0.3.0__py3-none-any.whl → 0.4.0__py3-none-any.whl
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/backend.py +2 -2
- labcode/dialect.py +66 -20
- labcode/extension.py +32 -0
- labcode/run_cli.py +12 -0
- {labcode-0.3.0.dist-info → labcode-0.4.0.dist-info}/METADATA +14 -4
- {labcode-0.3.0.dist-info → labcode-0.4.0.dist-info}/RECORD +10 -10
- {labcode-0.3.0.dist-info → labcode-0.4.0.dist-info}/WHEEL +0 -0
- {labcode-0.3.0.dist-info → labcode-0.4.0.dist-info}/entry_points.txt +0 -0
- {labcode-0.3.0.dist-info → labcode-0.4.0.dist-info}/licenses/LICENSE +0 -0
- {labcode-0.3.0.dist-info → labcode-0.4.0.dist-info}/top_level.txt +0 -0
labcode/backend.py
CHANGED
|
@@ -57,6 +57,7 @@ from labcode.extension import (
|
|
|
57
57
|
script_endpoints,
|
|
58
58
|
script_flavor,
|
|
59
59
|
spot_device,
|
|
60
|
+
transport_label,
|
|
60
61
|
transporter_connections,
|
|
61
62
|
)
|
|
62
63
|
from labcode.idgen import DEFAULT_ID_GENERATOR, IdGenerator
|
|
@@ -245,8 +246,7 @@ def make_transport_resolver(environment: dict) -> Callable:
|
|
|
245
246
|
code = _flavored(
|
|
246
247
|
code, script,
|
|
247
248
|
_transport_machines(transport, transporters, devices, script),
|
|
248
|
-
|
|
249
|
-
f"{transport.get('from')} -> {transport.get('to')}",
|
|
249
|
+
transport_label(transport),
|
|
250
250
|
)
|
|
251
251
|
routes[key] = code
|
|
252
252
|
|
labcode/dialect.py
CHANGED
|
@@ -59,8 +59,11 @@ from labcode.extension import (
|
|
|
59
59
|
parse_connection,
|
|
60
60
|
parse_op_timeout,
|
|
61
61
|
parse_probe,
|
|
62
|
+
route_transporter,
|
|
63
|
+
script_endpoints,
|
|
62
64
|
script_flavor,
|
|
63
65
|
spot_device,
|
|
66
|
+
transport_label,
|
|
64
67
|
unknown_key_messages,
|
|
65
68
|
)
|
|
66
69
|
from labcode.objectid import RESERVED_ID, reserved_collisions
|
|
@@ -464,11 +467,8 @@ def _validate_replenishments(environment: dict, errors: list, warnings: list) ->
|
|
|
464
467
|
)
|
|
465
468
|
|
|
466
469
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
f"transport {transport.get('transporter')!r} "
|
|
470
|
-
f"{transport.get('from')} -> {transport.get('to')}"
|
|
471
|
-
)
|
|
470
|
+
# `transport_label` (extension.py) is shared with the backend, so the same route is named
|
|
471
|
+
# the same way in a front-door diagnostic and in a failing operation.
|
|
472
472
|
|
|
473
473
|
|
|
474
474
|
def _validate_transports(
|
|
@@ -485,7 +485,7 @@ def _validate_transports(
|
|
|
485
485
|
for transport in environment.get("transports") or []:
|
|
486
486
|
if not isinstance(transport, dict):
|
|
487
487
|
continue
|
|
488
|
-
label =
|
|
488
|
+
label = transport_label(transport)
|
|
489
489
|
extension = transport.get(EXTENSION_KEY)
|
|
490
490
|
if extension is None:
|
|
491
491
|
if transport.get("from") != transport.get("to"): # a real move
|
|
@@ -505,7 +505,9 @@ def _validate_transports(
|
|
|
505
505
|
continue
|
|
506
506
|
_check_transport_endpoints(transport, script, label, devices, errors, warnings)
|
|
507
507
|
if script_flavor(script) == FLAVOR_SILA2:
|
|
508
|
-
_check_transport_connection(
|
|
508
|
+
_check_transport_connection(
|
|
509
|
+
transport, script, label, transporters, devices, errors
|
|
510
|
+
)
|
|
509
511
|
|
|
510
512
|
|
|
511
513
|
def _check_transport_endpoints(
|
|
@@ -519,7 +521,12 @@ def _check_transport_endpoints(
|
|
|
519
521
|
clients at all (§1.6), so the request cannot be honoured and the author expects
|
|
520
522
|
something that will not happen. Asking when neither end has an address is a **warning**
|
|
521
523
|
-- the route still works through its transporter, and an environment written before its
|
|
522
|
-
instruments have addresses is a legitimate intermediate state (as with `probe`).
|
|
524
|
+
instruments have addresses is a legitimate intermediate state (as with `probe`).
|
|
525
|
+
|
|
526
|
+
That last one holds only for a route that *has* a transporter to fall back on. A route
|
|
527
|
+
with no transporter (§4.6/§5.4) has nothing else to drive, so the same situation is an
|
|
528
|
+
error there -- reported by `_check_transport_connection`, which is where the rest of
|
|
529
|
+
that route's connection requirement lives, so that one problem yields one message."""
|
|
523
530
|
endpoints = script.get("endpoints")
|
|
524
531
|
prefix = f"{label}: x-labcode.script.endpoints"
|
|
525
532
|
if endpoints is None:
|
|
@@ -535,8 +542,9 @@ def _check_transport_endpoints(
|
|
|
535
542
|
f"{script_flavor(script)!r}: only a 'sila2' script is handed clients (§1.6)"
|
|
536
543
|
)
|
|
537
544
|
return
|
|
538
|
-
|
|
539
|
-
|
|
545
|
+
if route_transporter(transport) is None:
|
|
546
|
+
return # nothing to fall back on; _check_transport_connection reports it as an error
|
|
547
|
+
named = _route_ends(transport)
|
|
540
548
|
if not any(device in devices.with_connection for device in named):
|
|
541
549
|
warnings.append(
|
|
542
550
|
f"{prefix} is true, but neither end of the route ({', '.join(named)}) declares "
|
|
@@ -544,16 +552,54 @@ def _check_transport_endpoints(
|
|
|
544
552
|
)
|
|
545
553
|
|
|
546
554
|
|
|
555
|
+
def _route_ends(transport: dict) -> list[str]:
|
|
556
|
+
"""The devices at either end of a route, named and deduplicated in route order.
|
|
557
|
+
|
|
558
|
+
A move within one device names it once: it is one machine, and one client."""
|
|
559
|
+
ends = [spot_device(transport.get(end)) for end in ("from", "to")]
|
|
560
|
+
return list(dict.fromkeys(device for device in ends if device is not None))
|
|
561
|
+
|
|
562
|
+
|
|
547
563
|
def _check_transport_connection(
|
|
548
|
-
transport: dict,
|
|
564
|
+
transport: dict,
|
|
565
|
+
script: dict,
|
|
566
|
+
label: str,
|
|
567
|
+
transporters: _NodeIndex,
|
|
568
|
+
devices: _NodeIndex,
|
|
569
|
+
errors: list,
|
|
549
570
|
) -> None:
|
|
550
|
-
"""
|
|
551
|
-
|
|
552
|
-
|
|
571
|
+
"""Where a `sila2` transport script's connection has to be.
|
|
572
|
+
|
|
573
|
+
Normally the transporter: it is the machine that carries the move, and the one
|
|
574
|
+
`sila2_client` names.
|
|
575
|
+
|
|
576
|
+
A route with **no transporter** (`transporter: null`, ofplang-schedule §4.6/§5.4) is
|
|
577
|
+
performed by the devices at its ends -- a cycler loading its own block -- so those are
|
|
578
|
+
what the script must be able to reach. It receives them only if it asks (`endpoints`,
|
|
579
|
+
§1.6), and it is made to ask rather than being read as asking: `endpoints` is the
|
|
580
|
+
author's statement of which machines the script drives, and inferring it here would take
|
|
581
|
+
that statement away from them for exactly the routes where it matters most. The cost of
|
|
582
|
+
requiring it is one line in the environment; the cost of assuming it is a script handed
|
|
583
|
+
clients its author never asked for."""
|
|
553
584
|
prefix = f"{label}: x-labcode.script.flavor 'sila2' but"
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
585
|
+
identifier = route_transporter(transport)
|
|
586
|
+
if identifier is not None:
|
|
587
|
+
if identifier not in transporters.declared:
|
|
588
|
+
errors.append(f"{prefix} transporter {identifier!r} is not declared in transporters[]")
|
|
589
|
+
elif identifier not in transporters.with_connection:
|
|
590
|
+
errors.append(f"{prefix} transporter {identifier!r} declares no x-labcode.connection")
|
|
591
|
+
return
|
|
592
|
+
if not script_endpoints(script):
|
|
593
|
+
errors.append(
|
|
594
|
+
f"{prefix} the route names no transporter and the script does not ask for its "
|
|
595
|
+
f"endpoint clients; add `endpoints: true` so it is handed the devices at either "
|
|
596
|
+
f"end of the route, which are what performs a move nothing carries"
|
|
597
|
+
)
|
|
598
|
+
return
|
|
599
|
+
named = _route_ends(transport)
|
|
600
|
+
if not any(device in devices.with_connection for device in named):
|
|
601
|
+
errors.append(
|
|
602
|
+
f"{prefix} the route names no transporter and neither end "
|
|
603
|
+
f"({', '.join(named)}) declares an x-labcode.connection; there is no machine "
|
|
604
|
+
f"left for the script to drive"
|
|
605
|
+
)
|
labcode/extension.py
CHANGED
|
@@ -315,6 +315,38 @@ def declared_probe(extension: Any) -> dict[str, Any]:
|
|
|
315
315
|
return declared
|
|
316
316
|
|
|
317
317
|
|
|
318
|
+
def route_transporter(transport: Any) -> str | None:
|
|
319
|
+
"""The transporter carrying a transport route, or None when the route names none.
|
|
320
|
+
|
|
321
|
+
A route may declare ``transporter: null`` (ofplang-schedule SPECIFICATIONS §4.6/§5.4):
|
|
322
|
+
the move needs no transporter at all -- a device shifting material between its own
|
|
323
|
+
spots, a chute -- and is performed by the devices at its ends instead.
|
|
324
|
+
|
|
325
|
+
Anything that is not a non-empty string reads as "none" here. A malformed value is the
|
|
326
|
+
schedule environment validator's to reject, and this must not hold a second opinion
|
|
327
|
+
about it; what matters at this layer is only whether there is a transporter to drive.
|
|
328
|
+
"""
|
|
329
|
+
if not isinstance(transport, dict):
|
|
330
|
+
return None
|
|
331
|
+
identifier = transport.get("transporter")
|
|
332
|
+
return identifier if isinstance(identifier, str) and identifier else None
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def transport_label(transport: Any) -> str:
|
|
336
|
+
"""How a transport route is named in a message: its carrier, then its ends.
|
|
337
|
+
|
|
338
|
+
A route with no transporter says so in words. Rendering the absent id would print
|
|
339
|
+
``transport None a -> b``, which reads as a defect in the message rather than as a fact
|
|
340
|
+
about the route -- and this label reaches the user in both a front-door diagnostic and a
|
|
341
|
+
failing operation, so it is written once and shared.
|
|
342
|
+
"""
|
|
343
|
+
if not isinstance(transport, dict):
|
|
344
|
+
transport = {}
|
|
345
|
+
identifier = route_transporter(transport)
|
|
346
|
+
who = repr(identifier) if identifier is not None else "(no transporter)"
|
|
347
|
+
return f"transport {who} {transport.get('from')} -> {transport.get('to')}"
|
|
348
|
+
|
|
349
|
+
|
|
318
350
|
def spot_device(spot: Any) -> str | None:
|
|
319
351
|
"""The device a qualified spot (``<device>.<spot>``, schedule SPECIFICATIONS §8.2)
|
|
320
352
|
belongs to, or None when there is no name to take.
|
labcode/run_cli.py
CHANGED
|
@@ -65,6 +65,13 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
65
65
|
"--margin", type=int, default=None, metavar="M",
|
|
66
66
|
help="running-task margin for replans (default: the poll interval)",
|
|
67
67
|
)
|
|
68
|
+
p.add_argument(
|
|
69
|
+
"--max-transport-legs", type=int, default=1, metavar="N",
|
|
70
|
+
help="how many transport activities one Object-bearing arc may be carried in "
|
|
71
|
+
"(default 1: the single hop), joined by relays -- raise it for an "
|
|
72
|
+
"instrument the transporter reaches at one position only, or a plate "
|
|
73
|
+
"that has to cross a hand-off station",
|
|
74
|
+
)
|
|
68
75
|
p.add_argument(
|
|
69
76
|
"--poll-interval", type=int, default=1, metavar="D",
|
|
70
77
|
help="poll every D time units (default 1)",
|
|
@@ -329,6 +336,11 @@ def main(argv: Sequence[str] | None = None) -> int:
|
|
|
329
336
|
running_task_margin=args.margin,
|
|
330
337
|
random_seed=args.seed,
|
|
331
338
|
poll_interval=args.poll_interval,
|
|
339
|
+
# How many moves one arc may be carried in (schedule SPEC §6.4.1). Passed
|
|
340
|
+
# through rather than decided here: it is the scheduler's offer, and a real
|
|
341
|
+
# laboratory is exactly where an instrument reachable from one position
|
|
342
|
+
# only makes the single hop insufficient.
|
|
343
|
+
max_transport_legs=args.max_transport_legs,
|
|
332
344
|
# 0 is "no limit" here as it is upstream; the runner spells that None.
|
|
333
345
|
max_ticks=args.max_ticks or None,
|
|
334
346
|
seconds_per_tick=args.seconds_per_tick,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: labcode
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.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.
|
|
25
|
-
Requires-Dist: ofplang-run<0.
|
|
24
|
+
Requires-Dist: ofplang-schedule<0.9,>=0.8
|
|
25
|
+
Requires-Dist: ofplang-run<0.8,>=0.7
|
|
26
26
|
Provides-Extra: test
|
|
27
27
|
Requires-Dist: pytest>=7.0; extra == "test"
|
|
28
28
|
Requires-Dist: opentelemetry-sdk>=1.20; extra == "test"
|
|
@@ -143,7 +143,8 @@ described below.
|
|
|
143
143
|
lc run <workflow> --env <env>
|
|
144
144
|
[--boundary DOC] [-o OUT] [--boundary-out FILE] [--observation-out FILE]
|
|
145
145
|
[--seconds-per-tick S] [--op-timeout S | --no-op-timeout] [--no-probe]
|
|
146
|
-
[--ignore-resources] [--trace] [--mission-id ID]
|
|
146
|
+
[--max-transport-legs N] [--ignore-resources] [--trace] [--mission-id ID]
|
|
147
|
+
[--object-ids seeded|real]
|
|
147
148
|
```
|
|
148
149
|
|
|
149
150
|
- `<workflow>` — the portable v0 workflow: *what* happens.
|
|
@@ -176,6 +177,15 @@ lc run <workflow> --env <env>
|
|
|
176
177
|
else 7200 real seconds. The two forms exclude each other.
|
|
177
178
|
- `--no-probe` — ignore the environment's `x-labcode.probe` policies and treat every
|
|
178
179
|
machine as reachable (§1.6). The documents are still validated.
|
|
180
|
+
- `--max-transport-legs N` — how many transport activities one Object-bearing arc may
|
|
181
|
+
be carried in (schedule spec §6.4.1), joined by **relay** activities. It is 1 by
|
|
182
|
+
default: the single hop. Raise it where the transporter cannot reach an instrument
|
|
183
|
+
from where the plate is — a bench it serves from one position only, or a hand-off
|
|
184
|
+
station a plate has to cross — and the move is planned as the shortest chain between
|
|
185
|
+
the two spots instead of being reported unreachable. Only the fewest possible moves
|
|
186
|
+
are offered, so a plate one move away is never sent round by way of somewhere else.
|
|
187
|
+
This describes the laboratory's reach rather than tuning the loop, which is why it is
|
|
188
|
+
here and not among the replan knobs below.
|
|
179
189
|
- `--ignore-resources` — switch the consumable model off. The environment's resource
|
|
180
190
|
declarations are still checked for shape but none is applied, so a bench whose devices
|
|
181
191
|
declare stocks nobody is tracking runs without the boundary saying what they held.
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
labcode/__init__.py,sha256=jS_lhcLqdu2BfOAq6j5jPTvGNY5M3_1RMJtNRr3ABp4,79
|
|
2
2
|
labcode/__main__.py,sha256=T5DM7E3tdcuKP4oQ7JacBP4Mi-VmujWueJvB9yCIQTc,252
|
|
3
3
|
labcode/_child.py,sha256=dXsOyvX9J39-G9Y5XuXsT0Cp-gzZ6v0t4iKIHruIbRU,3695
|
|
4
|
-
labcode/backend.py,sha256=
|
|
4
|
+
labcode/backend.py,sha256=kkJ1DcJQciM-86fTQWyy0N-wAEVyo5JJ3HuVRzDiEuY,33630
|
|
5
5
|
labcode/cli.py,sha256=paeYARy1BvACCf0lmlZLdgWyCgq3VyywSpvUKsGI0Mc,3538
|
|
6
|
-
labcode/dialect.py,sha256=
|
|
7
|
-
labcode/extension.py,sha256=
|
|
6
|
+
labcode/dialect.py,sha256=x6defFkzP-fv4fi2SG-Qt8XxJRuF1NXhiOXxcKFyx3k,27468
|
|
7
|
+
labcode/extension.py,sha256=4qrw4jEMDNrj-l66HWNlBsy-bWHStSIe7CMcrj41-LQ,21377
|
|
8
8
|
labcode/idgen.py,sha256=MYCke8ytQU1GnwNVq2sPwlvzqrs0JS-FZzgs5MoosFw,2850
|
|
9
9
|
labcode/objectid.py,sha256=NQHZ81WCRwCXF7SwdXPUg2Q8ln4UJ936kepxHQAxUS0,10620
|
|
10
10
|
labcode/otel.py,sha256=FvMIb07nn6OSSuAjixA898AxXp8OcSD-iNJYboa_pyI,14713
|
|
@@ -12,14 +12,14 @@ labcode/otel_sila2.py,sha256=t2H8x_jH2cysWRQ5joX6b8cqFDArALYcObEPTXHzepw,8612
|
|
|
12
12
|
labcode/probe.py,sha256=niUm3qrHdKBq0DCbVTh3ThmBlIn0GIkx_zDSqKrPbfs,8305
|
|
13
13
|
labcode/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
14
|
labcode/record.py,sha256=TuqalknpDeOkOCjfoLuW_8cSMGou9NZVJK7x1N-Z6kA,12094
|
|
15
|
-
labcode/run_cli.py,sha256=
|
|
15
|
+
labcode/run_cli.py,sha256=7VUdOWoxdofbw5UI32UeWCH-sEPGHRHAjjau-bgLiZA,18897
|
|
16
16
|
labcode/runner.py,sha256=Ux_gkgAxOepuKO9zRicoeCe-YG5ycKjqRd8BY14ovxk,11277
|
|
17
17
|
labcode/sila2.py,sha256=jd_f2ARVvH96As5HCa9qAG6cDERPpsvjg_ZYioDyI7U,13477
|
|
18
18
|
labcode/sila2_commands.py,sha256=oYuqUDUKcN5sxo2QvxtkNqRfvietrHkLg7Yqvcj1eZ4,5349
|
|
19
19
|
labcode/sila2_instrument.py,sha256=Lne4AO257UfAHprh4Pmlqg8jVLng8haeJhMVfSfMfdE,24564
|
|
20
|
-
labcode-0.
|
|
21
|
-
labcode-0.
|
|
22
|
-
labcode-0.
|
|
23
|
-
labcode-0.
|
|
24
|
-
labcode-0.
|
|
25
|
-
labcode-0.
|
|
20
|
+
labcode-0.4.0.dist-info/licenses/LICENSE,sha256=weUnWoHlmaiawSjaLy_ECNpyTe7F5vDBfLGJKocpUhI,1071
|
|
21
|
+
labcode-0.4.0.dist-info/METADATA,sha256=yuxDVZ9Wg58EwEl2YV4mGFr7Qo7k624uaTpVKQMOKTc,16952
|
|
22
|
+
labcode-0.4.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
23
|
+
labcode-0.4.0.dist-info/entry_points.txt,sha256=eP2fNwGx6ZYNJ_AWzGZhiDJaPqeqaA0iZAc-Tae1PGQ,40
|
|
24
|
+
labcode-0.4.0.dist-info/top_level.txt,sha256=7WhrsXy6psZ8TDzksIEEuj2q93GeXWkHL9h1RbdBrJg,8
|
|
25
|
+
labcode-0.4.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|