UniteLabs
Pipetting

Advanced Pipetting

Using parameter sets for precise, reusable pipetting operations on Hamilton and Bravo.

Parameter sets let you group, validate, and reuse pipetting parameters as Python dataclasses. Instead of passing many keyword arguments to every aspirate or dispense call, you define the parameters once, optionally validate them before running, and pass the object to the pipetting method.

Both Hamilton and Bravo support parameter sets, though the feature depth differs. Hamilton's CoRe96 parameter sets include a full context/validation system and nested sub-parameter sets. Bravo's are simpler, covering the most common options.

Prerequisites

Creating and Using a Parameter Set

from unitelabs.labware.hamilton.liquids import LiquidClass
from unitelabs.liquid_handling.hamilton.interfaces import LLDMode
from unitelabs.liquid_handling.modules import PipetteMode
from unitelabs.liquid_handling.hamilton.modules.core96 import (
    CoRe96AspirateParameterSet,
    CoRe96DispenseParameterSet,
)

# Build a parameter set explicitly
aspirate_params = CoRe96AspirateParameterSet(
    volume=150,
    lld_mode=LLDMode.OFF,
    pipette_mode=PipetteMode.BOTTOM,
    liquid_offset=1.5,
)

# Or use a factory method
aspirate_params = CoRe96AspirateParameterSet.simple_bottom_mode(
    volume=150,
    liquid_offset=1.5,
)

# Pass to the pipetting method
await hamilton.core96.aspirate(trough, aspirate_params)
await hamilton.core96.dispense(plate, CoRe96DispenseParameterSet(volume=150))

Device-Independent Parameter Sets

You do not have to build a device-specific parameter set. The base AspirateParameterSet and DispenseParameterSet can be passed to any device's aspirate or dispense; the set is adopted onto that device's class, carrying over equivalent fields. A field the device cannot represent is rejected with a ValueError rather than silently dropped, so the same set can be reused across instruments.

from unitelabs.liquid_handling.modules.parameters import (
    AspirateParameterSet,
    DispenseParameterSet,
)
from unitelabs.liquid_handling.modules import AspiratePipetteMode

# Define once, device-independent
aspirate_params = AspirateParameterSet(
    volume=100,
    pipette_mode=AspiratePipetteMode.SURFACE,
    liquid_offset=1.0,
)

# Pass the same set to either device — each adopts it onto its own class
await hamilton.core96.aspirate(trough, aspirate_params)
await bravo.pipette_head.aspirate(source_reservoir, aspirate_params)

Serialization

Parameter sets can be saved to and loaded from plain dictionaries, making them easy to store in configuration files or databases.

# Dump to a dictionary
saved = aspirate_params.dumps()
print(saved)
# {'volume': 150, 'lld_mode': 'OFF', 'pipette_mode': 'BOTTOM', 'liquid_offset': 1.5}

# Load from a dictionary (string values for enum fields)
loaded = CoRe96AspirateParameterSet.loads(saved)
assert loaded == aspirate_params

Copy and Update

# Copy
copy = aspirate_params.copy()

# Update (modifies in place, also returns the updated set)
aspirate_params.update(
    CoRe96AspirateParameterSet(liquid_offset=2.0)
)

# Update only missing fields
aspirate_params.update(
    CoRe96AspirateParameterSet(liquid_offset=2.0),
    missing_only=True,
)

Hamilton: Context & Validation

This section is Hamilton-specific. The context system validates parameter sets against live liquid handler state (tip volume, container fill level, module configuration) before a run.

A context captures the current state of the module and target labware. validate checks the parameter set against the context without running; compile resolves all MISSING fields into concrete values.

from unitelabs.liquid_handling.hamilton.modules.core96 import CoRe96Context

context = CoRe96Context(module=hamilton.core96, target=plate)

# Explicit validate + compile (optional — done automatically inside aspirate/dispense)
assert aspirate_params.validate(context)
compiled = aspirate_params.compile(context)

Missing Fields

Omit any field to let the context fill it in at compile time. For example, volume defaults to the minimum of the tip's free volume and the container's available volume.

# Fully default parameter set — all fields resolved from context
default_aspiration = CoRe96AspirateParameterSet()
await hamilton.core96.aspirate(trough, default_aspiration)

Always provide explicit values for fields that must stay constant across different deck states.

Previewing Resolved Defaults

compile returns the low-level dictionary sent to the instrument. To see how your inputs resolve as a parameter set — without running anything — use show_defaults. It returns a copy with every MISSING field replaced by the value it would default to for the given context, and passes fields you set explicitly through their resolver so you see the effective value.

context = CoRe96Context(module=hamilton.core96, target=plate)

params = CoRe96AspirateParameterSet(volume=150)
resolved = params.show_defaults(context)

# `volume` was set explicitly, so it comes back unchanged.
assert resolved.volume == 150

