Source code for autoprotocol.container

"""
Container, Well, WellGroup objects and associated functions

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

"""
import json
import warnings

from dataclasses import dataclass
from typing import Dict, Optional, Union

from autoprotocol.util import parse_unit

from .constants import SBS_FORMAT_SHAPES
from .container_type import _CONTAINER_TYPES, ContainerType
from .unit import Unit, UnitError


SEAL_TYPES = ["ultra-clear", "foil", "breathable"]
COVER_TYPES = ["standard", "low_evaporation", "universal"]


class EntityPropertiesMixin:
    """
    The mixin for Container and Well entities used to mutate the entity instance
     ctx_properties and properties
    """

    @classmethod
    def validate_properties(cls, properties):
        """Validates that properties are valid"""
        entity = cls.__name__
        if not isinstance(properties, dict):
            raise TypeError(
                f"{str(entity)} properties {properties} are of type "
                f"{type(properties)}, they should be a `dict`."
            )
        for key, value in properties.items():
            if not isinstance(key, str):
                raise TypeError(
                    f"{str(entity)} property {key} : {value} has a key of type "
                    f"{type(key)}, it should be a 'str'."
                )
            try:
                json.dumps(value)
            except TypeError as e:
                raise TypeError(
                    f"{str(entity)} property {key} : {value} has a value of type "
                    f"{type(value)}, that isn't JSON serializable."
                ) from e

    def set_properties(self, properties):
        """
        Set properties for an entity (ie: Container or Well). Existing property dictionary
        will be completely overwritten with the new dictionary.

        Parameters
        ----------
        properties : dict
            Custom properties for an entity in dictionary form.

        Returns
        -------
        self
            Container or Well with modified properties
        """
        self.validate_properties(properties)
        self.properties = properties.copy()
        return self

    def add_properties(self, properties):
        """
        Add properties to the properties attribute of an entity (ie: Container or Well).

        If any property with the same key already exists for the entity then:
         - if both old and new properties are lists then append the new property
         - otherwise overwrite the old property with the new one

        Parameters
        ----------
        properties : dict
            Dictionary of properties to add to an entity.

        Returns
        -------
        self
            Container or Well with modified properties
        """

        self.validate_properties(properties)
        for key, new_value in properties.items():
            current_value = self.properties.get(key)
            if key in self.properties:
                if isinstance(current_value, list) and isinstance(new_value, list):
                    current_value.extend(new_value)
                else:
                    message = f"Overwriting existing property {key} for {self}."
                    warnings.warn(message=message)
                    self.properties[key] = new_value
            else:
                self.properties[key] = new_value
        return self

    def set_ctx_properties(self, dict_: Dict):
        """
        Sets custom_contextual_properties for an entity (ie: Container or Well).
        Existing property dictionary will be completely overwritten with the new dictionary.

        Parameters
        ----------
        dict_ : dict
            Custom custom_contextual_properties for an entity in dictionary form.

        Raises
        ------
        TypeError
            If dict_ is not of type dict

        Returns
        -------
        self
            Container or Well with modified custom_contextual_properties
        """
        self.validate_properties(dict_)
        self.ctx_properties = dict_
        return self

    def add_ctx_properties(self, dict_: Dict):
        """
        Add properties to the custom_contextual_properties attribute of an entity (ie: Container or Well).

        If any custom_contextual_properties with the same key already exists for the entity then:
         - if both old and new properties are lists then append the new property
         - otherwise overwrite the old property with the new one

        Parameters
        ----------
        dict_ : dict
            Dictionary of properties to add to a entity.

        Returns
        -------
        self
            Container or Well with modified custom_contextual_properties
        """
        self.validate_properties(dict_)
        for key, new_value in dict_.items():
            current_value = self.ctx_properties.get(key)
            if current_value:
                if isinstance(current_value, list) and isinstance(new_value, list):
                    current_value.extend(new_value)
                else:
                    message = f"Overwriting existing property {key} for {self}"
                    warnings.warn(message=message)
                    self.ctx_properties[key] = new_value
            else:
                self.ctx_properties[key] = new_value
        return self


