UniteLabs
Concepts

Liquids

How to represent liquids in the SDK — predefined types, custom instances with physical metadata, traceable samples, and custom aliases — and when to use each.

Every container in the labware model can hold liquid. How you represent that liquid determines how it behaves: whether it carries physical properties that influence pipetting, whether it carries a unique identifier that can be traced back to a sample, and how it homogenizes inside a mixture.

The SDK provides three representations, each with distinct semantics:

  • Predefined liquids — common lab reagents accessed via PredefinedLiquids (water, ethanol, DMSO, etc.)
  • Custom Liquid instances — user-defined reagents with optional physical metadata, created with Liquid("name")
  • Sample class — traceable biological specimens with unique identifiers

Predefined Liquids

Predefined liquids are accessed through the PredefinedLiquids registry:

from unitelabs.labware import Liquid, PredefinedLiquids

# Add predefined liquid to a container
container.add_liquid(PredefinedLiquids.WATER, volume=50_000)
container.add_liquid(PredefinedLiquids.ETHANOL, volume=10_000)
Liquid.WATER still works but is deprecated and will be removed in a future version. Use PredefinedLiquids.WATER instead.

Available Liquids

ConstantDisplay nameBuilt-in aliases
ACETONITRILEAcetonitrile
BLOODBlood
BRAIN_HOMOGENATEBrain-HomogenateBrain Homogenate, brain_homogenate, BrainHomogenate
CHLOROFORMChloroform
DIMETHYL_SULFOXIDEDimethyl sulfoxideDMSO, dimethyl_sulfoxide, DimethylSulfoxide
ETHANOLEthanolEtOH, ethanol, Ethyl Alcohol, ethyl_alcohol
GLYCERINGlycerin
METHANOLMethanolMeOH
OCTANOLOctanol1-Octanol, octanol
PBS_BUFFERPBS Buffer
PLASMAPlasma
SERUMSerum
TE_BUFFERTris-EDTA bufferTE Buffer, te_buffer, TEBuffer, Tris EDTA buffer, tris_edta_buffer
WATERWaterH2O, h_2_o, H_2_O, water, Aqua

All name and alias lookups are case-insensitive.

To see the full table at runtime (including any custom aliases you have added):

PredefinedLiquids.show()

Liquid Properties

Liquids can optionally carry properties that influence pipetting behavior:

  • Viscosity — viscosity of the liquid in mPa·s
  • Density — density of the liquid in g/mL

These properties can be used to select appropriate pipetting parameters (flow rates, mixing speeds, etc.) for optimal liquid transfer.


Custom Liquids

For substances that are not in the predefined list, create a Liquid instance directly. You can optionally supply physical metadata.

Creating a Custom Liquid

from unitelabs.labware import Liquid

# Minimal — name only
tris_buffer = Liquid("Tris buffer")

# With physical metadata
lysis_buffer = Liquid("Lysis buffer", viscosity=1.2, density=1.05)
Creating a Liquid with a name that matches a predefined liquid or its aliases raises ValueError. Use the PredefinedLiquids constant instead.
# Raises ValueError — "Water" is a predefined liquid
water = Liquid("Water")
# ValueError: Liquid named 'Water' conflicts with predefined liquid 'Water'.
#             Use PredefinedLiquids.WATER instead.

# Correct approach
water = PredefinedLiquids.WATER

Equality and Homogenization

Two Liquid instances are equal when their name, viscosity, and density all match. This means the same custom liquid defined in two places will homogenize correctly inside a Mixture:

from unitelabs.labware import Liquid, Mixture

buf_a = Liquid("Lysis buffer", viscosity=1.2, density=1.05)
buf_b = Liquid("Lysis buffer", viscosity=1.2, density=1.05)

mixture = Mixture()
mixture.add(buf_a, 100)
mixture.add(buf_b, 50)

print(len(mixture.ingredients))  # 1 — homogenized into one entry
print(mixture.volume)            # 150

Each Liquid instance also carries a short unique identifier (8-character hex string) that is excluded from equality and hashing — it exists solely for tracking purposes:

liq = Liquid("My Buffer")
print(liq.identifier)  # e.g. "3f8a1c2b"

Custom Aliases

If your lab uses a different name for a predefined liquid (e.g. your LIMS stores water as "AquaDest"), you can register an alias so that name resolves to the correct predefined instance and homogenizes transparently:

from unitelabs.labware import PredefinedLiquids, Mixture

# Register the alias once at startup
PredefinedLiquids.add_alias(PredefinedLiquids.WATER, "AquaDest")

# From that point on, the alias is interchangeable with the predefined liquid
mixture = Mixture()
mixture.add("AquaDest", 100)
mixture.add("Water", 50)

print(len(mixture.ingredients))  # 1 — both resolve to the same entry
print(mixture.volume)            # 150

Rules for Aliases

  • An alias can only be added to a predefined liquid; custom Liquid instances cannot have aliases.
  • Aliases must not conflict with any existing predefined name or existing alias (comparison is case-insensitive).
  • Multiple aliases for the same liquid are allowed.
# Multiple aliases for the same liquid — OK
PredefinedLiquids.add_alias(PredefinedLiquids.DIMETHYL_SULFOXIDE, "Dimethylsulfoxide")
PredefinedLiquids.add_alias(PredefinedLiquids.DIMETHYL_SULFOXIDE, "Me2SO")

# Raises ValueError — alias conflicts with the predefined name "Water"
PredefinedLiquids.add_alias(PredefinedLiquids.ETHANOL, "Water")