# `liquid_offset` was MISSING; the result shows what the context inferred.
print(resolved.liquid_offset)

Compare a field between the original and the result to tell whether a value was inferred: a field that is MISSING before but populated after was defaulted.

Resolution recurses into nested parameter sets, so mixing and tadm are resolved too. For example, a MixingParameterSet with no cycles reports the effective cycles (0, i.e. no mixing) rather than MISSING:

from unitelabs.labware import MISSING

aspiration = CoRe96AspirateParameterSet(mixing=MixingParameterSet(volume=50))

# Before: cycles is unset.
assert aspiration.mixing.cycles is MISSING

# After: cycles reports its effective value (0 = no mixing).
resolved = aspiration.show_defaults(context)
assert resolved.mixing.cycles == 0

show_defaults emits the same warnings as compile; capture them with warnings.catch_warnings if you want to inspect them. The original set is never mutated — the result is always a fresh copy of the same type.

Nested Parameter Sets

Mixing and TADM settings are nested inside aspirate/dispense parameter sets.

from unitelabs.liquid_handling.hamilton.modules.core96 import (
    MixingParameterSet,
    TADMParameterSet,
    TADMMode,
)

mix_3x50 = MixingParameterSet(volume=50, cycles=3)

aspiration = CoRe96AspirateParameterSet(
    volume=150,
    mixing=mix_3x50,
)
dispense = CoRe96DispenseParameterSet(
    volume=150,
    mixing=mix_3x50,
    tadm=TADMParameterSet(tadm_mode=TADMMode.ON, tadm_limit_curve=1),
)

await hamilton.core96.aspirate(trough, aspiration)
await hamilton.core96.dispense(plate, dispense)

Hamilton: Channels Parameter Sets

This section is Hamilton-specific. Channels parameter sets represent operations across multiple independent pipetting channels simultaneously.

Each channel can have a different volume, liquid offset, or mixing step. Think of a channels parameter set as a list of per-channel sub-parameter sets.

from unitelabs.liquid_handling.hamilton.modules.pipettes import ChannelsAspirateParameterSet

aspirate_channels = ChannelsAspirateParameterSet(
    channels=[0, 2, 4, 6],         # zero-based channel indices
    volume=[100, 200, 300, 400],    # one value per channel
    liquid_offset=1.5,              # single value → applied to all channels
    lld_mode=False,
    mixing=[mix_3x50, MixingParameterSet()] * 2,
)

Scalar fields are broadcast to all channels; list fields must match the length of channels.

Channels Context

from unitelabs.liquid_handling.hamilton.modules.pipettes import ChannelsContext

context = ChannelsContext(
    channels=[0, 2, 4, 6],
    module=hamilton.pipettes,
    target=[plate["A1"], plate["C1"], plate["E1"], plate["G1"]],
)

assert aspirate_channels.validate(context)

Building from Sub-Parameters

from unitelabs.liquid_handling.hamilton.modules.pipettes import ChannelAspirateParameterSet

sub_params = [ChannelAspirateParameterSet(liquid_offset=i, volume=100/i) for i in range(1, 5)]

channels_aspirate = ChannelsAspirateParameterSet.from_sub_parameters(
    channels=[3, 4, 5, 6],
    sub_parameters=sub_params,
)

Compiled Parameters Glossary

The tables below describe the dictionary keys returned by compile() for CoRe96AspirateParameterSet and CoRe96DispenseParameterSet. Fields marked from liquid class should be set on the liquid class before passing it to the parameter set.

CoRe96AspirateParameterSet

Compiled keySourceDescription
volumeparameter setVolume in µL to aspirate
lld_modeparameter setLiquid level detection mode
pipette_modeparameter setSURFACE or BOTTOM positioning
liquid_offsetparameter setZ-offset from reference point (mm)
flow_rateliquid classPlunger speed (µL/s)
swap_speedliquid classRetract speed after aspiration (mm/s)
settling_timeliquid classDwell time in liquid (s)
over_aspirate_volumeliquid classPre-wetting extra volume (µL)
transport_air_volumeliquid classAir drawn after aspiration (µL)
blowout_air_volumeliquid classPre-dispense blowout air (µL)

CoRe96DispenseParameterSet

Compiled keySourceDescription
volumeparameter setVolume in µL to dispense
pipette_modeparameter setSURFACE or BOTTOM positioning
liquid_offsetparameter setZ-offset from reference point (mm)
flow_rateliquid classPlunger speed (µL/s)
swap_speedliquid classRetract speed after dispense (mm/s)
stop_flow_rateliquid classFlow rate at end of dispense (µL/s)
stop_back_volumeliquid classAir volume re-aspirated after dispense (µL)
transport_air_volumeliquid classAir drawn at end of dispense step (µL)
blowout_air_volumeliquid classBlowout air dispensed before liquid (µL)