UniteLabs
Deck

Building a Deck

Compose a deck from carriers and labware, configure track bounds, and place carriers at specific coordinates.

See Deck for the coordinate model and resource tree. This guide covers the procedural building blocks.

For device-specific origin details (where the zero point falls relative to the physical deck), see the device guides — e.g. Positioning and movement.

Defining a Deck

Decks have a dimension that is constrained by the liquid handler space. When importing the deck directly as shown below, default constraints are applied.

from unitelabs.liquid_handling.hamilton import HamiltonDeck

deck = HamiltonDeck()
print(deck.summary())

Which returns:

  Tracks  Name               ID          Capacity     Content        X-Range   Height
======================================================================================
  1 - 30  ──

However, if the deck is created automatically using the specific liquid handler class, the deck dimensions are pre-configured for that particular liquid handler model.

from unitelabs.liquid_handling.hamilton import MicrolabSTAR

# Initialize the Hamilton Microlab STAR
hamilton = MicrolabSTAR(name="Microlab STAR")

await hamilton.initialize()

print(hamilton.deck.summary())

Adding Labware

Now we can start and add our first carrier. We will use a landscape tip carrier with 5 slots for tips racks. The location of the carrier on the deck is provided in absolute coordinates and passed using the Vector class. We place the carrier starting on rail 16.

from unitelabs.liquid_handling.hamilton import HamiltonDeck
from unitelabs.labware import Vector
from unitelabs.labware.hamilton import TIP_CAR_480_A00

deck = HamiltonDeck()
tip_carrier = TIP_CAR_480_A00(identifier="tip_carrier")
deck.add(tip_carrier, track=16)
# Alternatively the exact location can be set
# deck.add(tip_carrier, location=Vector(x=(16 - 1) * 22.5, y=63, z=0))
print(deck.summary())

Now our deck looks like this:

  Tracks  Name               ID          Capacity     Content        X-Range   Height
======================================================================================
  1 - 15  ┌─
 16 - 21  ├─ TIP_CAR_480_A00 tip_carrier      0 tips      0 tips  337.5 - 472.5  130.0
          │   ├─
          │   ├─
          │   ├─
          │   ├─
          │   └─
 22 - 30
Dynamic max deck size and deck dimension: When using a dedicated liquid handler class, like the MicrolabSTAR class, a deck is autogenerated and dynamically configured during the initialization process.

We can add more labware to the carrier like a pre-defined Hamilton tip rack and Hamilton standard filter tips. In the following code we first instantiate the HamiltonTipRack_300, then fill it with HamiltonTip_300_Filter and then set the tip rack into the second slot (from behind) of the tip carrier we created earlier. The tip_carrier object is linked to the deck.

from unitelabs.labware.hamilton import HamiltonTip_300_Filter, HamiltonTipRack_300

tip_rack = HamiltonTipRack_300(filled_with=HamiltonTip_300_Filter)
tip_carrier[1] = tip_rack

print(deck.summary())

Printing the deck summary shows that the tip rack and the tips were added successfully.

  Tracks  Name                   ID          Capacity     Content      X-Range Height
======================================================================================
  1 - 15  ┌─
 16 - 21  ├─ TIP_CAR_480_A00     tip_carrier   96 tips   96 tips   337.5 - 472.5 135.0
          │   ├─
          │   ├─ HamiltonTipRack_300 e3e3a74b      96 tips   96 tips
          │   ├─
          │   ├─
          │   └─
 22 - 30  └─

Adding plates can be done in the same way as adding racks. We will import a plate carrier and add a standard plate to it. We place the carrier starting on rail 10:

from unitelabs.labware.hamilton import TIP_CAR_480_A00, PLT_CAR_L5MD_A00
from unitelabs.labware.corning_costar import Cos_96_FB

plate_1 = Cos_96_FB()
plate_carrier = PLT_CAR_L5MD_A00(identifier="plate_carrier")
plate_carrier[1] = plate_1
deck.add(plate_carrier, track=10)

