Skip to main content

PyLabRobot Integration

Orca uses PyLabRobot for labware definitions and device backends.

Labware Definitions

PyLabRobot provides a comprehensive catalog of labware definitions that Orca uses through template classes.

Plate Definitions

from orca.sdk.labware import PlateTemplate
from pylabrobot.resources.thermo_fisher.plates import Thermo_Nunc_96_well_plate_1300uL_Rb
from pylabrobot.resources.corning.falcon.plates import Cor_Falcon_96_wellplate_340ul_Fb_Black

# Create templates with PLR plate factories
sample_plate = PlateTemplate(
"sample_plate",
Thermo_Nunc_96_well_plate_1300uL_Rb,
None # No lid
)

The second argument is a PyLabRobot plate factory function. These are imported from pylabrobot.resources submodules.

Common Plate Sources

ManufacturerImport Path
Thermo Fisherpylabrobot.resources.thermo_fisher.plates
Corningpylabrobot.resources.corning
Genericpylabrobot.resources

Browse the PyLabRobot resources directory for available definitions.

Tip Racks

from orca.sdk.labware import TipRackTemplate
from pylabrobot.resources.opentrons import opentrons_96_tiprack_300ul

tips = TipRackTemplate(
"tips_300",
opentrons_96_tiprack_300ul,
with_tips=True
)

Tube Racks and Troughs

from orca.resource_models.labware import TubeRackTemplate, TroughTemplate

tube_rack = TubeRackTemplate("tubes", tube_rack_factory)
trough = TroughTemplate("reagent_trough", trough_factory)

Device Backends

Orca wraps PyLabRobot backends so they work with Orca's driver interface.

Automatic Wrapping

Orca device constructors automatically wrap PLR backends:

from orca.devices.sealer import Sealer
from pylabrobot.sealing.a4s_backend import A4SBackend

# Pass PLR backend directly - Orca wraps it automatically
a4s_backend = A4SBackend(port="/dev/tty.usbserial-0001", timeout=10)
sealer = Sealer("sealer", a4s_backend)

The Sealer constructor detects the PLR backend type and wraps it with PLRSealerBackendWrapper.

Supported PLR Backends

Device TypePLR Backend ClassWrapper
SealerSealerBackendPLRSealerBackendWrapper
ShakerShakerBackendPLRShakerBackendWrapper
CentrifugeCentrifugeBackendPLRCentrifugeBackendWrapper
TransporterSCARABackendPLRTransporterBackendWrapper

How Wrapping Works

PLR backends use different method signatures than Orca's driver interfaces. Wrappers adapt them:

# PLR backend interface
class SealerBackend:
async def setup(self) -> None: ...
async def seal(self, temperature: int, duration: float) -> None: ...

# Orca driver interface
class ISealerDriver:
async def initialize(self) -> None: ...
async def seal(self, temperature: int, duration: float) -> None: ...

# Wrapper adapts PLR to Orca interface
class PLRSealerBackendWrapper(ISealerDriver):
def __init__(self, backend: SealerBackend):
self._backend = backend

async def initialize(self) -> None:
await self._backend.setup() # Maps initialize → setup

async def seal(self, temperature: int, duration: float) -> None:
await self._backend.seal(temperature=temperature, duration=duration)

Using PLR Backends Directly

You can also create the wrapper explicitly:

from cheshire_drivers import PLRSealerBackendWrapper
from pylabrobot.sealing.a4s_backend import A4SBackend

backend = A4SBackend(port="/dev/tty.usbserial-0001", timeout=10)
wrapper = PLRSealerBackendWrapper(backend)
sealer = Sealer("sealer", wrapper)

Labware Instances and PLR Objects

At runtime, Orca creates labware instances that wrap PLR resource objects:

# Template (design time)
sample_plate = PlateTemplate("sample", Thermo_Nunc_96_well_plate_1300uL_Rb, None)

# Instance created by Orca (runtime)
instance = sample_plate.create_instance()

# Access the underlying PLR Plate object
plr_plate = instance.PLR()

Each instance type has a PLR() method:

