Labware

Custom labware

Create new labware from templates.

Plates

To define a new plate, a technical drawing is essential. A standard plate class is defined based on the ANSI/SLAS Microplate Standards A typical technical drawing from a vendor has the following information:

Technical drawing of a microtiter plate.© by Perkin Elmer

We need the length, width, and height which are the base dimensions (x, y, z) of the plate respectively. Furthermore, we need the offsets in x-, y-, and z-direction starting from A. To define the wells, it may be necessary to measure the well width, length, and depth. The plate in this example is follows the standard definition of a 96 well plate and has the following dimensions:

  • Cols, rows: 12, 8
  • Length, width, height: 127.76, 85.48, 14.35

By using the place_standardized(count=96) method, the offsets and spacing are inferred from the standard specification. If the plate deviates from the specification, the place() method can be used to define custom offsets, well spacings, or alignments.

In the code below, we create a 96-well plate with a cylindrical form and a round bottom.

import dataclasses
from unitelabs.labware import Container
from unitelabs.labware.math import Cylinder, Vector, place_standardized
from unitelabs.labware.plates.plate import Plate
from unitelabs.labware.plates.well import Well


@dataclasses.dataclass
class Standard96Plate(Plate):
    cols = 12
    rows = 8
    dimensions: Vector = dataclasses.field(default_factory=lambda: Vector(x=127.76, y=85.48, z=14.35))
    children: list[Well] = dataclasses.field(
        repr=False,
        default_factory=lambda: [
            Well(
                container=Container(max_volume=300, sections=[Cylinder(radius=3.48, height=10.67)]),
                dimensions=dimension,
            ).copy(location=location)
            for location, dimension in place_standardized(count=96)
        ],
    )

Ad-hoc building in your workflow

You can find the plate that is defined below in their webshop under the model number 3959. Corning has published a table of the dimensions for all their plates here. They have a separate table for cell-culture plates.

In this example, we will define the plate "on the fly" and not as a dataclass. The total height of the plate is 23.24 mm. The well bottom is offset from the ground by 0.74 mm and the boundary box at which the well ends is 2.5 mm below the height of the plate. The spacing between the wells is 9.025 and thus deviating from the standard specification by 0.025 mm. The volume of the well is calculated with the approximation of a conical frustum. To yield more accurate results, the well could be divided into two sections: a conical frustum and a spherical segment at the bottom. However, the required measurements are not provided by the technical data sheet. A maximum volume is defined for this well. the maximum volume corresponds to the recommended working volume.

from unitelabs.labware import Plate, Well, place, Container
from unitelabs.labware.math import Vector, ConicalFrustum
from unitelabs.labware.plates.well import Shape

# MicroAmp_Optical_96_Well_Reaction_Plate approximated with a conical frustum shape
microamp_optical_96_well = Plate(
        dimensions=Vector(x=125.98, y=85.85, z=23.24), #base dimension in mm
        children=[            
            Well(
              container=Container(max_volume=200, sections=[ConicalFrustum(radius_lower=1, radius_upper=2.747, height=23.24-0.74-2.5)]), 
              dimensions=dimensions
            ).copy(location=location)
            for location, dimensions in place(
                rows=8,
                cols=12,
                boundary=Vector(x=125.98, y=85.85, z=23.24-2.5),
                item=Vector(x=9.025, y=9.025, z=23.24-0.74-2.5),
            )
        ],
    )

The plate and well object properties can be accessed to double-check their definition:

print(f'Absolute location of the plate: {microamp_optical_96_well.absolute_location}')

print(f'Base dimensions of the pate: {microamp_optical_96_well.dimensions}')

print(f'The relative location of well A1: {microamp_optical_96_well[0].location}')

print(f'The absolute location of well A1: {microamp_optical_96_well[0].absolute_location}')

print(f'The height of well A1: {microamp_optical_96_well[0].height}')

