Skip to main content
Version: 2.1.0

Transporters

A transporter is the arm that moves labware from one location to another. Every lab with more than one device needs at least one, so the runtime can move labware between methods. It needs teachpoints: the physical coordinates it picks and places at.

Declaring an arm​

from orca.resource_models.transporter import Transporter
from cheshire_drivers import AccessConfig, CartesianCoordinates, Teachpoint

c = CartesianCoordinates
vertical = AccessConfig(name="default_vertical", access_type="vertical")
stores.access_configs(seed=[vertical])

teachpoints = [
Teachpoint("stacker", c(0, 200, 300, 0, 90, 180), orientation="right", access=vertical),
Teachpoint("shaker", c(400, 200, 300, 0, 90, 180), orientation="right", access=vertical),
Teachpoint("sealer", c(800, 200, 300, 0, 90, 180), orientation="right", access=vertical),
Teachpoint("waste", c(1200, 200, 200, 0, 90, 180), orientation="right", access=vertical),
]

arm = Transporter(
"robotic_arm",
teachpoint_store=stores.teachpoints("robotic_arm", seed=teachpoints),
)

A transporter needs a name and a teachpoint_store. The store maps position names to physical coordinates. Resolution happens on every dispatch, so a teachpoint edited mid-run is visible to the next move.

Teach the bare device name for a device with a single position; the build expands it onto that device's site. Teach the site-qualified name for a deck (Teachpoint("flex_1/D1-slot", ...)). See Topology.

Kinds of transporter​

Three kinds ship in the box, and not all of them are arms:

ClassMachineImport
TransporterA robotic armorca.resource_models.transporter
TranslatorA single carriage on a rail: a shuttle or a bridgeorca.resource_models.translator
HumanTransferA person, prompted at each moveorca.sdk.devices

HumanTransfer is a Transporter whose driver prompts a person instead of actuating anything. It takes teachpoints the same way, as a list, a store, or a path to a file:

from orca.sdk.devices import HumanTransfer

manual = HumanTransfer("bench_operator", teachpoints=[...])

A fourth kind exists but you never construct it: the liquid handler's own on-deck gripper. The build wires it as a DeckGripperTransporter named <device>/gripper, and from then on it is a routed, reserved mover that shows up in a labware thread's history like any other.

Teachpoints​

A Teachpoint's first argument is position_id (the position this teachpoint reaches), followed by a coordinate and optional metadata:

Teachpoint(
"sealer",
coordinates=c(1300, 200, 300, 0, 90, 180),
orientation="right",
access=sealer_access_config, # optional
gateway="safe_approach", # optional waypoint to route through
taught_with="Cor_96_wellplate_360ul_Fb", # optional: what was in the jaws
)

Cartesian teachpoints must specify orientation ("left" or "right"); a Cartesian teachpoint with orientation=None raises at construction. Cartesian coordinates carry (x, y, z, yaw, pitch, roll).

Cartesian or joint. Pick and place positions use CartesianCoordinates (orientation required, supports access patterns). Waypoints along a path use JointCoordinates (per-joint values; no orientation, no access pattern). A joint teachpoint with an access_type raises. Both import from cheshire_drivers.

Waypoint gateways. A Teachpoint accepts a gateway: the name of a waypoint to route through before and after pick and place. Use it to chain a safe approach path (typically JointCoordinates waypoints) ahead of a Cartesian pick or place point. Gateway chains resolve outermost-first at dispatch.

taught_with records the labware that was in the jaws when the position was taught. Nothing derives a grip height from the difference between that labware and the one being moved: where the jaws close on a plate is not something a catalog number answers. It is there so a person can see what the number means. Per-position, per-labware exceptions live in by_labware; see Move and pipetting parameters.

Capturing them. In production you capture teachpoints by physically jogging the arm to each position and recording the coordinates. The values in these examples are placeholders. orca teachpoints create|update|delete edits them against a running deployment, and CLI: inspect the system reads them back.

Access configs​

Some devices need a particular approach: a horizontal slide-in for a plate sealer, a vertical drop for a centrifuge bucket. An access config says how the transporter enters and leaves:

from cheshire_drivers import AccessConfig

sealer_access = AccessConfig(
name="sealer_horizontal",
access_type="horizontal",
gripper_offset=25.0,
horizontal_clearance=120.0,
vertical_clearance=30.0,
)

AccessConfig fields: name, access_type ("vertical" or "horizontal"), gripper_offset (default 20.0), vertical_clearance (default 20.0), horizontal_clearance (default 100.0).

Seed access configs into the store once and reference the same one from many teachpoints:

vertical = AccessConfig(name="default_vertical", access_type="vertical")
horizontal = AccessConfig(name="default_horizontal", access_type="horizontal")
stores.access_configs(seed=[vertical, horizontal])

A position that names no approach is reached vertically. A position that names an approach the arm has no motion for is refused rather than defaulted: entering a hotel slot from above drives the arm into the shelf, so a misspelt approach has to stop.

