Skip to main content

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],
)
FieldTypeHolds
locationsdict[str, LocationValue]Named positions that can hold labware. Values are devices or passive placeables.
transporterslist[Transporter]Robotic arms that move labware between locations.
poolslist[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-2 or flex/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 a deck_layout naming a layout in its deck_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.