Since the plate is not assigned to a deck, is absolute location is the origin. The well is a part of the plate and therefore its absolute location is relative to the location of the plate. As shown in the well z-location, the well bottom sits 0.74 mm above the plate bottom, accounting for the material of the well bottom/plate base.

Absolute location of the plate: Vector(x=Decimal('0'), y=Decimal('0'), z=Decimal('0'))
Base dimensions of the pate: Vector(x=Decimal('125.98'), y=Decimal('85.85'), z=Decimal('23.24'))
The relative location of well A1: Vector(x=Decimal('8.840'), y=Decimal('70.000'), z=Decimal('0.74'))
The absolute location of well A1: Vector(x=Decimal('8.840'), y=Decimal('70.000'), z=Decimal('0.74'))
The height of well A1: 20.0

The wells on the plate have containers. These containers are defined by their shape by which volume and liquid level calculations are performed. While these are used heavily in the background, they can also be directly accessed. here some useful properties and methods of the general container object:

from unitelabs.labware import Liquid

microamp_optical_96_well[0].container.add_liquid(liquid=Liquid.WATER, volume=50)

print(f'The max volume defined by the creator: {microamp_optical_96_well[0].container.max_volume} µL')

print(f'The max calculated volume derived from the dimensions: {round(microamp_optical_96_well[0].container.max_fitting_volume, 3)} µL')

print(f'The current volume inside of this container: {round(microamp_optical_96_well[0].container.volume, 3)} µL')

print(f'The current liquid level inside of the container: {round(microamp_optical_96_well[0].container.liquid_level, 3)} mm')

print(f'Calculating the volume inside the container for a height of 10 mm: {round(microamp_optical_96_well[0].container.volume_for_height(height=10), 3)} µL')

print(f'Calculating the liquid level height for volume of 100 µL: {round(microamp_optical_96_well[0].container.height_for_volume(volume=100), 3)} mm')

These functions can be used to verify the labware definition.

The max volume defined by the creator: 200 µL
The max calculated volume derived from the dimensions: 236.520 µL
The current volume inside of this container: 50.000 µL
The current liquid level inside of the container: 8.348 mm
Calculating the volume inside the container for a height of 10 mm: 66.848 µL
Calculating the liquid level height for volume of 100 µL: 12.662 mm

Plate racks

🚧 Documentation coming soon 🚧

Tips

In this example we will use the Hamilton standard 300 μL conductive tips. From the technical specification, all required data (the height and type) can be extracted.

We need the length, width, and height of the tip (x, y, z), as well as the fitting-depth in millimeters. Fitting depth, length and width are constant for all conventional Hamilton tips. Furthermore, we need the collar type to check for compatibility with the pipetting heads. Only the height and collar type can be extracted from the technical specification. The dimensions of the regular channels are 9x9 mm.

The tip in this example has the following parameters:

  • Length, width, height: 9.0, 9.0, 59.9
  • Fitting depth: 8
  • Collar type: STANDARD

Other collar types can be LOW_VOLUME, HIGH_VOLUME, CORE_384_AXYGEN, XL, CORE_384_HAMILTON.

Labware parameters: Most, if not all, of these parameters are stored in the liquid handler vendor software. For Hamilton Venus, you can find them in the Labware folder. You can also always reach out to UniteLabs if you can't find the right dimensions for a liquid handler vendor labware.
Warning: Make sure that the volumes are correct. Over-aspirating may damage the pipetting channel! The actual volume of the tip can be larger than the working volume not just to keep space for transport and blow out volume, but also as safety backup volume. The maximum volume only limits the liquid volume to be aspirated. When aspirating excessive amounts of transport and blowout air, the maximum can be exceeded.
import dataclasses

from unitelabs.labware.math import Vector
from unitelabs.labware.tips.hamilton import CollarType
from unitelabs.labware.hamilton import HamiltonTip