# Raises ValueError — alias targets a custom liquid, not a predefined one
custom = Liquid("My Buffer")
PredefinedLiquids.add_alias(custom, "MB")

Samples

Sample represents traceable biological specimens with unique identifiers:

from unitelabs.labware import Sample

# Create a sample with tracking information
sample = Sample(
    name="MySample",
    identifier="SAMPLE-001",
)

# Add to container
container.add_liquid(sample, volume=100)

Samples are appropriate when you need:

  • Traceability — unique identification and tracking of biological specimens
  • Isolation — each sample is distinct, even if the names match

When to Use Each Type

TypeUse CaseKey Features
Predefined liquidsStandard reagents (water, ethanol, etc.)Registry-based, aliases, no construction
Custom Liquid instancesWorkflow-specific buffers and reagentsOptional viscosity/density, homogenization
Sample classTraceable biological specimensUnique IDs, tracking, never homogenized

Use predefined liquids for standard reagents, custom Liquid instances for workflow-specific reagents, and Sample for traceable biological specimens.


Pre-filling Labware

Adding liquid one container at a time is useful in many cases as volumes or liquids vary between the wells and granural control is needed.

plate = Standard96Plate(identifier="plate")
for well in plate:
    well.container.add_liquid(PredefinedLiquids.WATER, 100)

If a whole plate is filled with same volume of a liquid, a simpler method on labware construction can be preferable.

Pipettable labware — plates and troughs — accept a filled_with argument that does this at construction, mirroring the way a TipRack is pre-filled with tips:

from unitelabs.labware import PredefinedLiquids
from unitelabs.labware.plates import Standard96Plate

plate = Standard96Plate(identifier="plate", filled_with=(PredefinedLiquids.WATER, 100))

filled_with takes a single (liquid, volume) pair, or a list of pairs to pre-fill each container with a mixture. The volume is the amount in microliters added to every container of the labware:

plate = Standard96Plate(
    identifier="plate",
    filled_with=[(PredefinedLiquids.WATER, 80), (PredefinedLiquids.ETHANOL, 20)],
)

"Every container" is what keeps the behavior uniform across labware shapes — the argument fills whatever the labware's containers are, whether that is 96 wells on a plate, the twelve independent channels of a column trough, or the single reservoir of a simple trough:

from unitelabs.labware.troughs import StandardTrough12Column

# Each of the twelve columns is filled with the same mixture
reservoir = StandardTrough12Column(
    identifier="reservoir",
    filled_with=[(PredefinedLiquids.WATER, 80), (PredefinedLiquids.ETHANOL, 20)],
)
A filled_with volume that exceeds a container's max_volume fails immediately at construction with LiquidOverflowError, rather than silently overfilling.

To fill only some containers, leave filled_with off and add liquid to the wells you want — slicing works with chess-key ranges:

plate = Standard96Plate(identifier="plate")
for well in plate["A1":"H1"]:
    well.container.add_liquid(PredefinedLiquids.WATER, 100)

Migration Guide

The Liquid type was redesigned from a str enum into a frozen dataclass. Predefined substances are no longer members of the Liquid class itself; they live in the PredefinedLiquids registry. Custom liquids are now plain Liquid instances.

Import changes

# Before
from unitelabs.labware import Liquid

# After
from unitelabs.labware import Liquid, PredefinedLiquids

Accessing predefined liquids

# Before — Liquid was a str(enum.Enum)
container.add_liquid(Liquid.WATER, volume=50_000)
container.add_liquid(Liquid.DMSO, volume=10_000)

# After — predefined liquids live in PredefinedLiquids
container.add_liquid(PredefinedLiquids.WATER, volume=50_000)
container.add_liquid(PredefinedLiquids.DIMETHYL_SULFOXIDE, volume=10_000)

Liquid.WATER is deprecated and will be removed in a future version — use PredefinedLiquids.WATER instead.

String comparison no longer works directly

# Before — Liquid was a str enum, so this was True
Liquid.WATER == "Water"  # True

# After — Liquid is a frozen dataclass
PredefinedLiquids.WATER == "Water"  # False
str(PredefinedLiquids.WATER) == "Water"  # True — compare via str() if needed

Constructing a Liquid with a predefined name is now forbidden

# Before — silently created a duplicate
water = Liquid("Water")

# After — raises ValueError
water = Liquid("Water")
# ValueError: Liquid named 'Water' conflicts with predefined liquid 'Water'.
#             Use PredefinedLiquids.WATER instead.

The same guard applies to all built-in aliases ("H2O", "DMSO", "EtOH", etc.) and is case-insensitive.


PredefinedLiquids API Reference

from unitelabs.labware import PredefinedLiquids
Method / attributeDescription
PredefinedLiquids.WATERDirect attribute access to any predefined liquid
PredefinedLiquids.get_by_name(name)Case-insensitive lookup by name or alias; returns Liquid or None
PredefinedLiquids.get_by_identifier(id)Lookup by the identifier field; returns Liquid or None
PredefinedLiquids.add_alias(liquid, alias)Register a custom alias for a predefined liquid
PredefinedLiquids.find(**kwargs)Filter predefined liquids by name, viscosity, and/or density
PredefinedLiquids.show()Print a formatted table of all predefined liquids, aliases, and properties

Integration with Liquid Classes

Custom liquids with viscosity and density properties can influence liquid-class selection for pipetting parameters. See Liquid Classes.