Topology
The topology is your lab's physical layout: which devices exist, where they sit, and which arms can move labware between them. It's defined separately from any workflow so the same physical lab can serve many workflows, and the same workflow can run against simulated and real topologies without code changes.
The Topology object
from orca.sdk.build import Topology
topology = Topology(
locations={"shaker": shaker, "sealer": sealer, ...},
transporters=[robotic_arm],
pools=[shaker_pool],
)
| Field | Type | Holds |
|---|---|---|
locations | dict[str, LocationValue] | Named positions that can hold labware. Values are devices or passive placeables. |
transporters | list[Transporter] | Robotic arms that move labware between locations. |
pools | list[ResourcePool] | Multi-device pools (e.g., a bank of shakers treated as one). |
Locations
A location is a named position in your lab. Each location holds either:
- A device (
Sealer,Shaker,LiquidHandler,Reader,Storage,Waste, etc.) — Orca wraps these in a staging bridge so labware can sit on them. - A passive placeable (
PlatePad) — somewhere labware can rest that isn't an active instrument.
from orca.devices.devices import LiquidHandler, Reader, Storage, Waste
from orca.devices.shaker import Shaker
from orca.devices.sealer import Sealer
from orca.devices.centrifuge import Centrifuge
from orca.resource_models.plate_pad import PlatePad
locations = {
"stacker": Storage("stacker"),
"pad_1": PlatePad("pad_1"),
"shaker": Shaker("shaker"),
"sealer": Sealer("sealer"),
"waste": Waste("waste"),
}
Names are free-form and used by your workflow to reference start/end positions and by the transporter to navigate.
Liquid handlers: deck-modeled vs deckless
Two handler classes, and which you pick shapes the topology:
-
LiquidHandler(orca.devices.devices): a handler whose deck Orca models, either Hamilton carriers or Opentrons slot decks (Flex / OT-2). Each deck position is a place labware can sit and be delivered to, addressed as<device>/<site>(e.g.mlstar_1/carrier-7-2orflex/D3-slot), so threads can pin deck-resident labware and the transporter can deliver onto the deck. You do NOT declare handoff points: handoff is derived from the topology. Any deck site the external arm teaches is a transit entry, and the handler's internal gripper relays labware between deck sites automatically (the build wires those gripper edges). It takes adeck_layoutnaming a layout in itsdeck_layout_store(required for DEVICE_SIM and LIVE, optional for PURE_SIM):mlstar_1 = LiquidHandler(
"mlstar_1",
deck_layout_store=stores.deck_layouts("mlstar_1", seed={"default": MLSTAR_1_DECK}),
deck_layout="default",
) -
LiquidHandlerProtocol(orca.devices.devices): a handler with no Orca-modeled deck (Bravo/VWorks, remote/gateway handlers). No addressable deck sites and no internal-gripper relay. Takes just a name.
For Hamilton Venus protocol files, use the Venus device (orca.sdk.devices): a protocol runner rather than a deck-modeled liquid handler. See Devices.
Resource pools
A ResourcePool lets you group identical devices and treat them as one addressable resource. When an action targets a pool, the runtime picks the next available device from the pool.
shaker_1 = Shaker("shaker_1")
shaker_2 = Shaker("shaker_2")
shaker_pool = ResourcePool("shaker_pool", [shaker_1, shaker_2])
topology = Topology(
locations={"shaker_1": shaker_1, "shaker_2": shaker_2, ...},
transporters=[arm],
pools=[shaker_pool],
)
@orca.action(device=shaker_pool, inputs=[plate]) # picks the next free shaker
async def shake(ctx: ActionContext):
await ctx.device().shake(duration=30, speed=500)
Single-device pools are created automatically — you only need a ResourcePool for multi-device cases.
Looking devices up from a topology
Inside a build_workflow(topology) function you can fetch a typed device handle:
liquid_handler = topology.device("liquid_handler", LiquidHandler)
shaker_pool = topology.pool("shaker_pool")
The second argument to topology.device(...) is a type assertion — Orca raises a helpful error if the location holds a different device type than you expected.
Sim vs hardware
The device factory context in scope decides whether each device gets a simulated or real driver. Device constructors call resolve_drivers(...); you never pass a driver= argument. Swap the factory context without touching the topology, and the same workflow runs on a sim lab and a production lab.
TODO: full driver/factory-context walkthrough (binding a real-hardware factory, the pure-sim default, run-mode selection). Probably its own page.
Where to go next
- Devices — what device types ship in the box.
- Transporters — defining teachpoints and routing.