from pylabrobot.liquid_handling import LiquidHandler
from pylabrobot.liquid_handling.backends import LiquidHandlerChatterboxBackend
from pylabrobot.resources import (
STARLetDeck, PLT_CAR_L5AC_A00,
cor_96_wellplate_360uL_Fb, nest_1_troughplate_195000uL_Vb,
)
lh = LiquidHandler(backend=LiquidHandlerChatterboxBackend(), deck=STARLetDeck())
await lh.setup()
carrier = PLT_CAR_L5AC_A00(name="carrier")
lh.deck.assign_child_resource(carrier, rails=10)
carrier[0] = plate = cor_96_wellplate_360uL_Fb(name="sample_plate")
carrier[1] = trough = nest_1_troughplate_195000uL_Vb(name="buffer")4 Standard labware
Labware in PyLabRobot ships as generic geometric definitions in Python, and is reusable across instruments. Individual labware items are stateful and can be queried for different properties such as location or liquid volume.
Python you need here
carrier[0] = plate — the same __setitem__ assignment you met in chapter 1. It assigns the plate to the carrier’s site 0.
4.1 Decode a labware name
Decode nest_1_troughplate_195000uL_Vb without looking it up.
The naming convention is a decoder ring. Every current name is built the same way:
<vendor>_<count>_<type>_<volume>uL_<bottom>
Applied to the two definitions imported in the setup:
for definition in (cor_96_wellplate_360uL_Fb, nest_1_troughplate_195000uL_Vb):
print("definition:", definition.__name__)
print("instance:", plate.name, "/", trough.name)- 1
- The definition name is the factory function you imported — that is the name the convention describes, and the one to decode.
- 2
-
The instance name is whatever you passed as
name=. It identifies this plate on this deck and says nothing about what the labware is. Two different things, easy to conflate.
definition: cor_96_wellplate_360uL_Fb
definition: nest_1_troughplate_195000uL_Vb
instance: sample_plate / buffer
- vendor —
cor(Corning),nest,opentrons,alpaqua,eppendorf, … One package per vendor underpylabrobot.resources. - count — how many wells or reservoirs.
96,1,24. - type —
wellplate,troughplate,filtertiprack,tuberack,plateadapter, … - volume — the well capacity in µL.
360uL,195000uL(yes, that is 195 mL — a reservoir). - bottom — the well-bottom shape:
Fb(flat),Vb(V-shaped),U/Ub. This is not cosmetic; it drives the volume↔︎height math (chapter 17).
The convention above is the current one. Roughly ten vendor packages still carry old-style names as deprecation shims marked # remove v1b1 in the source — e.g. Cor_96_wellplate_360ul_Fb (next to the current cor_96_wellplate_360uL_Fb). Both import and both work today.
The catch: nothing about a shim tells you it is scheduled for deletion — no warning, no DeprecationWarning, just a name that quietly stops existing one release later. Prefer the snake_case current names. Chapter 9 uses one.
See also: resources/diy/ holds community 3D-printed labware definitions.
4.2 Find a part by vendor and catalog number
You physically own a Corning 3603 microplate. Which import is it?
Every definition’s docstring carries the manufacturer’s catalog number. Ask the library:
# grep the installed source for a catalog number you own:
# grep -rn "cat. no" pylabrobot/resources/corning/plates.pyprint(cor_96_wellplate_360uL_Fb.__doc__.splitlines()[0])- 1
-
The first line of the docstring is the catalog reference. The definition for
cor_96_wellplate_360uL_Fbopens withCorning cat. no.s: 3603— that is your plate. The full docstring also carries the manufacturer link, distributor, material, and notes.
What to do when the part is not there.
- Grep the source for the catalog number — it may be defined under a slightly different name.
- Use the closest definition and note the difference in a comment. Nobody measures a well to the micron.
- Define it yourself — chapter 17. 0.2.2 ships no PCR plate, so that chapter builds one.
See also: the vendor package list is pylabrobot.resources.* — 26 packages including the new-in-0.2.2 bioer, btx, diy, greiner, imcs, and perkin_elmer. The old corning_axygen, corning_costar, ml_star, and stanley packages were consolidated into corning.
4.3 Look up a resource by name
Find the plate named “sample_plate” on a deck you did not build, or raise if it is absent.
plate = lh.deck.get_resource("sample_plate")
print("found:", plate.name, type(plate).__name__)
# every well in the tree, in one flat list:
wells = lh.deck.get_all_children()
print("total resources on deck:", len(wells))- 1
-
get_resourcewalks the whole tree and returns the unique resource with that name. - 2
-
get_all_childrenflattens the tree — the same call used in chapter 1.
found: sample_plate Plate
total resources on deck: 118
try:
lh.deck.get_resource("no_such_plate")
except Exception as e:
print(type(e).__name__, "-", e)A missing name raises ResourceNotFoundError: Resource 'no_such_plate' not found — one of the eight portable errors that raise identically on every backend (chapter 10).
Gotcha: the tree is state, not a directory.
get_resource sees what PLR thinks is on the deck. Move a plate by hand and PLR still thinks it is where it was — which is exactly the off-deck bookkeeping problem in chapter 9. A lookup is only as truthful as the model behind it.
See also: ResourceNotFoundError is catalogued with the rest of the portable errors in chapter 10.
4.4 Ask a resource what it is
A definition carries the dimensions you would otherwise look up in a vendor drawing. Everything here is local geometry — it answers whether or not the resource is on a deck.
well = plate.get_well("A1")
print("footprint: ", plate.get_size_x(), "x",
plate.get_size_y(), "x", plate.get_size_z(), "mm")
print("grid: ", plate.num_items, "wells,",
f"{plate.num_items_x} x {plate.num_items_y}")
print("pitch: ", round(plate.item_dx, 1), "x",
round(plate.item_dy, 1), "mm")
print("well depth:", well.get_size_z(), "mm")
print("well max: ", well.max_volume, "uL")- 1
-
item_dx/item_dyare the center-to-center well spacing. 9.0 mm is the SBS standard for a 96-well plate; a 384 reports 4.5. - 2
-
The capacity the volume tracker checks a dispense against — exceeding it is
TooLittleVolumeError(chapter 10).set_volume()does not check it.
footprint: 127.76 x 85.48 x 14.2 mm
grid: 96 wells, 12 x 8
pitch: 9.0 x 9.0 mm
well depth: 10.67 mm
well max: 360 uL
Where it is needs the deck, because a position only exists relative to a parent:
print("plate: ", plate.get_absolute_location())
print("A1: ", well.get_absolute_location())
print("A1 c: ", well.get_absolute_location(
x="c", y="c", z="cavity_bottom"))
print("top: ", round(plate.get_highest_known_point(), 2), "mm")- 1
- Absolute deck coordinates, resolved through the whole parent chain — well to plate to carrier site to deck.
- 2
-
The anchors pick which point of the resource to report:
"c"for center, andzalso takes"cavity_bottom", which is where a channel bottoms out. Without them you get the front-left-bottom corner, which is rarely the point you want to pipette at. - 3
- The tallest known point above the deck, which is what a traverse height has to clear.
plate: Coordinate(306.500, 071.500, 183.120)
A1: Coordinate(317.370, 142.270, 186.150)
A1 c: Coordinate(320.800, 145.700, 186.650)
top: 197.32 mm
An unassigned resource has no position to report — not (0, 0, 0), but NoLocationError:
loose = cor_96_wellplate_360uL_Fb(name="not_on_deck")
try:
loose.get_absolute_location()
except Exception as e:
print(type(e).__name__, "-", e)
print("but its geometry still answers:", loose.get_size_x(), "mm")NoLocationError - Resource 'not_on_deck' has no location.
but its geometry still answers: 127.76 mm
Local geometry is a property of the definition; absolute position is a property of the tree. This is the same distinction as the lookup gotcha above — the tree is state.
See also: what a tip carries is the same idea one class over (chapter 8); the volume in a well is read through its tracker (chapter 5) and serialized in chapter 11.
4.5 Search the library
Filter a resource tree by type or by name pattern, in one call.
from pylabrobot.resources import Well
from pylabrobot.resources.utils import query
all_wells = query(plate, type_=Well)
print(len(all_wells), "wells on the plate")
a1 = query(plate, name=r".*_well_A1$")
print("A1:", [w.name for w in a1])- 1
-
query(root, type_=...)returns every descendant ofrootthat is an instance oftype_— here, all 96 wells of the plate. - 2
-
name=is a regular expression matched withre.match. Anchored at the start byre.matchsemantics, so.*_well_A1$pins down exactly well A1 (the$matters —.*_A1alone also matches A10, A11, A12).
96 wells on the plate
A1: ['sample_plate_well_A1']
query prunes branches that do not match
query only descends into nodes that themselves match the filters. Search the deck for wells and you get nothing, because the carriers above the wells do not match type_=Well and their subtrees are skipped:
query(lh.deck, type_=Well) # [] — the carrier branch is pruned before it reaches the wellsFor a whole-deck search use get_all_children() plus Python filtering, or get_resource for a specific name. query is at its best on a flat collection like a plate’s wells, or a carrier site’s single plate.
See also: query’s sibling helpers in resources/utils.py — row_index_to_label, label_to_row_index, and sort_by_xy_and_chunk_by_x appear in chapter 5 and chapter 7.
4.6 What to remember
- Names decode as
<vendor>_<count>_<type>_<volume>uL_<bottom>. Read them, do not memorize them. - 26 vendor packages, 143 plates. Catalog numbers live in each definition’s docstring.
- Old-style names survive as
# remove v1b1shims — they import and work, and nothing tells you. - Names are unique per deck, enforced at assignment time.
get_resourceis a safe lookup. queryfilters by type/regex/coordinates but prunes non-matching branches — use it on flat collections, not whole decks.- A definition answers its own geometry off-deck — footprint, grid, pitch, well depth and capacity. Absolute position is not geometry: it needs a parent chain, and raises
NoLocationErrorwithout one.