Source code for autoprotocol.util

"""
Module containing utility functions

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

"""

from .constants import SBS_FORMAT_SHAPES
from .unit import Unit, UnitStringError, UnitValueError


[docs]def is_valid_well(well): """Checks if an input is of type Well, WellGroup or list of type Well. Example Usage: .. code-block:: python if not is_valid_well(source): raise TypeError("Source must be of type Well, list of Wells, or " "WellGroup.") Parameters ---------- well : Well or WellGroup or list(Well) Parameter to validate is type Well, WellGroup, list of Wells. Returns ------- bool Returns True if param is of type Well, WellGroup or list of type Well. """ from autoprotocol.container import Well, WellGroup if not isinstance(well, (Well, WellGroup, list)): return False if isinstance(well, list): if not all(isinstance(well, Well) for well in well): return False return True
[docs]def parse_unit(unit, accepted_unit=None): """ Parses and checks unit provided and ensures its of valid type and dimensionality. Note that this also checks against the dimensionality of the `accepted_unit`. I.e. `parse_unit("1:s", "minute")` will return True. Raises type errors if the Unit provided is invalid. Parameters ---------- unit: Unit or str Input to be checked accepted_unit: Unit or str or list(Unit) or list(str), optional Dimensionality of unit should match against the accepted unit(s). Examples -------- .. code-block:: python parse_unit("1:ul", "1:ml") parse_unit("1:ul", "ml") parse_unit("1:ul", ["ml", "kg"]) Returns ------- Unit Parsed and checked unit Raises ------ TypeError Error when input does not match expected type or dimensionality """ if not isinstance(unit, Unit): try: unit = Unit(unit) except (UnitStringError, UnitValueError) as e: raise TypeError(f"{unit} is not of type Unit/str") from e if accepted_unit is not None: # Note: This is hacky. We should formalize the concept of base Units # in AP-Py def parse_base_unit(base_unit): if not isinstance(base_unit, Unit): if isinstance(base_unit, str): if ":" not in base_unit: base_unit = "1:" + base_unit return Unit(base_unit) if isinstance(accepted_unit, list): accepted_unit = [parse_base_unit(a_u) for a_u in accepted_unit] else: accepted_unit = [parse_base_unit(accepted_unit)] if all([unit.dimensionality != a_u.dimensionality for a_u in accepted_unit]): raise TypeError( f"{unit} is not of the expected dimensionality " f"{accepted_unit}" ) return unit
def _validate_as_instance(item, target_type): """ Validates that the item is an instance of the target_type and if not, checks whether the item is the Parameters ---------- item : target_type target_type : type Returns ------- target_type the item as an instance of the target type Raises ------ TypeError if the passed item isn't either target_type or an instance thereof """ if not isinstance(item, target_type): try: item = _validate_as_instance(item(), target_type) except TypeError as e: raise TypeError(f"{item} can't be parsed as a {target_type}.") from e return item def _check_container_type_with_shape(container_type, shape): """ Checks whether the selected origin and shape pair are valid Parameters ---------- container_type : ContainerType the origin of the liquid handling operation. for multi channel operations this is the top left well of the stamp. for single channel operations this is the source well. shape : dict the shape of the transfer. used to determine the shape format of multichannel liquid handling operations see LiquidHandle.builders.shape Raises ------ ValueError invalid combination of container and shape specified """ from .instruction import Instruction shape = Instruction.builders.shape(**shape) format_rows = SBS_FORMAT_SHAPES[shape["format"]]["rows"] format_columns = SBS_FORMAT_SHAPES[shape["format"]]["columns"] is_single = shape["rows"] == shape["columns"] == 1 if container_type.is_tube and not is_single: raise ValueError( f"Tube container_type {container_type} was specified with multi " f"channel transfer shape {shape}, but tubes only support single " f"channel liquid handling." ) container_wells = container_type.well_count if container_wells == 24: rows_one_or_even = shape["rows"] == 1 or shape["rows"] % 2 == 0 columns_one_or_even = shape["columns"] == 1 or shape["columns"] % 2 == 0 if not (rows_one_or_even and columns_one_or_even): raise ValueError( f"24 well container_type {container_type.container_type} was " f"specified, but multi channel transfers in 24 well " f"containers must have row and column counts either equal to " f"1 or divisible by 2, but {shape} was specified." ) if ( shape["format"] == "SBS384" and container_wells < 384 and "sbs384_compatible" not in container_type.capabilities ): raise ValueError( f"SBS384 transfers can only be executed in 384 well plates, but " f"container_type: {container_type} has {container_wells} wells." f"If the container_type is compatible with less than 384 wells, make sure to include: sbs384_compatible in the container_type's capabilities list" ) # check for valid multi channel shapes max_rows = shape["rows"] == format_rows max_columns = shape["columns"] == format_columns is_full = max_rows and max_columns is_selective = (max_rows or max_columns) and not (max_rows and max_columns) if is_single or is_full: pass elif is_selective: if shape["format"] != "SBS96": raise ValueError( f"{shape['format']} formatted transfers require rows: " f"{format_rows} and columns: {format_columns}, but {shape} was " f"specified." ) else: raise ValueError( f"Invalid transfer shape passed: only individual wells or full " f"rows/columns can be transferred. For {shape['format']} format " f"a full row consists of {format_columns} columns and a full " f"column consists of {format_rows} rows, but {shape} was specified." ) def _validate_liha_shape(device: str, shape: dict) -> None: """Validates LiHa shape for liquid handle dispense. Args: device (str): The LiHa device name. Currently supported devices are "x_mantis" and "x_tempest_chip". shape (dict): A dictionary containing the number of rows and columns for the LiHa shape. For example, {"rows": 8, "columns": 1}. Raises: ValueError: If the provided device name is not one of the supported devices or if the provided shape is not valid for the specified device. Example: >>> _validate_liha_shape("x_mantis", {"rows": 1, "columns": 1}) None """ accepted_shape = {} if device == "x_mantis": accepted_shape.update({"rows": 1, "columns": 1}) elif device == "x_tempest_chip": accepted_shape.update({"rows": 8, "columns": 1}) else: raise ValueError( f"Device {device} is not in list of accepted devices " f"[x_mantis, x_tempest_chip]" ) if shape != accepted_shape: raise ValueError( f"Input LiHa shape: {shape}. " f"Should be {accepted_shape} for device {device}." )