Instance TypeReturns
PlateInstancepylabrobot.resources.Plate
TipRackInstancepylabrobot.resources.TipRack
TubeRackInstancepylabrobot.resources.TubeRack
TroughInstancepylabrobot.resources.Trough

Transporter Integration

Orca's transporter integration with PLR is more sophisticated, handling:

  • Coordinate conversion (Cartesian and Joint)
  • Access patterns (vertical/horizontal)
  • Crossover maneuvers (changing elbow orientation)
  • Gateway waypoint traversal

Coordinate Conversion

from cheshire_drivers import convert_cartesian_to_plr_coord, convert_joint_to_plr_list

# Cartesian coordinates
from cheshire_drivers.teachpoints import CartesianCoordinates
coords = CartesianCoordinates(x=100, y=200, z=50, roll=0, pitch=90, yaw=0)
plr_coords = convert_cartesian_to_plr_coord(coords, orientation="right")

# Joint coordinates
from cheshire_drivers.teachpoints import JointCoordinates
joints = JointCoordinates(rail=0, base=170, shoulder=0, elbow=180, wrist=0, gripper=0)
plr_joints = convert_joint_to_plr_list(joints) # Returns [0, 170, 0, 180, 0, 0]

Access Patterns

PLR defines access patterns for pick/place operations. Orca teachpoints configure these:

{
"name": "shaker",
"access_type": "horizontal",
"horizontal_clearance": 30,
"vertical_clearance": 20
}

Maps to PLR HorizontalAccess or VerticalAccess patterns.

Example: Full PLR Integration

import asyncio
from orca.devices.sealer import Sealer
from orca.devices.shaker import Shaker
from orca.resource_models.transporter import Transporter
from orca.sdk.labware import PlateTemplate
from orca.sdk.workflow import MethodTemplate, ThreadTemplate, WorkflowTemplate
from orca.sdk.system import SdkToSystemBuilder, WorkflowExecutor, ResourceRegistry, SystemMap
from orca.sdk.events import EventBus
from orca.sdk.actions import Seal, Shake

# PLR imports
from pylabrobot.sealing.a4s_backend import A4SBackend
from pylabrobot.shaking.inheco_backend import InhecoThermoShakeBackend
from pylabrobot.arms.precise_flex.backend import PreciseFlexBackend
from pylabrobot.resources.thermo_fisher.plates import Thermo_Nunc_96_well_plate_1300uL_Rb

# Labware with PLR definition
plate = PlateTemplate("plate", Thermo_Nunc_96_well_plate_1300uL_Rb, None)

# Devices with PLR backends (auto-wrapped)
sealer = Sealer("sealer", A4SBackend(port="/dev/ttyUSB0", timeout=10))
shaker = Shaker("shaker", InhecoThermoShakeBackend(port="/dev/ttyUSB1"))
arm = Transporter("arm", PreciseFlexBackend(ip="192.168.1.100"), "teachpoints.json")

# Build workflow
resources = ResourceRegistry()
resources.add_resources([sealer, shaker, arm])

system_map = SystemMap(resources)
system_map.assign_resources({"sealer": sealer, "shaker": shaker})

seal_method = MethodTemplate(
name="seal",
actions=[Seal(resource=sealer, temperature=165, duration=5, inputs=[plate], outputs=[plate])]
)

shake_method = MethodTemplate(
name="shake",
actions=[Shake(resource=shaker, duration=30, speed=800, inputs=[plate], outputs=[plate])]
)

thread = ThreadTemplate(
plate,
system_map.get_location("input"),
system_map.get_location("output"),
[shake_method, seal_method]
)

workflow = WorkflowTemplate("plr_workflow")
workflow.add_thread(thread, is_start=True)

# Build and run
builder = SdkToSystemBuilder(
"plr_example", "PLR integration example",
[plate], resources, system_map,
[seal_method, shake_method], [workflow], EventBus()
)
system = builder.get_system()

async def run():
await system.initialize_all()
executor = WorkflowExecutor(workflow, system)
await executor.start(sim=False)

asyncio.run(run())

Next Steps