Advanced Pipetting
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
- Basic understanding of pipetting (See Basic Pipetting)
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))
from unitelabs.liquid_handling.agilent.interfaces.parameters import (
BravoAspirateParameterSet,
BravoDispenseParameterSet,
)
from unitelabs.liquid_handling.modules import PipetteMode
aspirate_params = BravoAspirateParameterSet(
volume=100,
pipette_mode=PipetteMode.SURFACE,
liquid_offset=0.0,
)
dispense_params = BravoDispenseParameterSet(
volume=100,
pipette_mode=PipetteMode.BOTTOM,
liquid_offset=3.0,
)
await bravo.pipette_head.aspirate(source_reservoir, aspirate_params)
await bravo.pipette_head.dispense(dest_plate, dispense_params)
For the full Bravo parameter reference see Bravo Basic Pipetting.
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
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
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 key | Source | Description |
|---|---|---|
volume | parameter set | Volume in µL to aspirate |
lld_mode | parameter set | Liquid level detection mode |
pipette_mode | parameter set | SURFACE or BOTTOM positioning |
liquid_offset | parameter set | Z-offset from reference point (mm) |
flow_rate | liquid class | Plunger speed (µL/s) |
swap_speed | liquid class | Retract speed after aspiration (mm/s) |
settling_time | liquid class | Dwell time in liquid (s) |
over_aspirate_volume | liquid class | Pre-wetting extra volume (µL) |
transport_air_volume | liquid class | Air drawn after aspiration (µL) |
blowout_air_volume | liquid class | Pre-dispense blowout air (µL) |
CoRe96DispenseParameterSet
| Compiled key | Source | Description |
|---|---|---|
volume | parameter set | Volume in µL to dispense |
pipette_mode | parameter set | SURFACE or BOTTOM positioning |
liquid_offset | parameter set | Z-offset from reference point (mm) |
flow_rate | liquid class | Plunger speed (µL/s) |
swap_speed | liquid class | Retract speed after dispense (mm/s) |
stop_flow_rate | liquid class | Flow rate at end of dispense (µL/s) |
stop_back_volume | liquid class | Air volume re-aspirated after dispense (µL) |
transport_air_volume | liquid class | Air drawn at end of dispense step (µL) |
blowout_air_volume | liquid class | Blowout air dispensed before liquid (µL) |