@dataclasses.dataclass
class StandardTip(HamiltonTip):
    """
    Hamilton's 300 μL CO-RE® II pipetting tips are designed to make your
    pipetting process simpler and more efficient. Hamilton's advanced systems
    feature the Liquid Level Detection and Total Aspiration and Dispense
    Monitoring, ensuring accuracy and reliability during your pipetting process.
    With a fine, thin tip that facilitates micro-volume dosing, you can enjoy
    enhanced pipetting safety and reliability with CO-RE® II Technology.
    Compatible with the Hamilton 1000 μL pipetting channels or CO-RE 96 Probe
    Head, these tips are essential for precise pipetting.
    """

    model: str = "235902"
    dimensions: Vector = dataclasses.field(default_factory=lambda: Vector(x=9, y=9, z=59.9))
    has_filter: bool = False
    fitting_depth: float = 8
    collar_type: CollarType = CollarType.STANDARD

    max_volume: dataclasses.InitVar[float] = 400.0

Tip racks

A peculiarity about the tip rack is the negative z-offset of the attributed TipSpot children. Instead of modeling the tips hanging in the tip rack, it is assumed that the tips stand on the virtual bottom of the tip rack. The height of the tip rack in this example is 20 mm. The height of a HighVolumeTip is 95.1. The approach height for tip pick-up is 8.4 mm above the tip. That places the TipSpot at 103.5 mm. Now assuming that the top of the tip levels out with the tip rack and the tip rack being 20 mm high, then the tip is 103.5 mm - 20 mm deep in the tip rack: -83.5 mm. The dimensions of the tip spot are equal to the dimensions of the tip type that the rack is for. Since the tip spot is 2-dimensional, the z-component can be set to 0 or left out entirely. The height for the pick-up is defined by the tip height itself.

The following parameters were used in the example below:

  • Cols, rows: 12, 8
  • Length, width, height: 122.4, 82.6, 20
  • Tip spot A1 offsets: 7.2, 5.3, -83.5
  • Tip spot width, length, depth: 9, 9, 0

For Hamilton: All parameters except the offset in x- and y-direction of the tip spot can be found in the Venus labware file.

import dataclasses

from unitelabs.labware.math import Vector, place
from unitelabs.labware.tips.tip_rack import TipRack
from unitelabs.labware.tips.tip_spot import TipSpot


@dataclasses.dataclass
class HighVolumeTipRack(TipRack):
    """
    Rack with 96 10ul Low Volume Tip
    """

    dimensions: Vector = dataclasses.field(default_factory=lambda: Vector(x=122.4, y=82.6, z=20.0))
    rows: int = 8
    cols: int = 12
    children: list[TipSpot] = dataclasses.field(
        repr=False,
        default_factory=lambda: [
            TipSpot(dimensions=dimensions).copy(location=location)
            for location, dimensions in place(
                HighVolumeTipRack.cols,
                HighVolumeTipRack.rows,
                item=Vector(x=9.0, y=9.0, z=0),
                boundary=Vector(x=122.4, y=82.6, z=0),
                offset=Vector(z=-83.5),
            )
        ],
    )

Tubes

Tubes can be defined using the Tube base class. A tube has a base dimension, which is equal to the max. outer diameter in x- and y-direction and the max. total height. A fillable tube also has container, which can be made up of several sections. It is possible to define tubes with one or more Holes to access the tube with a pipetting channel. In the example below, a 9 mm by 9 mm Hole is defined with a depth of 113.65 mm.

import dataclasses

from unitelabs.labware.liquids import Container, Hole
from unitelabs.labware.math import ConicalFrustum, Cylinder, Vector, place
from unitelabs.labware.tubes import Tube


