UniteLabs
Agilent Bravo

Liquid Classes

Using predefined and custom liquid classes with the Agilent Bravo.

Liquid classes define the motion parameters and volume correction curves used during aspirate and dispense operations. The SDK provides predefined liquid classes for common configurations and supports creating custom liquid classes for specialized protocols.

Prerequisites

  • Basic understanding of the Bravo pipetting operations (See Basic Pipetting)

Predefined Liquid Classes

The SDK includes predefined liquid classes for different head types, tip sizes, and volume ranges. Use the BravoLiquidClasses factory to access them.

from unitelabs.labware.agilent import BravoLiquidClasses

liquid_classes = BravoLiquidClasses

# Access a predefined class by name
liquid_class = liquid_classes.OQ_96LT_water_highVol

# Use with aspirate
await bravo.pipette_head.aspirate(
    plate=source_reservoir,
    volume=100,
    liquid_class=liquid_class,
)

Available Liquid Classes

96LT Head

Liquid ClassTipVolume RangeDescription
OQ_96LT_water_lowVolAgilentTip_2000–50 µLWater, low volume
OQ_96LT_water_highVolAgilentTip_25051–250 µLWater, high volume

Finding Liquid Classes

Use the find() method to discover liquid classes matching specific criteria.

from unitelabs.labware.agilent.tips import AgilentTip_250

results = liquid_classes.find(tip=AgilentTip_250, volume=100)
When no liquid_class is provided to aspirate or dispense, the SDK automatically selects a matching liquid class based on the mounted tip type and the requested volume.

Volume Correction

Liquid classes define polynomial coefficients that correct for systematic pipetting errors. The corrected volume determines the actual plunger (W-axis) position during aspirate and dispense operations.

The correction formula is a polynomial:

corrected = c₀ + c₁ × volume + c₂ × volume² + ...

For example, a liquid class with coefficients [0.0, 1.0074] applies a linear correction: the plunger moves slightly further than the nominal volume to compensate for dead volume or compression effects.

Liquid Class Parameters

Every liquid class defines the following parameters that control motion during pipetting:

ParameterDefaultDescription
liquidMixture()Liquid type (e.g., water, ethanol)
tipAgilentTip_200Compatible tip type
min_volume / max_volume0 / 250 µLValid volume range
coefficients0.0, 1.0Polynomial volume correction coefficients
aspirate_velocity5.0 mm/sPlunger velocity during aspirate
aspirate_acceleration10.0 mm/s²Plunger acceleration during aspirate
aspirate_velocity_into_wells50.0 mm/sZ descent velocity into wells
aspirate_velocity_out_of_wells50.0 mm/sZ retract velocity out of wells
aspirate_acceleration_into_wells100.0 mm/s²Z descent acceleration
aspirate_acceleration_out_of_wells100.0 mm/s²Z retract acceleration
aspirate_post_delay_ms250 msDelay after aspirate completes
dispense_velocity5.0 mm/sPlunger velocity during dispense
dispense_acceleration10.0 mm/s²Plunger acceleration during dispense
dispense_velocity_into_wells50.0 mm/sZ descent velocity into wells
dispense_velocity_out_of_wells50.0 mm/sZ retract velocity out of wells
dispense_acceleration_into_wells100.0 mm/s²Z descent acceleration
dispense_acceleration_out_of_wells100.0 mm/s²Z retract acceleration
dispense_post_delay_ms250 msDelay after dispense completes

Custom Liquid Class with Coefficients

Create a custom liquid class by subclassing BravoLiquidClass and providing direct polynomial coefficients. This approach is best when you already know the correction formula.

import dataclasses
import decimal

from unitelabs.labware.agilent.liquids.bravo_liquid_class import BravoLiquidClass
from unitelabs.labware.agilent.tips import AgilentTip_250, AgilentTip
from unitelabs.labware.liquids import Mixture, PredefinedLiquids
from unitelabs.labware.math import Decimal