Persistence caveat: pass a named AccessConfig via access=..., not inline access params. A teachpoint built with inline params (access_type=..., gripper_offset=...) has no access config name and is rejected by persistent teachpoint stores. Always reference a named config so the teachpoint round-trips through storage.

The four approach fields are owned by the position, not by the arm. An edit that tries to set them on an arm's move defaults or on a labware grip profile is refused, because every teachpoint supplies them on every move and a value stored wider would never be read. See Move and pipetting parameters.

Advanced​

Read this when your lab has a bridge between two robot zones, a liquid handler an external arm reaches into, or plates that queue for a busy device.

Translators are their own device kind​

A translator is one carriage on a rail. Its taught endpoints are the same physical pad seen from two robot zones, so the whole position set holds at most one labware.

from orca.resource_models.translator import Translator

bridge = Translator(
"translator_1",
teachpoint_store=stores.teachpoints("translator_1", seed=bridge_teachpoints),
)

Declare Translator, not Transporter. The device kind is what selects the one-carriage driver pair, so the fact survives whichever factory a deployment binds. A topology that says Transporter and leans on a package-local factory to name-match its translators is wrong the moment a host binds its own factory: every bridge comes up modelled as an arm, and the scheduler promises two plates one carriage.

At build time, SystemMap registers a translator's taught positions as an exclusion group. A reservation on one position is refused while a sibling holds another thread's labware or reservation. Overlapping groups fail loudly at build.

A single carriage is a corridor​

A carriage position is a legal hop destination, so without a further rule a plate could finish a move onto the carriage and then wait there for its next hop. A plate resting on the one seat severs the bridge for every other thread. This was a real deadlock in batched runs.

The rule: boarding is granted only together with the onward run. A move whose hop lands on a carriage position is granted only with reservations for the rest of the run, through to the first position that is not in an exclusion group, which is the first place the plate can legitimately rest. Both or nothing, resolved in one coordinator tick. If the run cannot be granted, the plate waits where it is, which is safe.

Two things follow. Nothing can sit on the carriage without a reserved place to get off. And a denied boarding casts its wait at the occupied onward position rather than at the free carriage seat, so the deadlock detector sees the real blocker.

Set supports_deadlock_resolution=False on the PlatePads at either end of a bridge, so the resolver never tries to free a plate by parking it on the bridge.

Routing: which transporter makes each move​

A topology can have several transporters, which is normal in a system with more than one robot zone. The runtime picks the transporter for each move from the routing graph: an arm covers the positions it is taught, a translator bridges two zones, and a liquid handler's own gripper covers the edges between that handler's deck sites. Handoff is derived from the teachpoints, not declared: any deck site an external arm teaches is a transit entry.

The graph holds one transporter per pair of positions, so two external arms reaching the same pair is not modelled today.

Both ends are asked before the arm commits​

The source and the target are both asked to accept the move before the pick, and outside the mover's lock. The refusals a move owes (a stranger on the target, an operator holding the machine, a plate no longer on its source) are one check that both the pre-flight and the actuation run.

This matters because a device can be busy for hours. Asking the target only after the pick meant an arm stood holding a plate while it waited; on the bench that was 34 seconds in the jaws, 25 of them waiting. The gates inside the actuation stay, because a device that was ready when the arm committed can be busy again by the time it arrives.

The handler steps aside​

A gantry stays wherever its last operation left it, gripper hanging off it, and nothing else moves it. An external arm reaching into that deck meets it. On the bench a PF400 placing a tip rack into a Flex staging slot clipped the Flex gripper.

So before an arm reaches into a liquid handler's deck, the handler is asked to park. Two things happen:

  • The handler parks early, right after handing labware to one of its own sites, so the park overlaps the arm's travel. This is an optimisation, and a refusal here is not an error.
  • The handler is asked again on the way in, and this one is the gate. It waits: a handler refuses while it is holding something (tips on a head, a plate in its own jaws), and refusing is not failing. The work in progress wins and the gate asks again until it is done, up to 900 seconds, after which it raises an error naming what the handler is still holding.

Declare where to park with park_gripper_at=GantryParkPosition(x=..., y=..., z=...) on the LiquidHandler. Declaring it for a driver that cannot park raises at build. A handler whose driver does not advertise IGantryParking is warned about once and passed: nothing here detects a gantry that is in the way, it only asks handlers that can move to move. See Devices for the gantry-parking capability.

A blocked plate vacates a shared deck​

A plate must not occupy a device other threads need while it waits, possibly for hours, for its own destination. When a rejected move's labware is sitting on a device-owned working site, the move's candidate collection widens after site_vacate_patience seconds (default 15) to include deadlock-resolution pads.

This is not a timeout. The real targets stay in the collection and win whenever they free; the pads only grant while the targets stay blocked. A thread that parks on a pad this way settles there, and its next move re-plans from the pad.

See also​