UniteLabs
Concepts

Labware

Understand what labware is and how it is modelled in UniteLabs.

Labware is any physical object that holds, transfers, or interacts with samples in the lab: plates, tips, tubes, reservoirs, and the carriers that position them on the deck. In UniteLabs, every piece of labware is a Python object with a precise position, physical dimensions, and an optional liquid model. This page explains the conceptual building blocks.

Everything is a Resource

The foundation of the labware model is the Resource class. Every physical object (a single tip, a well, a plate, a carrier) is a Resource with:

  • A unique identifier (auto-generated UUID unless you supply one)
  • Dimensions (width × depth × height in mm as a Vector)
  • A location relative to its parent resource
  • An optional parent and a list of children

All concrete labware inherits from Resource via Labware:

Resource
└── Labware
    ├── Plate       (holds Wells)
    ├── Tip
    ├── TipRack     (holds TipSpots → Tips)
    ├── Tube
    ├── TubeRack    (holds TubeSpots → Tubes)
    ├── Trough
    └── Carrier     (holds CarrierSites → Plates / Troughs)

Anything that can contain other resources is a Group, which gives it parent-child management and serialization.

Labware always sits inside a deck's resource tree — a plate inside a carrier site, a carrier site inside a carrier, a carrier on the deck. That tree, and how absolute positions are computed from it, is covered in Deck.

Labware Types

TypeWhat it isKey property
Plate96/384/1536-well microplaterows, cols, wells indexed A1H12
WellA single well within a plateHas a Container for tracking liquid volume
TipA single pipette tipTracks air volumes, fitting depth, max volume
TipRackA rack holding tipsnext_tips() returns available tips
TubeA standalone tube (e.g. Eppendorf)Has a Container, optional Cap
TubeRackA rack holding tubesGrid of TubeSpot children
TroughA large reagent reservoirSingle Container with multiple access holes
CarrierHolds plates/tubes on the deckrows of CarrierSite slots

Plates and wells

A Plate is a Group of Well objects. Wells are auto-created when the plate is instantiated and indexed column-major (A1 = index 0, B1 = index 1, … H1 = index 7, A2 = index 8):

from unitelabs.labware import Standard96Plate

plate = Standard96Plate(identifier="my_plate")

well_a1 = plate["A1"]   # by label
well_a1 = plate[0]      # by index (column-major)

Carriers

A Carrier is a Rack of CarrierSite slots. Each site has a defined position and can hold one piece of labware. Hamilton carrier names encode their geometry:

  • PLT_CAR_L5MD_A00 → Plate carrier, Landscape, 5 slots, Medium Density, revision A00
  • TIP_CAR_480_A00 → Tip carrier, 480 tips, revision A00
from unitelabs.labware.hamilton import PLT_CAR_L5MD_A00, TIP_CAR_480_A00
from unitelabs.labware.corning_costar import Cos_96_FB

carrier = PLT_CAR_L5MD_A00(identifier="plate_carrier")
carrier[0] = Cos_96_FB(identifier="plate_1")  # slot 0 (front)

Readable child identifiers

Since labware 0.29.0, auto-built children of a labeled group (plate wells, tip-rack and tube-rack spots, and other pipettables) get a readable <parent>:<key> identifier instead of an opaque UUID. The key is the chess label where the group supports it, and the numeric slot index otherwise (carriers are addressed by index):

plate = Standard96Plate(identifier="source_plt")

plate["A1"].identifier    # "source_plt:A1"
plate["H12"].identifier   # "source_plt:H12"

# Readable identifiers resolve through find()
plate.find("source_plt:A1") is plate["A1"]   # True

A child you name explicitly is never overwritten, and copying a group relabels its children from the new identifier (plate.copy(identifier="dest_plt") gives "dest_plt:A1"), so saved layouts stay reproducible.

Use select() to resolve an identifier to a list of resources. It accepts a single readable identifier, a bare group identifier (expands to all children), or a <parent>::<start>:<stop> range mirroring slice access:

plate.select("source_plt:A1")        # [plate["A1"]]
plate.select("source_plt")           # every well, as a list
plate.select("source_plt::A1:H1")    # list(plate["A1":"H1"]) — the first column

A missing or malformed identifier raises MissingError rather than leaking an index error.

Positions and Dimensions

All positions use a Vector(x, y, z) with coordinates in millimeters. The coordinate origin is the bottom-left front corner of the deck:

  • x: left → right (track direction)
  • y: front → back (depth)
  • z: bottom → top (height)
from unitelabs.labware.math import Vector

# A carrier placed at track 15 has an x-offset of (15-1) × 22.5 mm
location = Vector(x=315.0, y=63.0, z=0.0)

Every resource has two location properties:

PropertyWhat it returns
resource.locationPosition relative to parent (None if not placed)
resource.absolute_locationAbsolute position from deck origin
resource.dimensionsWidth × depth × height as a Vector

Liquid Modelling

Any labware that can hold liquid (wells, tubes, troughs, tips) inherits from Fillable. Fillable resources have a Container that tracks volume and composition:

Fillable (Well, Tube, Trough, Tip)
└── Container
    ├── max_volume (µL)
    ├── volume     → current total volume
    └── mixture    → Mixture {Liquid: volume_µL, ...}

Container

The Container tracks the total liquid volume in a well or tube and enforces physical limits:

from unitelabs.labware import PredefinedLiquids

well = plate["A1"]

well.container.add_liquid(PredefinedLiquids.WATER, 100)     # add 100 µL water
well.container.add_liquid(PredefinedLiquids.DIMETHYL_SULFOXIDE, 10)  # add 10 µL DMSO
print(well.container.volume)                      # Decimal('110')
print(well.container.liquid_level)               # height in mm (shape-dependent)

well.container.remove_liquid(50)                 # remove 50 µL

Overflow and underflow raise LiquidOverflowError / LiquidUnderflowError.

Liquid and Mixture

A Mixture maps liquid types to volumes. Predefined liquids accessed via PredefinedLiquids cover common lab reagents:

from unitelabs.labware import PredefinedLiquids

# Predefined liquids
PredefinedLiquids.WATER
PredefinedLiquids.DIMETHYL_SULFOXIDE
PredefinedLiquids.ETHANOL
PredefinedLiquids.PBS_BUFFER
PredefinedLiquids.BLOOD
# ... and others

For traceable user-defined samples, use Sample instead of Liquid. Samples carry an identifier that can be linked to external systems (e.g. a LIMS barcode).

Liquid level calculation

The container calculates the liquid level in mm using the labware's physical shape model (Shape objects: Cylinder, Cone, ConicalFrustum, etc.). This is used for liquid level detection (LLD) during pipetting, so the SDK can aspirate exactly at the current surface height rather than a fixed z-offset.

Identifiers

Every resource gets a UUID identifier on construction. You can supply your own:

plate = Standard96Plate(identifier="wellplate")
reservoir = StandardTrough(identifier="reservoir_ice")

Identifiers are stable across serialization; they are the key used in deck JSON files and in lh.deck.find():

# Find by the identifier you assigned
plate = lh.deck.find(identifier="wellplate")

# Or by the UUID assigned at creation
plate = lh.deck.find(identifier="a5ecb299")
Use stable, descriptive identifiers (like "reservoir_ice") when defining deck layouts that will be saved to JSON. Auto-generated UUIDs change each time a resource is instantiated, which makes saved deck files non-reproducible.

Next Steps