Source code for autoprotocol.liquid_handle.liquid_class

"""LiquidClass

Base class for defining the portions of liquid handling behavior that are
intrinsic to specific types of liquids.

    :copyright: 2021 by The Autoprotocol Development Team, see AUTHORS
        for more details.
    :license: BSD, see LICENSE for more details

"""
from collections import namedtuple
from dataclasses import dataclass
from numbers import Number
from typing import Optional

from ..util import parse_unit


# pylint: disable=too-many-instance-attributes,too-few-public-methods
[docs]@dataclass class LiquidClass: """Contains properties intrinsic to individual LiquidClasses Attributes ---------- name : str the name of the liquid_class may be used by vendors to generate more sensible defaults for unspecified behavior volume_calibration_curve : dict(str, VolumeCalibration) a calibration curve describing the relationship between tip_type, volume bins, and volume calibration parameters See Also VolumeCalibration aspirate_flowrate_calibration_curve : dict(str, VolumeCalibration) a calibration curve describing the relationship between tip_type, volume bins, and aspirate flowrate calibration parameters See Also VolumeCalibration dispense_flowrate_calibration_curve : dict(str, VolumeCalibration) a calibration curve describing the relationship between tip_type, volume bins, and dispense flowrate calibration parameters See Also VolumeCalibration _safe_volume_multiplier: Numeric a multiplier used by LiquidHandleMethods to estimate safe pump buffers for volume calibration without any prior knowledge about tip_type See Also LiquidHandleMethod._estimate_calibrated_volume Examples -------- For specifying a single, global liquid handling behavior across all volumes the easiest way is to specify parameters when instantiating a LiquidClass. If the following LiquidClass is specified then the pump_override_volume will always be set to 10:uL and the flowrate for all aspirate steps will have a target of 10:uL/s, regardless of the stated volume to be transferred. .. code-block:: python from autoprotocol import Unit from autoprotocol.instruction import LiquidHandle from autoprotocol.liquid_handle import LiquidClass lc = LiquidClass( aspirate_flowrate=LiquidHandle.builders.flowrate( target=Unit(10, "ul/s") ), calibrated_volume=Unit(10, "uL") ) For behavior that differs between volumes you can define your own LiquidClass. .. code-block:: python from autoprotocol import Unit from autoprotocol.instruction import LiquidHandle from autoprotocol.liquid_handle.liquid_class import ( LiquidClass, VolumeCalibration, VolumeCalibrationBin ) vol_curve = { "generic_1_50": VolumeCalibration( (Unit(5, "uL"), VolumeCalibrationBin( slope=1.1, intercept=Unit(0.1, "uL") )), (Unit(10, "uL"), VolumeCalibrationBin( slope=0.9, intercept=Unit(0.2, "uL") )) ) } asp_flow_curve = { "generic_1_50": VolumeCalibration( (Unit(5, "uL"), LiquidHandle.builders.flowrate( target=Unit(50, "uL/s") )), (Unit(15, "uL"), LiquidHandle.builders.flowrate( target=Unit(200, "uL/s") )) ) } class NewLC(LiquidClass): def __init__(self, *args, **kwargs): super(NewLC, self).__init__(*args, **kwargs) self.volume_calibration_curve = vol_curve self.aspirate_flowrate_calibration_curve = asp_flow_curve See Also -------- VolumeCalibration : used to specify calibration_curves LiquidHandleMethod : used to specify liquid handling movement behavior Protocol.transfer : accepts LiquidClass arguments to determine behavior Protocol.mix : accepts a LiquidClass argument to determine behavior """ calibrated_volume: Optional["Unit"] = None aspirate_flowrate: Optional[dict] = None dispense_flowrate: Optional[dict] = None delay_time: Optional["Unit"] = None clld_threshold: Optional["Unit"] = None plld_threshold: Optional["Unit"] = None """ Parameters ---------- calibrated_volume : Unit, optional used to specify a calibrated volume, if not specified then will default to the calibration from `volume_calibration_curve` aspirate_flowrate : dict, optional used to specify an aspirate flowrate, if not specified then will default to the calibration using `aspirate_flowrate_calibration_curve` dispense_flowrate : dict, optional used to specify a dispense flowrate, if not specified then will default to the calibration using `dispense_flowrate_calibration_curve` delay_time : Unit, optional the amount of time to wait after each liquid handling step. this is helpful for cases such as pressure equilibration clld_threshold : Unit, optional the capacitive liquid level detection threshold plld_threshold : Unit, optional the pressure liquid level detection threshold """ def __post_init__(self): # Dicts of {tip_type: VolumeCalibration} self.volume_calibration_curve = None self.aspirate_flowrate_calibration_curve = None self.dispense_flowrate_calibration_curve = None # May be used by vendors to set defaults for different liquid classes self.name = None # Multiplier for volume when making calibrated volume estimates without # any volume_calibration_curve or prior tip type knowledge self._safe_volume_multiplier = 1.1 def _has_calibration(self): """Checks whether any calibration attributes are specified Returns ------- bool Whether there are any calibration attributes for this LiquidClass """ return any( [ self.calibrated_volume, self.aspirate_flowrate, self.dispense_flowrate, self.volume_calibration_curve, self.aspirate_flowrate_calibration_curve, self.dispense_flowrate_calibration_curve, ] ) def _get_calibrated_volume(self, volume, tip_type): """Calculates the calibrated volume for a given volume and tip_type Parameters ---------- volume : str or Unit Desired volume to be transferred into the target well tip_type: str liquid handling device to be used for the transfer Returns ------- Unit calibrated volume """ if self.calibrated_volume is not None: calibrated_volume = self.calibrated_volume elif self.volume_calibration_curve is not None: # pylint: disable=unsubscriptable-object calibration = self.volume_calibration_curve[tip_type] volume_calibration = calibration.binned_calibration_for_volume(volume) calibrated_volume = volume_calibration.calibrate_volume(volume) else: calibrated_volume = None return calibrated_volume def _get_aspirate_flowrate(self, volume, tip_type): """Returns recommended aspiration flowrate based on transfer volume Parameters ---------- volume : str or Unit Desired volume to be transferred into the target well tip_type : str liquid handling device to be used for the transfer Returns ------- dict flowrate params """ if self.aspirate_flowrate is not None: flowrate = self.aspirate_flowrate elif self.aspirate_flowrate_calibration_curve is not None: # pylint: disable=unsubscriptable-object calibration = self.aspirate_flowrate_calibration_curve[tip_type] flowrate = calibration.binned_calibration_for_volume(volume) else: flowrate = None return flowrate def _get_dispense_flowrate(self, volume, tip_type): """Returns recommended aspiration flowrate based on transfer volume Parameters ---------- volume : str or Unit Desired volume to be transferred into the target well tip_type : str liquid handling device to be used for the transfer Returns ------- dict flowrate params """ if self.dispense_flowrate is not None: flowrate = self.dispense_flowrate elif self.dispense_flowrate_calibration_curve is not None: # pylint: disable=unsubscriptable-object calibration = self.dispense_flowrate_calibration_curve[tip_type] flowrate = calibration.binned_calibration_for_volume(volume) else: flowrate = None return flowrate
[docs]class VolumeCalibrationBin(namedtuple("VolumeCalibrationBin", ["slope", "intercept"])): """Wrapper for slope and intercept parameters for linear fitting Holds information required to calibrate a volume for liquid handle step assuming a linear relationship between volume and calibrated volume. """
[docs] def __new__(cls, slope, intercept): """ Parameters ---------- slope : Number The slope of the linear fit volume calibration function. intercept : Unit The intercept of the linear fit volume calibration function. Returns ------- VolumeCalibrationBin an object used for linear fitting volumes within a bin Raises ------ TypeError if slope is not a number """ if not isinstance(slope, Number): raise TypeError(f"slope {slope} is not a Number") intercept = parse_unit(intercept, "microliter") return super(VolumeCalibrationBin, cls).__new__(cls, slope, intercept)
[docs] def calibrate_volume(self, volume): """Calibrates the volume using slope and intercept Parameters ---------- volume : Unit the volume to be calibrated Returns ------- Unit calibrated volume """ return self.slope * volume + self.intercept
# pylint: disable=too-few-public-methods
[docs]class VolumeCalibration(object): """Wrapper for a volume-binned calibration curve A data structure that represents a calibration curve for either volumes or flowrates that are binned by upper bounded volume ranges. """ def __init__(self, *args): """ Parameters ---------- args : (Unit(volume), VolumeCalibrationBin or dict) individual calibration bins Raises ------ TypeError Not all points on the calibration curve are of the correct type """ calibration_curve = list((parse_unit(bin, "uL"), point) for bin, point in args) points = [point for _, point in calibration_curve] calibration_types = (VolumeCalibrationBin, dict) if not all(isinstance(_, calibration_types) for _ in points): raise TypeError(f"values {points} are not one of {calibration_types}") sorted_curve = list(sorted(calibration_curve, key=lambda k: k[0])) self.calibration_curve = sorted_curve
[docs] def binned_calibration_for_volume(self, volume): """Gets the smallest suitable bin in the calibration curve Finds the smallest point on the calibration curve that has a bin that's greater than or equal to the size of the specified value. Parameters ---------- volume: Unit or int or float the value to be binned Returns ------- dict target_bin Raises ------ RuntimeError No suitably large calibration bin """ volume = parse_unit(volume, "microliter") valid_bins = list( point for _, point in filter(lambda b: b[0] >= volume, self.calibration_curve) ) if not valid_bins: raise RuntimeError( f"No volume calibration bin in {self.calibration_curve} is " f"large enough to accommodate {volume}." ) return valid_bins[0]