print(deck.summary())
  Tracks  Name                   ID          Capacity     Content      X-Range Height
======================================================================================
  1 -  9  ┌─
 10 - 15  ├─ PLT_CAR_L5MD_A00    plate_carrier 5 plates 1 plates   202.5 - 337.5 130.0
          │   ├─
          │   ├─ Cos_96_FB       a9e7efb0      34560 µl     0 µl
          │   ├─
          │   ├─
          │   └─
 16 - 21  ├─ TIP_CAR_480_A00     tip_carrier   96 tips   96 tips   337.5 - 472.5 135.0
          │   ├─
          │   ├─ HamiltonTipRack_300 e3e3a74b      96 tips   96 tips
          │   ├─
          │   ├─
          │   └─
 22 - 30  └─

It is often required to create custom labware. See the custom labware guide for details.

Hamilton Deck Bounds

By default, the deck enforces track boundaries that match the physical deck size of your liquid handler. However, some workflows require placing carriers on negative tracks or beyond the maximum track, for example, storing tips below the minimum track of a Hamilton STAR CO-RE96 access.

Attempting to add a carrier outside the default track bounds will raise an error. To allow out-of-bounds placement, remove the deck limitation by configuring the minimum and/or maximum track to None:

await hamilton.deck.configure(min_track=None, max_track=None)

You can also remove only one limit. For example, to allow only negative tracks:

await hamilton.deck.configure(min_track=None)
Carriers placed outside of the standard deck bounds may define positions that are not reachable by all modules. For instance, pipetting channels may not be able to reach positions outside the track area, while the CO-RE96 can. Always verify that the intended module can physically reach the carrier positions you define.

A common use case is placing a tip carrier in the negative track area of a Hamilton STAR, where tips are accessible by the CO-RE96 but not by individual pipetting channels:

from unitelabs.liquid_handling.hamilton import MicrolabSTAR
from unitelabs.labware.hamilton import TIP_CAR_480_A00

hamilton = MicrolabSTAR(name="Microlab STAR")
await hamilton.initialize()

# Remove the minimum track limit to allow negative tracks
await hamilton.deck.configure(min_track=None)

# Place the tip carrier starting at track -3
hamilton.deck.add(TIP_CAR_480_A00(), track=-3)

print(hamilton.deck.summary())

Custom Carrier at an Absolute Position

Another use case is placing a custom carrier at specific x, y, z coordinates that lie outside the standard deck bounds. These coordinates are typically measured or taught, for example using the iSWAP (see Training a Custom Deck Position):

from unitelabs.labware import Vector

# Remove deck track limits
await hamilton.deck.configure(min_track=None, max_track=None)

# Carrier coordinates, measured or taught (e.g. by iSWAP)
CARRIER_X = -53.7
CARRIER_Y = 89.0
CARRIER_Z = 209.3

custom_carrier = CustomCarrier(identifier="custom_carrier")

# Add the carrier at an absolute position, offset by the deck origin
deck_location = hamilton.deck.location or Vector(x=0, y=0, z=0)
hamilton.deck.add(
    labware=custom_carrier,
    location=Vector(x=CARRIER_X, y=CARRIER_Y, z=CARRIER_Z) - deck_location,
)

Hamilton Deck Bounds

By default, the deck enforces track boundaries that match the physical deck size of your liquid handler. However, some workflows require placing carriers on negative tracks or beyond the maximum track — for example, storing tips below the minimum track of a Hamilton STAR CO-RE96 access.

Attempting to add a carrier outside the default track bounds will raise an error. To allow out-of-bounds placement, remove the deck limitation by configuring the minimum and/or maximum track to None:

await hamilton.deck.configure(min_track=None, max_track=None)

You can also remove only one limit. For example, to allow only negative tracks:

await hamilton.deck.configure(min_track=None)
Warning: Carriers placed outside of the standard deck bounds may define positions that are not reachable by all modules. For instance, pipetting channels may not be able to reach positions outside the track area, while the CO-RE96 can. Always verify that the intended module can physically reach the carrier positions you define.