[docs]@dataclass(eq=False) class Well(EntityPropertiesMixin): """ A Well object describes a single location within a container. Do not construct a Well directly -- retrieve it from the related Container object. Parameters ---------- container : Container The Container this well belongs to. index : int The index of this well within the container. properties : dict, optional mapping of key value properties associated to the Container ctx_properties : dict, optional mapping of key value properties associated to the Container """ container: "Container" # Forward references index: int volume: Optional[Union[str, Unit]] = None mass: Optional[Union[str, Unit]] = None name: Optional[str] = None compounds: Optional[list] = None properties: Optional[dict] = None ctx_properties: Optional[dict] = None def __post_init__(self): if not isinstance(self.container, Container): raise TypeError( f"Well initialization: Input 'container' for well [{self.index}] " f"is not valid: {self.container}" ) if not isinstance(self.properties, dict): self.properties = dict() if not isinstance(self.ctx_properties, dict): self.ctx_properties = dict()
[docs] def set_mass(self, mass): """ Set the theoretical mass of contents in a Well. Parameters ---------- mass : str, Unit Theoretical mass to indicate for a Well. Returns ------- Well Well with modified mass Raises ------ TypeError Incorrect input-type given """ if mass is None: # Set mass as None if no mass is known, as this is different from a mass of 0:mg self.mass = None return self if not isinstance(mass, str) and not isinstance(mass, Unit): raise TypeError( f"Mass {mass} is of type {type(mass)}, it should be either " f"'str' or 'Unit'." ) m = parse_unit(mass) self.mass = m return self
[docs] def set_volume(self, vol): """ Set the theoretical volume of liquid in a Well. Parameters ---------- vol : str, Unit Theoretical volume to indicate for a Well. Returns ------- Well Well with modified volume Raises ------ TypeError Incorrect input-type given ValueError Volume set exceeds maximum well volume """ if not isinstance(vol, str) and not isinstance(vol, Unit): raise TypeError( f"Volume {vol} is of type {type(vol)}, it should be either " f"'str' or 'Unit'." ) v = Unit(vol) max_vol = self.container.container_type.true_max_vol_ul if v > max_vol: containerIdInfo = "" if self.container.id: containerIdInfo = f" with container ID: {self.container.id}" raise ValueError( f"Theoretical volume [{v}] to be set exceeds maximum well " f"volume [{max_vol}] for container '{self.container.name}'{containerIdInfo} " f"when setting the volume for well {self.index}." ) self.volume = v return self
[docs] def set_compounds(self, compounds): """ Sets the list of associated compounds, and their metadata, to an aliquot. Parameters ---------- compounds : list List of compounds associated to a well. Returns ------- Well Well with associated compounds Raises ------ TypeError Incorrect input-type given """ # expected parameters and label transformations expected_params = { "id": "id", "molecularWeight": "molecular_weight", "smiles": "smiles", "concentration": "concentration", "solubilityFlag": "solubility_flag", } if not isinstance(compounds, list): raise TypeError( f"Compound list {compounds} is of type {type(compounds)}, it should be a 'list'." ) for compound in compounds: if isinstance(compound, dict): for k in expected_params: if k in compound: # transform {"molecularWeight": float} -> {"molecular_weight": Unit} if k == "molecularWeight": try: mw = Unit(compound.pop(k), "g/mol") except (UnitError): mw = None compound[expected_params[k]] = mw elif k == "solubilityFlag": compound[expected_params[k]] = compound.pop(k) elif k == "concentration": try: conc = Unit(compound.pop(k), "millimol/liter") except (UnitError): conc = None compound[expected_params[k]] = conc elif k == "smiles": # TODO: validation on smiles pass else: pass else: # set unspecified keys using preferred param string compound[expected_params[k]] = None else: pass self.compounds = compounds return self
[docs] def add_volume(self, vol: Unit): """ Updates the volume of the well Parameters ---------- vol : str, Unit Theoretical volume to indicate for a Well. Returns ------- Unit the updated volume of the Well Raises ------ TypeError Error when input does not match expected type or dimensionality ValueError Volume set exceeds maximum well volume """ v = parse_unit(vol) if self.volume: self.volume += v else: self.volume = v return self.volume
[docs] def set_name(self, name): """ Set a name for this well for it to be included in a protocol's "outs" section Parameters ---------- name : str Well name. Returns ------- Well Well with modified name """ self.name = name return self
[docs] def humanize(self): """ Return the human readable representation of the integer well index given based on the ContainerType of the Well. Uses the humanize function from the ContainerType class. Refer to `ContainerType.humanize()` for more information. Returns ------- str Index of well in Container (in human readable form) """ return self.container.humanize(self.index)
[docs] def available_volume(self): """ Returns the available volume of a Well. This is calculated as nominal volume - container_type dead volume Returns ------- Unit(volume) Volume in well Raises ------ RuntimeError Well has no volume """ if self.volume is None: raise RuntimeError(f"well {self} has no volume") return self.volume - self.container.container_type.dead_volume_ul
[docs] def __repr__(self): """ Return a string representation of a Well. """ return f"Well({str(self.container)}, {str(self.index)}, " f"{str(self.volume)})"
[docs]class WellGroup(object): """ A logical grouping of Wells. Wells in a WellGroup do not necessarily need to be in the same container. Parameters ---------- wells : list List of Well objects contained in this WellGroup. Raises ------ TypeError Wells is not of the right input type """ def __init__(self, wells): if isinstance(wells, Well): wells = [wells] elif isinstance(wells, WellGroup): wells = wells.wells elif isinstance(wells, list): if not all(isinstance(well, Well) for well in wells): raise TypeError("All elements in list must be wells") else: raise TypeError("Wells must be Well, list of wells, WellGroup.") self.wells = wells self.name = None def __eq__(self, other): if isinstance(other, self.__class__): return self.__dict__ == other.__dict__ else: return False
[docs] def set_properties(self, properties): """ Set the same properties for each Well in a WellGroup. Parameters ---------- properties : dict Dictionary of properties to set on Well(s). Returns ------- WellGroup WellGroup with modified properties """ for w in self.wells: w.set_properties(properties) return self
[docs] def add_properties(self, properties): """ Add the same properties for each Well in a WellGroup. Parameters ---------- properties : dict Dictionary of properties to set on Well(s). Returns ------- WellGroup WellGroup with modified properties """ for w in self.wells: w.add_properties(properties) return self
[docs] def set_volume(self, vol): """ Set the volume of every well in the group to vol. Parameters ---------- vol : Unit, str Theoretical volume of each well in the WellGroup. Returns ------- WellGroup WellGroup with modified volume """ for w in self.wells: w.set_volume(vol) return self
[docs] def indices(self): """ Return the indices of the wells in the group in human-readable form, given that all of the wells belong to the same container. Returns ------- list(str) List of humanized indices from this WellGroup """ indices = [] for w in self.wells: assert w.container == self.wells[0].container, ( "All wells in WellGroup must belong to the same container to " "get their indices." ) indices.append(w.humanize()) return indices
[docs] def append(self, other): """ Append another well to this WellGroup. Parameters ---------- other : Well Well to append to this WellGroup. Returns ------- WellGroup WellGroup with appended well Raises ------ TypeError other is not of type Well """ if not isinstance(other, Well): raise TypeError("Input given is not of type 'Well'.") else: return self.wells.append(other)
[docs] def extend(self, other): """ Extend this WellGroup with another WellGroup. Parameters ---------- other : WellGroup or list of Wells WellGroup to extend this WellGroup. Returns ------- WellGroup WellGroup extended with specified WellGroup Raises ------ TypeError Input WellGroup is not of the right type """ if not isinstance(other, (WellGroup, list)): raise TypeError("Input given is not of type 'WellGroup' or " "'list'.") else: if not all(isinstance(well, Well) for well in other): raise TypeError("Input given is not of type 'Well'.") return self.wells.extend(WellGroup(other).wells)
[docs] def set_group_name(self, name): """ Assigns a name to a WellGroup. Parameters ---------- name: str WellGroup name Returns ------- str Name of wellgroup """ self.name = name return self
[docs] def wells_with(self, prop, val=None): """ Returns a wellgroup of wells with the specified property and value Parameters ---------- prop: str the property you are searching for val: str, optional the value assigned to the property Returns ------- WellGroup WellGroup with modified properties Raises ------ TypeError property or value defined does not have right input type """ if not isinstance(prop, str): raise TypeError(f"property is not a string: {prop!r}") if val is not None: return WellGroup( [ w for w in self.wells if prop in w.properties and w.properties[prop] == val ] ) else: return WellGroup([w for w in self.wells if prop in w.properties])
[docs] def pop(self, index=-1): """ Removes and returns the last well in the wellgroup, unless an index is specified. If index is specified, the well at that index is removed from the wellgroup and returned. Parameters ---------- index: int, optional the index of the well you want to remove and return Returns ------- Well Well with selected index from WellGroup """ return self.wells.pop(index)
[docs] def insert(self, i, well): """ Insert a well at a given position. Parameters ---------- i : int index to insert the well at well : Well insert this well at the index Returns ------- WellGroup WellGroup with inserted wells Raises ------ TypeError index or well defined does not have right input type """ if not isinstance(i, int): raise TypeError("Input given is not of type 'Int'") if not isinstance(well, Well): raise TypeError("Input given is not of type 'Well'") if i >= len(self.wells): return self.wells.append(well) else: self.wells = self.wells[:i] + [well] + self.wells[i:] return self.wells
[docs] def __setitem__(self, key, item): """ Set a specific Well in a WellGroup. Parameters ---------- key : int Position in a WellGroup in robotized form. item: Well Well or WellGroup to be added Raises ------ TypeError Item specified is not of type `Well` """ if not isinstance(item, Well): raise TypeError("Input given is not of type 'Well'.") self.wells[key] = item
[docs] def __getitem__(self, key): """ Return a specific Well from a WellGroup. Parameters ---------- key : int Position in a WellGroup in robotized form. Returns ------- Well Specified well from given key """ return self.wells[key]
[docs] def __len__(self): """ Return the number of Wells in a WellGroup. """ return len(self.wells)
[docs] def __repr__(self): """ Return a string representation of a WellGroup. """ return "WellGroup(%s)" % (str(self.wells))
[docs] def __add__(self, other): """ Append a Well or Wells from another WellGroup to this WellGroup. Parameters ---------- other : Well, WellGroup. Returns ------- WellGroup WellGroup with appended wells Raises ------ TypeError Input given is not of type Well or WellGroup """ if not isinstance(other, (Well, WellGroup)): raise TypeError("You can only add a Well or WellGroups " "together.") if isinstance(other, Well): return WellGroup(self.wells + [other]) else: return WellGroup(self.wells + other.wells)
# pylint: disable=redefined-builtin
[docs]@dataclass(eq=False) class Container(EntityPropertiesMixin): """ A reference to a specific physical container (e.g. a tube or 96-well microplate). Every Container has an associated ContainerType, which defines the well count and arrangement, amongst other properties. There are several methods on Container which present a convenient interface for defining subsets of wells on which to operate. These methods return a WellGroup. Containers are usually declared using the Protocol.ref method. Parameters ---------- id : str, optional Alphanumerical identifier for a Container. container_type : ContainerType ContainerType associated with a Container. name : str, optional name of the container/ref being created. storage : str, optional name of the storage condition. cover : str, optional name of the cover on the container. properties : dict, optional mapping of key value properties associated to the Container ctx_properties : dict, optional mapping of key value properties associated to the Container Raises ------ AttributeError Invalid cover-type given """ id: Optional[str] = None container_type: Union[ContainerType, str] = "" name: Optional[str] = None storage: Optional[str] = None cover: Optional[str] = None properties: Optional[dict] = None ctx_properties: Optional[dict] = None def __post_init__(self): # Validate container_name = "" if self.name: container_name = f" Container '{self.name}'" container_id = "" if self.id: container_id = f" with ID [{self.id}]" if not isinstance(self.container_type, (ContainerType, str)): raise TypeError( f"Container initialization:{container_name}{container_id} failed! " f"Input 'container_type' object is not valid: {self.container_type}" ) elif isinstance(self.container_type, str): container_type_str = self.container_type self.container_type = _CONTAINER_TYPES.get(container_type_str) if not self.container_type: raise TypeError( f"Container initialization:{container_name}{container_id} failed! " f"Input 'container_type' did not exist: {container_type_str}" ) if self.cover and not (self.is_covered() or self.is_sealed()): raise AttributeError( f"Container initialization:{container_name}{container_id} failed! " f"{self.cover} is not a valid seal or cover type." ) if not isinstance(self.properties, dict): self.properties = dict() if not isinstance(self.ctx_properties, dict): self.ctx_properties = dict() self._wells = [Well(self, idx) for idx in range(self.container_type.well_count)]
[docs] def well(self, i) -> Well: """ Return a Well object representing the well at the index specified of this Container. Parameters ---------- i : int, str Well reference in the form of an integer (ex: 0) or human-readable string (ex: "A1"). Returns ------- Well Well for given reference Raises ------ TypeError index given is not of the right type """ if not isinstance(i, (int, str)): raise TypeError("Well reference given is not of type 'int' or " "'str'.") return self._wells[self.robotize(i)]
[docs] def well_from_coordinates(self, row, column): """ Gets the well at 0-indexed position (row, column) within the container. The origin is in the top left corner. Parameters ---------- row : int The 0-indexed row index of the well to be fetched column : int The 0-indexed column index of the well to be fetched Returns ------- Well The well at position (row, column) """ return self.well( self.container_type.well_from_coordinates(row=row, column=column) )
[docs] def tube(self): """ Checks if container is tube and returns a Well representing the zeroth well. Returns ------- Well Zeroth well of tube Raises ------- AttributeError If container is not tube """ if self.container_type.is_tube: return self.well(0) else: raise AttributeError( f"{self} is a {self.container_type.shortname} " f"and is not a tube" )
[docs] def wells(self, *args): """ Return a WellGroup containing references to wells corresponding to the index or indices given. Parameters ---------- args : str, int, list Reference or list of references to a well index either as an integer or a string. Returns ------- WellGroup Wells from specified references Raises ------ TypeError Well reference is not of a valid input type """ if isinstance(args[0], list): wells = args[0] else: wells = [args[0]] for a in args[1:]: if isinstance(a, list): wells.extend(a) else: wells.extend([a]) for w in wells: if not isinstance(w, (str, int, list)): raise TypeError( "Well reference given is not of type" " 'int', 'str' or 'list'." ) return WellGroup([self.well(w) for w in wells])
[docs] def robotize(self, well_ref): """ Return the integer representation of the well index given, based on the ContainerType of the Container. Uses the robotize function from the ContainerType class. Refer to `ContainerType.robotize()` for more information. """ if not isinstance(well_ref, (str, int, Well, list)): raise TypeError( "Well reference given is not of type 'str' " "'int', 'Well' or 'list'." ) return self.container_type.robotize(well_ref)
[docs] def humanize(self, well_ref): """ Return the human readable representation of the integer well index given based on the ContainerType of the Container. Uses the humanize function from the ContainerType class. Refer to `ContainerType.humanize()` for more information. """ if not isinstance(well_ref, (int, str, list)): raise TypeError( "Well reference given is not of type 'int'," "'str' or 'list'." ) return self.container_type.humanize(well_ref)
[docs] def decompose(self, well_ref): """ Return a tuple representing the column and row number of the well index given based on the ContainerType of the Container. Uses the decompose function from the ContainerType class. Refer to `ContainerType.decompose()` for more information. """ if not isinstance(well_ref, (int, str, Well)): raise TypeError( "Well reference given is not of type 'int', " "'str' or Well." ) return self.container_type.decompose(well_ref)
[docs] def all_wells(self, columnwise=False): """ Return a WellGroup representing all Wells belonging to this Container. Parameters ---------- columnwise : bool, optional returns the WellGroup columnwise instead of rowwise (ordered by well index). Returns ------- WellGroup WellGroup of all Wells in Container """ if columnwise: num_cols = self.container_type.col_count num_rows = self.container_type.well_count // num_cols return WellGroup( [ self._wells[row * num_cols + col] for col in range(num_cols) for row in range(num_rows) ] ) else: return WellGroup(self._wells)
[docs] def inner_wells(self, columnwise=False): """ Return a WellGroup of all wells on a plate excluding wells in the top and bottom rows and in the first and last columns. Parameters ---------- columnwise : bool, optional returns the WellGroup columnwise instead of rowwise (ordered by well index). Returns ------- WellGroup WellGroup of inner wells """ num_cols = self.container_type.col_count num_rows = self.container_type.row_count() inner_wells = [] if columnwise: for c in range(1, num_cols - 1): wells = [] for r in range(1, num_rows - 1): wells.append((r * num_cols) + c) inner_wells.extend(wells) else: well = num_cols for _ in range(1, num_rows - 1): inner_wells.extend(range(well + 1, well + (num_cols - 1))) well += num_cols inner_wells = [self._wells[x] for x in inner_wells] return WellGroup(inner_wells)
[docs] def wells_from(self, start, num, columnwise=False): """ Return a WellGroup of Wells belonging to this Container starting from the index indicated (in integer or string form) and including the number of proceeding wells specified. Wells are counted from the starting well rowwise unless columnwise is True. Parameters ---------- start : Well or int or str Starting well specified as a Well object, a human-readable well index or an integer well index. num : int Number of wells to include in the Wellgroup. columnwise : bool, optional Specifies whether the wells included should be counted columnwise instead of the default rowwise. Returns ------- WellGroup WellGroup of selected wells Raises ------ TypeError Incorrect input types, e.g. `num` has to be of type int """ if not isinstance(start, (str, int, Well)): raise TypeError( "Well reference given is not of type 'str'," "'int', or 'Well'." ) if not isinstance(num, int): raise TypeError("Number of wells given is not of type 'int'.") start = self.robotize(start) if columnwise: row, col = self.decompose(start) num_rows = self.container_type.row_count() start = col * num_rows + row return WellGroup(self.all_wells(columnwise).wells[start : start + num])
[docs] def is_sealed(self): """ Check if Container is sealed. """ return self.cover in SEAL_TYPES
[docs] def is_covered(self): """ Check if Container is covered. """ return self.cover in COVER_TYPES
[docs] def quadrant(self, quad): """ Return a WellGroup of Wells corresponding to the selected quadrant of this Container. Parameters ---------- quad : int or str Specifies the quadrant number of the well (ex. 2) Returns ------- WellGroup WellGroup of wells for the specified quadrant Raises ------ ValueError Invalid quadrant specified for this Container type """ # TODO(Define what each quadrant number corresponds toL) if isinstance(quad, str): quad = quad.lower() if quad == "a1": quad = 0 elif quad == "a2": quad = 1 elif quad == "b1": quad = 2 elif quad == "b2": quad = 3 else: raise ValueError("Invalid quadrant index.") # n_wells: n_cols allowed_layouts = {96: 12, 384: 24} n_wells = self.container_type.well_count if ( n_wells not in allowed_layouts or self.container_type.col_count != allowed_layouts[n_wells] ): raise ValueError( "Quadrant is only defined for standard 96 and " "384-well plates" ) if n_wells == 96: if quad == 0: return WellGroup(self._wells) else: raise ValueError( "0 or 'A1' is the only valid quadrant for a 96-well " "plate." ) if quad not in [0, 1, 2, 3]: raise ValueError( f"Invalid quadrant {quad} for plate type " f"{str(self.name)}" ) start_well = [0, 1, 24, 25] wells = [] for row_offset in range(start_well[quad], 384, 48): for col_offset in range(0, 24, 2): wells.append(row_offset + col_offset) return self.wells(wells)
[docs] def set_storage(self, storage): """ Set the storage condition of a container, will overwrite an existing storage condition, will remove discard True. Parameters ---------- storage : str Storage condition. Returns ------- Container Container with modified storage condition Raises ---------- TypeError If storage condition not of type str. """ if not isinstance(storage, str): raise TypeError( f"Storage condition given ({storage}) is not of " f"type str. {type(storage)}." ) self.storage = storage return self
[docs] def discard(self): """ Set the storage condition of a container to None and container to be discarded if ref in protocol. Example ---------- .. code-block:: python p = Protocol() container = p.ref("new_container", cont_type="96-pcr", storage="cold_20") p.incubate(c, "warm_37", "30:minute") container.discard() Autoprotocol generated: .. code-block:: json "refs": { "new_container": { "new": "96-pcr", "discard": true } } """ self.storage = None return self
# pylint: disable=too-many-locals
[docs] def wells_from_shape(self, origin, shape): """ Gets a WellGroup that originates from the `origin` and is distributed across the container in `shape`. This group has a Well for each index in `range(shape["rows"] * shape["columns"])`. In cases where the container dimensions are smaller than the shape format's dimensions the returned WellGroup will reference some wells multiple times. This is analogous to an SBS96-formatted liquid handler acting with multiple tips in each well of an SBS24-formatted plate. Parameters ---------- origin : int or str The index of the top left corner origin of the shape shape : dict See Also Instruction.builders.shape Returns ------- WellGroup The group of wells distributed in `shape` from the `origin` Raises ------ ValueError if the shape exceeds the extents of the container """ from .instruction import Instruction shape = Instruction.builders.shape(**shape) origin = self.well(origin) # unpacking container and shape format properties container_rows = self.container_type.row_count() container_cols = self.container_type.col_count format_rows = SBS_FORMAT_SHAPES[shape["format"]]["rows"] format_cols = SBS_FORMAT_SHAPES[shape["format"]]["columns"] # getting the row and column values for the origin origin_row, origin_col = self.decompose(origin) # ratios of container shape to format shape row_scaling = container_rows / format_rows col_scaling = container_cols / format_cols # the 0-indexed coordinates of all wells in origin plate to be included well_rows = [] well_cols = [] for idx in range(shape["rows"]): well_row = int(origin_row + idx * row_scaling) well_rows.append(well_row) for idx in range(shape["columns"]): well_col = int(origin_col + idx * col_scaling) well_cols.append(well_col) # coordinates of the tail (bottom right well) should not exceed bounds tail_row = well_rows[-1] tail_col = well_cols[-1] # tail_row and tail_col are 0-indexed based # container_rows and container_cols are 1-indexed based if tail_row + 1 > container_rows or tail_col + 1 > container_cols: raise ValueError( f"origin: {origin} with shape: {shape} exceeds the bounds of " f"container: {self}" ) return WellGroup( [self.well_from_coordinates(x, y) for x in well_rows for y in well_cols] )
[docs] def __repr__(self): """ Return a string representation of a Container using the specified name. (ex. Container('my_plate')) """ return ( f"Container({str(self.name)}" f"{', cover=' + self.cover if self.cover else ''})" )