@dataclasses.dataclass
class Standard50mLTube(Tube):
    dimensions: Vector = dataclasses.field(default_factory=lambda: Vector(x=29.36, y=29.36, z=114.65))
    container: Container = dataclasses.field(
        default_factory=lambda: Container(
            max_volume=50_000, 
            sections=[
                Cylinder(radius=13.89, height=98.77), 
                ConicalFrustum(radius_lower=3.6, radius_upper=13.89, height=14.88)
                         ]
        )
    )
    capacity: int = 1
    children: list[Hole] = dataclasses.field(
        repr=False,
        default_factory=lambda: [
            Hole(dimensions=dimensions).copy(location=location) 
            for location, dimensions in place(cols=1, rows=1, item=Vector(x=9, y=9, z=113.65), boundary=Vector(x=29.36, y=29.36, z=114.65))
        ],
    )

Tube racks

🚧 Documentation coming soon 🚧

Throughs

Troughs, also called reservoirs, are used to store large amounts of liquids such as water, ethanol or buffer solutions. In contrast to a plate, aspiration operations from a trough with multiple channels draws from the same shared container volume. Conceptually, a trough is a single container that can have a defined number of access points for pipetting channels called Holes. These Holes can be arranged in a custom way. In the example below, a grid of 96 Holes is generated similar to the layout of a 96 well plate.

import dataclasses

from unitelabs.labware.liquids import Container, Hole
from unitelabs.labware.math import Cuboid, Vector, place_standardized
from unitelabs.labware.troughs import Trough


@dataclasses.dataclass
class StandardTrough(Trough):
    dimensions: Vector = dataclasses.field(default_factory=lambda: Vector(x=127.76, y=85.48, z=44))
    container: Container = dataclasses.field(
        default_factory=lambda: Container(max_volume=300_000, sections=[Cuboid(width=108, depth=72, height=40)])
    )
    capacity: int = 96
    children: list[Hole] = dataclasses.field(
        repr=False,
        default_factory=lambda: [
            Hole(dimensions=dimensions).copy(location=location) for location, dimensions in place_standardized(count=96)
        ],
    )

Carriers

Plate carriers

Building for your plate carrier library

A plate carrier can hold labware of the Plate type. The capacity determines the number of carrier sites. A carrier has a base dimension defined by its width, depth, and height for x,y,z respectively. The carrier sites are located on the carrier with first position at x=4.0, y=8.5, and z=111.75, which is equivalent to its offset from the base. The distance between the frontal edges of the carrier sites is 96 mm, which is the depth of the plate + the gap between the carrier sites. The carrier site itself is a 2-dimensional plane and only requires the x-, and y-dimensions.

The plate carrier in this example has the following dimensions:

  • Capacity: 5
  • Length, width, height: 135.0, 497.0, 130.0
  • Carrier site dimension: 127.0, 86.0
  • Carrier site 1 offsets: 4.0, 8.5, 111.75
  • Distance between carrier sites: 96
import dataclasses, typing

from unitelabs.labware.math import Vector, fill_space
from unitelabs.labware.plates.plate import Plate
from unitelabs.labware.carriers.carrier_site import CarrierSite
from unitelabs.labware.carriers import Orientation
from unitelabs.labware.hamilton import HamiltonCarrier, LabwareType
from unitelabs.labware.troughs import Trough


@dataclasses.dataclass
class PLT_CAR_L5FLEX_MD_A00(HamiltonCarrier[typing.Union[Plate, Trough]]):
    """
    Carries 5x MTPs in height adjustable landscape positions. Occupies 6 tracks.
    """

    model: str = ""
    rows: int = 5
    dimensions: Vector = dataclasses.field(default_factory=lambda: Vector(x=157.5, y=497.0, z=130.0))
    labware: LabwareType = LabwareType.PLATES
    orientation: Orientation = Orientation.LANDSCAPE
    revision: str = "A00"
    barcode: str = "X01*****"

    children: list[CarrierSite] = dataclasses.field(
        repr=False,
        default_factory=lambda: [
            CarrierSite(dimensions=Vector(x=127.0, y=86.0)).copy(location=Vector(x=15.25, y=y, z=115.8))
            for y in [392.5, 296.5, 200.5, 104.5, 8.5]
        ],
    )