A common use case is placing a tip carrier in the negative track area of a Hamilton STAR, where tips are accessible by the CO-RE96 but not by individual pipetting channels:

from unitelabs.liquid_handling.hamilton import MicrolabSTAR
from unitelabs.labware.hamilton import TIP_CAR_480_A00

hamilton = MicrolabSTAR(name="Microlab STAR")
await hamilton.initialize()

# Remove the minimum track limit to allow negative tracks
await hamilton.deck.configure(min_track=None)

# Place the tip carrier starting at track -3
hamilton.deck.add(TIP_CAR_480_A00(), track=-3)

print(hamilton.deck.summary())

Custom Carrier at an Absolute Position

Another use case is placing a custom carrier at specific x, y, z coordinates that lie outside the standard deck bounds. These coordinates are typically measured or taught, for example using the iSWAP (see Training a Custom Deck Position):

from unitelabs.labware import Vector

# Remove deck track limits
await hamilton.deck.configure(min_track=None, max_track=None)

# Carrier coordinates, measured or taught (e.g. by iSWAP)
CARRIER_X = -53.7
CARRIER_Y = 89.0
CARRIER_Z = 209.3

custom_carrier = CustomCarrier(identifier="custom_carrier")

# Add the carrier at an absolute position, offset by the deck origin
deck_location = hamilton.deck.location or Vector(x=0, y=0, z=0)
hamilton.deck.add(
    labware=custom_carrier,
    location=Vector(x=CARRIER_X, y=CARRIER_Y, z=CARRIER_Z) - deck_location,
)

Serializing and Deserializing Decks

Deck serialization allows you to save and restore deck configurations, enabling several useful workflows:

  • Sharing deck layouts across different workflows and team members
  • Persisting deck state between sessions for reproducibility
  • Version control for deck configurations using JSON files
  • Creating templates for common deck setups

The SDK provides four methods for working with deck serialization: converting decks to/from JSON-compatible Python dictionaries and saving/loading decks to/from JSON files.

Converting Deck to Dictionary

Use the to_json() method to convert a deck configuration to a JSON-compatible Python dictionary:

from unitelabs.liquid_handling.hamilton import MicrolabSTAR

hamilton = MicrolabSTAR(name="Microlab STAR")
await hamilton.initialize()

# Configure your deck with carriers and labware
# ... (add carriers, tip racks, plates, etc.)

# Convert deck to JSON-compatible dictionary
deck_dict = hamilton.deck.to_json()

The returned dictionary contains all deck information including:

  • Deck dimensions and constraints
  • All installed carriers and their positions
  • Nested labware structures (carriers → racks → tips/plates)
  • Module lifecycle stages and configuration

Creating Deck from Dictionary

Use the from_json() class method to create a deck from a JSON-compatible Python dictionary:

from unitelabs.liquid_handling.hamilton.modules import HamiltonDeck

# Load deck from dictionary
restored_deck = HamiltonDeck.from_json(deck_dict)
print(restored_deck.summary())

This creates a fully configured deck instance that preserves:

  • Track index mapping
  • Module lifecycle stages (INSTALLED, CONFIGURED, INITIALIZED, ACTIVE)
  • All nested labware structures with complete fidelity

Saving Deck to File

Use the save() method to persist a deck configuration to a JSON file:

from pathlib import Path

# Save to file (accepts str or pathlib.Path)
hamilton.deck.save("my_deck_layout.json")

# Or using pathlib
deck_path = Path("deck_layouts/standard_setup.json")
hamilton.deck.save(deck_path)

The JSON file is saved with indentation for readability and can be committed to version control systems.

Loading Deck from File

Use the load() class method to restore a deck from a JSON file:

from unitelabs.liquid_handling.hamilton.modules import HamiltonDeck

# Load deck from file
loaded_deck = HamiltonDeck.load("my_deck_layout.json")
print(loaded_deck.summary())