Labware
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
| Type | What it is | Key property |
|---|---|---|
| Plate | 96/384/1536-well microplate | rows, cols, wells indexed A1→H12 |
| Well | A single well within a plate | Has a Container for tracking liquid volume |
| Tip | A single pipette tip | Tracks air volumes, fitting depth, max volume |
| TipRack | A rack holding tips | next_tips() returns available tips |
| Tube | A standalone tube (e.g. Eppendorf) | Has a Container, optional Cap |
| TubeRack | A rack holding tubes | Grid of TubeSpot children |
| Trough | A large reagent reservoir | Single Container with multiple access holes |
| Carrier | Holds plates/tubes on the deck | rows 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 A00TIP_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:
| Property | What it returns |
|---|---|
resource.location | Position relative to parent (None if not placed) |
resource.absolute_location | Absolute position from deck origin |
resource.dimensions | Width × 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")
"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
- Standard Labware: browse and import pre-built labware types
- Custom Labware: define your own plates, tubes, tips, and carriers
- Building a Deck: compose labware into a full deck layout
Deck
The coordinate frame and resource tree that place labware in 3D space, and the layout files that make a deck reproducible across runs.
Liquids
How to represent liquids in the SDK — predefined types, custom instances with physical metadata, traceable samples, and custom aliases — and when to use each.