def _ethanol_mixture() -> Mixture:
    return Mixture({PredefinedLiquids.ETHANOL: 1})


@dataclasses.dataclass
class EthanolLiquidClass(BravoLiquidClass):
    liquid: Mixture = dataclasses.field(default_factory=lambda: Mixture({Liquid.ETHANOL: 1}))
    tip: type[AgilentTip] = AgilentTip_250
    min_volume: Decimal = dataclasses.field(default=Decimal(default="0"))
    max_volume: Decimal = dataclasses.field(default=Decimal(default="250"))
    coefficients: list[decimal.Decimal] = dataclasses.field(
        default_factory=lambda: [decimal.Decimal("0.05"), decimal.Decimal("1.02")]
    )
    aspirate_velocity: Decimal = dataclasses.field(default=Decimal(default="35.0"))
    dispense_velocity: Decimal = dataclasses.field(default=Decimal(default="40.0"))

The coefficients [0.05, 1.02] define a linear correction: corrected = 0.05 + 1.02 × volume. Only the parameters you want to override need to be specified; all others inherit from the default values.

await bravo.pipette_head.aspirate(
    plate=source_reservoir,
    volume=100,
    liquid_class=EthanolLiquidClass,
)

Custom Liquid Class with Calibration Curve

When you have measured calibration data but don't know the exact correction formula, you can provide a curve dictionary and let the SDK fit a polynomial automatically. Set coefficients to an integer specifying the polynomial order.

@dataclasses.dataclass
class CalibratedEthanol(BravoLiquidClass):
    tip: type[AgilentTip] = AgilentTip_250
    min_volume: Decimal = dataclasses.field(default=Decimal(default="51"))
    max_volume: Decimal = dataclasses.field(default=Decimal(default="250"))
    # Set coefficients to an int to specify polynomial order for fitting
    coefficients: list[decimal.Decimal] | int = 2
    # Measured calibration points: target µL → corrected plunger µL
    curve: dict[float, float] | None = dataclasses.field(
        default_factory=lambda: {
            0:   0.0,
            50:  51.8,
            100: 102.5,
            150: 153.4,
            200: 204.6,
            250: 255.9,
        }
    )
    aspirate_velocity: Decimal = dataclasses.field(default=Decimal(default="35.0"))
    aspirate_acceleration: Decimal = dataclasses.field(default=Decimal(default="75.0"))
    aspirate_velocity_into_wells: Decimal = dataclasses.field(default=Decimal(default="45.0"))
    aspirate_velocity_out_of_wells: Decimal = dataclasses.field(default=Decimal(default="55.0"))
    aspirate_acceleration_into_wells: Decimal = dataclasses.field(default=Decimal(default="90.0"))
    aspirate_acceleration_out_of_wells: Decimal = dataclasses.field(default=Decimal(default="110.0"))
    aspirate_post_delay_ms: int = 500
    dispense_velocity: Decimal = dataclasses.field(default=Decimal(default="40.0"))
    dispense_acceleration: Decimal = dataclasses.field(default=Decimal(default="80.0"))
    dispense_velocity_into_wells: Decimal = dataclasses.field(default=Decimal(default="55.0"))
    dispense_velocity_out_of_wells: Decimal = dataclasses.field(default=Decimal(default="45.0"))
    dispense_acceleration_into_wells: Decimal = dataclasses.field(default=Decimal(default="110.0"))
    dispense_acceleration_out_of_wells: Decimal = dataclasses.field(default=Decimal(default="90.0"))
    dispense_post_delay_ms: int = 500
  • coefficients: int = 2 means "fit a quadratic polynomial (2 coefficients) to the calibration data"
  • The curve dictionary maps target volumes (µL) to measured corrected plunger volumes (µL)
  • The SDK uses least-squares fitting to derive the polynomial coefficients automatically at initialization time
The curve approach is recommended when you have measured calibration data from gravimetric testing. Use direct coefficients when you already know the correction formula or want a simple linear correction.