Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 49 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ autonomous-lab vision # what a camera catches; what none
autonomous-lab throughput single_cell_genomics # plates/day, or why that number does not exist
autonomous-lab provenance single_cell_genomics # what could be proven about a run afterwards
autonomous-lab lineage single_cell_genomics # which cell did this read come from?
autonomous-lab schedule single_cell_genomics --plates 10 # what serializes across a workcell?
autonomous-lab knowledge # encoded expert judgment and robot benchmarks
```

Expand Down Expand Up @@ -225,6 +226,51 @@ between the plate now on the deck and the record describing it rests on the huma
carried the right one. Software cannot close that by being careful. A barcode read at both
ends can, or an arm that reports the move.

### More than one plate: what serializes, and what an arm actually buys

Every layer above costs a **single** run. A workcell exists because one plate at a time
wastes the instruments, and the second plate introduces constraints a single-run report
cannot see.

```
$ autonomous-lab schedule single_cell_genomics --plates 10

serialization point namocell (7 of 18 steps, 39%)
70 sequential operations for 10 plates
unattended run gets 1 of 18 step(s) in

CYCLE TIME NOT COMPUTABLE

22 stall(s): decode 8 attended 7 handoff 5 gate 2

A PLATE-MOVING ARM would clear 5 stall(s) and leave 17.
```

The design constraint is that **none of this uses durations**. `throughput` refuses a
plates-per-day number because 18 of 18 steps are untimed, and adding plates makes that
refusal stronger rather than weaker -- a pipelined estimate compounds every unmeasured
duration in it. But most of what a workcell planner needs is structural, not temporal.
*Which instrument serializes* is the one that appears in the most steps; that is true at
any speed, needs no stopwatch, and names which box to buy a second of.

Stalls are **classified rather than collapsed**, because five constraints stop a run and
each is cleared by a different budget: `decode` is reverse-engineering, `handoff` is a
plate mover or a barcode, `gate` is an instrument that returns a usable number, `trust` is
a calibration experiment, and `attended` is the one no purchase fixes.

`arm_relief()` is the computation this repo did not have anywhere. "Buy a plate mover" is
the reflex answer to a stalling schedule, and here it is **wrong**: an arm clears 5 of 22
stalls and none of the 8 undecoded commands. It carries plates and reports the move, which
is the one thing that closes a custody gap -- and it decodes nothing, makes no broken reader
return a number, and runs no calibration. Which purchase helps is computable before the
order rather than after it.

`trust` is scoped to where it actually binds. Almost nothing here has a met benchmark, so
flagging every step would bury the other four classes; and a USB enumeration moves nothing,
so no robot benchmark gates it. It fires only where the machine **can** act and has not
earned the right to -- which, with plr-tested wired in, is exactly the two steps that run on
real hardware under supervision.

### Sample identity: which cell the read came from

Everything above tracks **containers**. The ledger costs a plate hop, provenance records
Expand Down Expand Up @@ -472,7 +518,7 @@ event receiver and silently steals the first one's callbacks.
pip install -e '.[dev]' && pytest
```

149 device-free tests. The ones that matter most try to make the layer lie:
160 device-free tests. The ones that matter most try to make the layer lie:

- claim a step is automated when its command is undecoded; claim a decoded command is
runnable while its siblings are not; claim a federated leg runs when no run card was ever
Expand All @@ -488,6 +534,8 @@ pip install -e '.[dev]' && pytest
- treat an unbenchmarked operation as trusted.
- report an intact sample chain through a pooling step that destroyed attribution, or
claim a pooled absorbance reading says something about one well.
- return a workcell cycle time over steps nobody timed, or report a plate-moving arm as
clearing a stall that is really an undecoded command.

