diff --git a/api/opentrons/protocol_api/labware.py b/api/opentrons/protocol_api/labware.py index d8adc43dffb..cdb6c350b27 100644 --- a/api/opentrons/protocol_api/labware.py +++ b/api/opentrons/protocol_api/labware.py @@ -1,7 +1,9 @@ """This module will replace Placeable""" +import re from typing import List, Dict from enum import Enum, auto from opentrons.types import Point +from collections import defaultdict class WellShape(Enum): @@ -116,25 +118,103 @@ class Labware: provides methods for accessing wells within the labware. """ def __init__(self, definition: dict, parent: Point) -> None: - pass + self._ordering = [well + for col in definition['ordering'] + for well in col] + self._wells = definition['wells'] + offset = definition['cornerOffsetFromSlot'] + self._offset = Point(x=offset['x'], y=offset['y'], z=offset['z']) + self._pattern = re.compile(r'^([A-Z]+)([1-9][0-9]*)$', re.X) def wells(self) -> List[Well]: - pass + """ + Accessor function used to generate a list of wells in top -> down, + left -> right order. This is representative of moving down `rows` and + across `columns` (e.g. 'A1', 'B1', 'C1'...'A2', 'B2', 'C2') + + With indexing one can treat it as a typical python + list. To access well A1, for example, simply write: labware.wells()[0] + + :return: Ordered list of all wells in a labware + """ + return [Well(self._wells[well], self._offset) + for well in self._ordering] def wells_by_index(self) -> Dict[str, Well]: - pass + """ + Accessor function used to create a look-up table of Wells by name. + + With indexing one can treat it as a typical python + dictionary whose keys are well names. To access well A1, for example, + simply write: labware.wells_by_index()['A1'] + + :return: Dictionary of well objects keyed by well name + """ + return {well: Well(self._wells[well], self._offset) + for well in self._ordering} def rows(self) -> List[List[Well]]: - pass + """ + Accessor function used to navigate through a labware by row. + + With indexing one can treat it as a typical python nested list. + To access row A for example, simply write: labware.rows()[0]. This + will output ['A1', 'A2', 'A3', 'A4'...] + + :return: A list of row lists + """ + rowDict = self._create_indexed_dictionary(group=1) + keys = sorted(rowDict) + return [rowDict[key] for key in keys] def rows_by_index(self) -> Dict[str, List[Well]]: - pass + """ + Accessor function used to navigate through a labware by row name. + + With indexing one can treat it as a typical python dictionary. + To access row A for example, simply write: labware.rows_by_index()['A'] + This will output ['A1', 'A2', 'A3', 'A4'...]. + + :return: Dictionary of Well lists keyed by row name + """ + rowDict = self._create_indexed_dictionary(group=1) + return rowDict def columns(self) -> List[List[Well]]: - pass + """ + Accessor function used to navigate through a labware by column. + + With indexing one can treat it as a typical python nested list. + To access row A for example, + simply write: labware.columns()[0] + This will output ['A1', 'B1', 'C1', 'D1'...]. + + :return: A list of column lists + """ + colDict = self._create_indexed_dictionary(group=2) + keys = sorted(colDict) + return [colDict[key] for key in keys] def columns_by_index(self) -> Dict[str, List[Well]]: - pass + """ + Accessor function used to navigate through a labware by column name. + + With indexing one can treat it as a typical python dictionary. + To access row A for example, + simply write: labware.columns_by_index()['1'] + This will output ['A1', 'B1', 'C1', 'D1'...]. + + :return: Dictionary of Well lists keyed by column name + """ + colDict = self._create_indexed_dictionary(group=2) + return colDict + + def _create_indexed_dictionary(self, group=0): + dictList = defaultdict(list) + for well in self._ordering: + wellObj = Well(self._wells[well], self._offset) + dictList[self._pattern.match(well).group(group)].append(wellObj) + return dictList def _load_definition_by_name(name: str) -> dict: diff --git a/api/tests/opentrons/protocol_api/test_accessor_fn.py b/api/tests/opentrons/protocol_api/test_accessor_fn.py new file mode 100644 index 00000000000..27f4e38536b --- /dev/null +++ b/api/tests/opentrons/protocol_api/test_accessor_fn.py @@ -0,0 +1,201 @@ +from opentrons.protocol_api import labware +from opentrons.types import Point + +minimalLabwareDef = { + "cornerOffsetFromSlot": { + "x": 10, + "y": 10, + "z": 5 + }, + "ordering": [["A1"], ["A2"]], + "wells": { + "A1": { + "depth": 40, + "totalLiquidVolume": 100, + "diameter": 30, + "x": 0, + "y": 0, + "z": 0, + "shape": "circular" + }, + "A2": { + "depth": 40, + "totalLiquidVolume": 100, + "diameter": 30, + "x": 10, + "y": 0, + "z": 0, + "shape": "circular" + } + } +} + +minimalLabwareDef2 = { + "cornerOffsetFromSlot": { + "x": 10, + "y": 10, + "z": 5 + }, + "ordering": [["A1", "B1", "C1"], ["A2", "B2", "C2"]], + "wells": { + "A1": { + "depth": 40, + "totalLiquidVolume": 100, + "diameter": 30, + "x": 0, + "y": 18, + "z": 0, + "shape": "circular" + }, + "B1": { + "depth": 40, + "totalLiquidVolume": 100, + "diameter": 30, + "x": 0, + "y": 9, + "z": 0, + "shape": "circular" + }, + "C1": { + "depth": 40, + "totalLiquidVolume": 100, + "diameter": 30, + "x": 0, + "y": 0, + "z": 0, + "shape": "circular" + }, + "A2": { + "depth": 40, + "totalLiquidVolume": 100, + "diameter": 30, + "x": 9, + "y": 18, + "z": 0, + "shape": "circular" + }, + "B2": { + "depth": 40, + "totalLiquidVolume": 100, + "diameter": 30, + "x": 9, + "y": 9, + "z": 0, + "shape": "circular" + }, + "C2": { + "depth": 40, + "totalLiquidVolume": 100, + "diameter": 30, + "x": 9, + "y": 0, + "z": 0, + "shape": "circular" + } + } +} + + +def test_labware_init(): + deck = Point(0, 0, 0) + fakeLabware = labware.Labware(minimalLabwareDef, deck) + ordering = [well for col in minimalLabwareDef['ordering'] for well in col] + assert fakeLabware._ordering == ordering + assert fakeLabware._wells == minimalLabwareDef['wells'] + assert fakeLabware._offset == Point(x=10, y=10, z=5) + + +def test_well_pattern(): + deck = Point(0, 0, 0) + fakeLabware = labware.Labware(minimalLabwareDef, deck) + assert fakeLabware._pattern.match('A1') + assert fakeLabware._pattern.match('A10') + assert not fakeLabware._pattern.match('A0') + + +def test_wells_accessor(): + deck = Point(0, 0, 0) + fakeLabware = labware.Labware(minimalLabwareDef, deck) + depth1 = minimalLabwareDef['wells']['A1']['depth'] + depth2 = minimalLabwareDef['wells']['A2']['depth'] + x = minimalLabwareDef['wells']['A2']['x'] + y = minimalLabwareDef['wells']['A2']['y'] + offset = fakeLabware._offset + a1 = Point(x=offset[0], y=offset[1], z=offset[2] + depth1) + a2 = Point(x=offset[0] + x, y=offset[1] + y, z=offset[2] + depth2) + assert fakeLabware.wells()[0]._position == a1 + assert fakeLabware.wells()[1]._position == a2 + + +def test_wells_index_accessor(): + deck = Point(0, 0, 0) + fakeLabware = labware.Labware(minimalLabwareDef, deck) + depth1 = minimalLabwareDef['wells']['A1']['depth'] + depth2 = minimalLabwareDef['wells']['A2']['depth'] + x = minimalLabwareDef['wells']['A2']['x'] + y = minimalLabwareDef['wells']['A2']['y'] + offset = fakeLabware._offset + a1 = Point(x=offset[0], y=offset[1], z=offset[2] + depth1) + a2 = Point(x=offset[0] + x, y=offset[1] + y, z=offset[2] + depth2) + assert fakeLabware.wells_by_index()['A1']._position == a1 + assert fakeLabware.wells_by_index()['A2']._position == a2 + + +def test_rows_accessor(): + deck = Point(0, 0, 0) + fakeLabware = labware.Labware(minimalLabwareDef2, deck) + depth1 = minimalLabwareDef2['wells']['A1']['depth'] + x1 = minimalLabwareDef2['wells']['A1']['x'] + y1 = minimalLabwareDef2['wells']['A1']['y'] + depth2 = minimalLabwareDef2['wells']['B2']['depth'] + x2 = minimalLabwareDef2['wells']['B2']['x'] + y2 = minimalLabwareDef2['wells']['B2']['y'] + offset = fakeLabware._offset + a1 = Point(x=offset[0] + x1, y=offset[1] + y1, z=offset[2] + depth1) + b2 = Point(x=offset[0] + x2, y=offset[1] + y2, z=offset[2] + depth2) + assert fakeLabware.rows()[0][0]._position == a1 + assert fakeLabware.rows()[1][1]._position == b2 + + +def test_row_index_accessor(): + deck = Point(0, 0, 0) + fakeLabware = labware.Labware(minimalLabwareDef2, deck) + depth1 = minimalLabwareDef2['wells']['A1']['depth'] + x1 = minimalLabwareDef2['wells']['A1']['x'] + y1 = minimalLabwareDef2['wells']['A1']['y'] + depth2 = minimalLabwareDef2['wells']['B2']['depth'] + x2 = minimalLabwareDef2['wells']['B2']['x'] + y2 = minimalLabwareDef2['wells']['B2']['y'] + offset = fakeLabware._offset + a1 = Point(x=offset[0] + x1, y=offset[1] + y1, z=offset[2] + depth1) + b2 = Point(x=offset[0] + x2, y=offset[1] + y2, z=offset[2] + depth2) + assert fakeLabware.rows_by_index()['A'][0]._position == a1 + assert fakeLabware.rows_by_index()['B'][1]._position == b2 + + +def test_cols_accessor(): + deck = Point(0, 0, 0) + fakeLabware = labware.Labware(minimalLabwareDef, deck) + depth1 = minimalLabwareDef['wells']['A1']['depth'] + depth2 = minimalLabwareDef['wells']['A2']['depth'] + x = minimalLabwareDef['wells']['A2']['x'] + y = minimalLabwareDef['wells']['A2']['y'] + offset = fakeLabware._offset + a1 = Point(x=offset[0], y=offset[1], z=offset[2] + depth1) + a2 = Point(x=offset[0] + x, y=offset[1] + y, z=offset[2] + depth2) + assert fakeLabware.columns()[0][0]._position == a1 + assert fakeLabware.columns()[1][0]._position == a2 + + +def test_col_index_accessor(): + deck = Point(0, 0, 0) + fakeLabware = labware.Labware(minimalLabwareDef, deck) + depth1 = minimalLabwareDef['wells']['A1']['depth'] + depth2 = minimalLabwareDef['wells']['A2']['depth'] + x = minimalLabwareDef['wells']['A2']['x'] + y = minimalLabwareDef['wells']['A2']['y'] + offset = fakeLabware._offset + a1 = Point(x=offset[0], y=offset[1], z=offset[2] + depth1) + a2 = Point(x=offset[0] + x, y=offset[1] + y, z=offset[2] + depth2) + assert fakeLabware.columns_by_index()['1'][0]._position == a1 + assert fakeLabware.columns_by_index()['2'][0]._position == a2