Tip carriers

Building for your plate carrier library

Tip carriers can be built analogous to the plate carriers, except that the tip carrier can only hold labware of the type
TipRack.

The tip carrier in this example has the following dimensions:

  • Capacity: 5
  • Length, width, height: 135.0, 497.0, 130.0
  • Carrier site dimension: 122.4, 82.6
  • Carrier site 1 offsets: 6.2, 10.0, 114.95
  • Distance between carrier sites: 96
import dataclasses

from unitelabs.labware.math import Vector
from unitelabs.labware.tips import TipRack
from unitelabs.labware.carriers.carrier_site import CarrierSite
from unitelabs.labware.carriers import Orientation
from unitelabs.labware.hamilton import HamiltonCarrier, LabwareType


@dataclasses.dataclass
class TIP_CAR_480_A00(HamiltonCarrier[TipRack]):
    """
    Carrier for 3x 96 tip racks (10μl, 50μl, 300μl, 1000μl) or 5x 24 tip racks
    (5ml). Occupies 6 tracks.
    """

    model: str = "182085"
    rows: int = 5
    dimensions: Vector = dataclasses.field(default_factory=lambda: Vector(x=135.0, y=497.0, z=130.0))
    labware: LabwareType = LabwareType.TIPS
    orientation: Orientation = Orientation.LANDSCAPE
    revision: str = "A00"
    barcode: str = "T02*****"

    children: list[CarrierSite] = dataclasses.field(
        repr=False,
        default_factory=lambda: [
            CarrierSite(dimensions=Vector(x=122.4, y=82.6)).copy(location=Vector(x=6.2, y=y, z=114.95))
            for y in [394.0, 298.0, 202.0, 106.0, 10.0]
        ],
    )

Trough carriers

Trough carriers can be built analogous to the plate carriers, except that the trough carrier can only hold labware of the type Trough.

The trough carrier in this example has the following dimensions:

  • Capacity: 5
  • Length, width, height: 22.5, 497.0, 93.0
  • Carrier site dimension: 20.0, 89.9
  • Carrier site 1 offsets: 1.2, 6.0, 63.2
  • Distance between carrier sites: 96
import dataclasses

from unitelabs.labware.math import Vector
from unitelabs.labware.carriers.carrier_site import CarrierSite
from unitelabs.labware.carriers import Orientation
from unitelabs.labware.hamilton import HamiltonCarrier, LabwareType
from unitelabs.labware.trough.hamilton.troughs import RGT_CONT_50ml


@dataclasses.dataclass
class RGT_CAR_5R_A00(HamiltonCarrier[RGT_CONT_50ml]):
    """
    Carries 5x 50ml reagent troughs in portrait orientation. Occupies 1 track.
    """

    model: str = "187299"
    rows: int = 5
    dimensions: Vector = dataclasses.field(default_factory=lambda: Vector(x=22.5, y=497, z=82))
    labware: LabwareType = LabwareType.REAGENT
    orientation: Orientation = Orientation.PORTRAIT
    revision: str = "A00"
    barcode: str = "R10*****"

    children: list[CarrierSite] = dataclasses.field(
        repr=False,
        default_factory=lambda: [
            CarrierSite(dimensions=Vector(x=20, y=90), location=Vector(x=1.25, y=390.5, z=18.5)),
            CarrierSite(dimensions=Vector(x=20, y=90), location=Vector(x=1.25, y=294.5, z=18.5)),
            CarrierSite(dimensions=Vector(x=20, y=90), location=Vector(x=1.25, y=198.5, z=18.5)),
            CarrierSite(dimensions=Vector(x=20, y=90), location=Vector(x=1.25, y=102.5, z=18.5)),
            CarrierSite(dimensions=Vector(x=20, y=90), location=Vector(x=1.25, y=6.5, z=18.5)),
        ],
    )

Copyright © 2024