Two tests exist to keep the honesty from drifting quietly. One asserts that even a fully
equipped workcell -- every camera, every calibration, every model -- still leaves
Expand Down
9 changes: 9 additions & 0 deletions autonomous_lab/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@
provenance a tamper-evident run record, and where the custody chain breaks.
lineage which cell a read came from -- and whether pooling already destroyed the
answer, which is a different question from whether the plate was tracked.
orchestrate more than one plate at once: what serializes, where it stalls and on which
of five constraints, and what a plate-moving arm would actually buy.
intelligence the tacit expert judgment and the benchmarks a robot must meet first.
"""

Expand Down Expand Up @@ -64,6 +66,7 @@
undeclared_transforms,
)
from .model import Artifact, Protocol, Role, Step, Tier, Transform, Verdict, ZeroDecodeOp
from .orchestrate import Constraint, Contention, Schedule, Stall, contention, orchestrate
from .provenance import Attestation, CustodyGap, Event, RunRecord, provenance_report
from .qc import Basis, Criterion, Decision, Gate, Readiness, evaluate, gate_report
from .recovery import Detection, FailureMode, Latency, Severity, recovery_report
Expand Down Expand Up @@ -98,6 +101,8 @@ def loop_closure_for(protocol, workcell=None):
"Basis",
"Benchmark",
"BenchmarkStatus",
"Constraint",
"Contention",
"Criterion",
"CustodyGap",
"Decision",
Expand Down Expand Up @@ -126,7 +131,9 @@ def loop_closure_for(protocol, workcell=None):
"Readiness",
"Role",
"RunRecord",
"Schedule",
"Separability",
"Stall",
"RunReport",
"Severity",
"Step",
Expand All @@ -146,6 +153,7 @@ def loop_closure_for(protocol, workcell=None):
"ZeroDecodeOp",
"build_ledger",
"build_lineage",
"contention",
"cost_step",
"declared",
"estimate",
Expand All @@ -155,6 +163,7 @@ def loop_closure_for(protocol, workcell=None):
"lineage_report",
"loop_closure",
"loop_closure_for",
"orchestrate",
"provenance_report",
"rank_unlocks",
"recovery_report",
Expand Down
52 changes: 52 additions & 0 deletions autonomous_lab/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
)
from .ledger import build_ledger, rank_unlocks
from .lineage import MISASSIGNMENT, Separability, lineage_report
from .orchestrate import orchestrate
from .model import Verdict
from .provenance import provenance_report
from .qc import MEASUREMENTS, gate_report
Expand Down Expand Up @@ -380,6 +381,51 @@ def _lineage(args) -> int:
return 0 if report.verdict.ok else 1


def _orchestrate(args) -> int:
"""More than one plate at once: what serializes, and where it stops."""
wc = _workcell(args)
p = protocols.get(args.protocol)
ledger = build_ledger(p, wc)
sched = orchestrate(
p, ledger, gate_report(p.name, ledger), plates=args.plates, untrusted=untrusted_ops(p)
)

print(f"workcell schedule for {p.name}, {sched.plates} plate(s)\n")
b = sched.bottleneck
if b is not None:
print(f" serialization point {b.instrument} ({b.steps} of {len(p.steps)} steps, {b.share:.0%})")
print(f" {b.serialized_slots(sched.plates)} sequential operations for {sched.plates} plates")
print(f" unattended run gets {sched.unattended_steps} of {len(p.steps)} step(s) in")
print(f" plates in flight {sched.pipeline_depth} (distinct instruments before it stops)\n")

print(" CYCLE TIME NOT COMPUTABLE")
print(f" {sched.why_no_makespan()}\n")

counts = sched.by_constraint()
print(f" {len(sched.stalls)} stall(s), by what it would take to clear them:")
for kind, n in sorted(counts.items(), key=lambda kv: -kv[1]):
print(f" {kind:9} {n}")

first = sched.first_stall()
if first is not None:
print(f"\n STOPS AT {first.step_op} on {first.instrument} [{first.constraint.value}]")
print(f" {first.detail}")
print(f" clears it: {first.clears_it}")

removed, remains = sched.arm_relief()
print(f"\n A PLATE-MOVING ARM would clear {len(removed)} stall(s) and leave {len(remains)}.")
for st in removed:
print(f" cleared {st.step_op}: {st.detail}")
if remains:
worst = remains[0]
print(
f" remains {worst.step_op} [{worst.constraint.value}] and {len(remains) - 1} more."
f"\n An arm carries plates. It decodes no command sets, makes no broken"
"\n reader return a number, and runs no calibration."
)
return 0 if sched.runs_unattended else 1


def _knowledge(args) -> int:
"""The tacit layer: expert judgments and the benchmarks a robot must meet."""
s = knowledge_summary()
Expand Down Expand Up @@ -523,6 +569,12 @@ def common(sp):
ln.add_argument("--unit", default="cell", help="what the science needs to distinguish")
ln.set_defaults(func=_lineage)

oc = sub.add_parser("schedule", help="run more than one plate: what serializes and where it stops")
common(oc)
oc.add_argument("protocol", nargs="?", default="single_cell_genomics")
oc.add_argument("--plates", type=int, default=10, help="how many plates to push through")
oc.set_defaults(func=_orchestrate)

kn = sub.add_parser("knowledge", help="encoded expert judgment and robot benchmarks")
common(kn)
kn.set_defaults(func=_knowledge)
Expand Down
Loading
Loading