diff --git a/api/setup.py b/api/setup.py index c63ddea625a..f6b0c0a4b0f 100755 --- a/api/setup.py +++ b/api/setup.py @@ -16,6 +16,7 @@ SHARED_DATA_SUBDIRS = ['labware-json-schema', 'protocol-json-schema', 'definitions', + 'definitions2', 'robot-data'] # Where, relative to the package root, we put the files we copy DEST_BASE_PATH = 'shared_data' diff --git a/api/src/opentrons/protocol_api/__init__.py b/api/src/opentrons/protocol_api/__init__.py index 580e56c2ccb..b90d60270f6 100644 --- a/api/src/opentrons/protocol_api/__init__.py +++ b/api/src/opentrons/protocol_api/__init__.py @@ -4,13 +4,18 @@ control the OT2. """ +from collections import UserDict import enum +import logging import os from typing import List, Dict from opentrons.protocol_api.labware import Well, Labware, load +from opentrons import types from . import back_compat +MODULE_LOG = logging.getLogger(__name__) + def run(protocol_bytes: bytes = None, protocol_json: str = None, @@ -52,21 +57,22 @@ class ProtocolContext: """ def __init__(self): - pass + self._deck_layout = Deck() def load_labware( - self, labware_obj: Labware, location: str, - label: str = None, share: bool = False): + self, labware_obj: Labware, location: types.DeckLocation, + label: str = None, share: bool = False) -> Labware: """ Specify the presence of a piece of labware on the OT2 deck. This function loads the labware specified by ``labware`` (previously loaded from a configuration file) to the location specified by ``location``. """ - pass + self._deck_layout[location] = labware_obj + return labware_obj def load_labware_by_name( - self, labware_name: str, location: str) -> Labware: + self, labware_name: str, location: types.DeckLocation) -> Labware: """ A convenience function to specify a piece of labware by name. For labware already defined by Opentrons, this is a convient way @@ -76,18 +82,18 @@ def load_labware_by_name( This function returns the created and initialized labware for use later in the protocol. """ - labware = load(labware_name, location) - self.load_labware(labware, location) - return labware + labware = load(labware_name, + self._deck_layout.position_for(location)) + return self.load_labware(labware, location) @property - def loaded_labwares(self) -> Dict[str, Labware]: + def loaded_labwares(self) -> Dict[int, Labware]: """ Get the labwares that have been loaded into the protocol context. The return value is a dict mapping locations to labware, sorted in order of the locations. """ - pass + return dict(self._deck_layout) def load_instrument( self, instrument_name: str, mount: str) \ @@ -283,6 +289,7 @@ def pick_up_current(self, amps: float): :param amps: The current, in amperes. Acceptable values: (0.0, 2.0) """ + pass @property def type(self) -> TYPE: @@ -308,3 +315,62 @@ def trash_container(self) -> Labware: @trash_container.setter def trash_container(self, trash: Labware): pass + + +class Deck(UserDict): + def __init__(self): + super().__init__() + row_offset = 90.5 + col_offset = 132.5 + for idx in range(1, 13): + self.data[idx] = None + self._positions = {idx+1: types.Point((idx % 3) * col_offset, + idx//3 * row_offset, + 0) + for idx in range(12)} + + @staticmethod + def _assure_int(key: object) -> int: + if isinstance(key, str): + return int(key) + elif isinstance(key, int): + return key + else: + raise TypeError(type(key)) + + def _check_name(self, key: object) -> int: + should_raise = False + try: + key_int = Deck._assure_int(key) + except Exception: + MODULE_LOG.exception("Bad slot name: {}".format(key)) + should_raise = True + should_raise = should_raise or key_int not in self.data + if should_raise: + raise ValueError("Unknown slot: {}".format(key)) + else: + return key_int + + def __getitem__(self, key: types.DeckLocation) -> Labware: + return self.data[self._check_name(key)] + + def __delitem__(self, key: types.DeckLocation) -> None: + self.data[self._check_name(key)] = None + + def __setitem__(self, key: types.DeckLocation, val: Labware) -> None: + key_int = self._check_name(key) + if self.data.get(key_int) is not None: + raise ValueError('Deck location {} already has an item: {}' + .format(key, self.data[key_int])) + self.data[key_int] = val + + def __contains__(self, key: object) -> bool: + try: + key_int = self._check_name(key) + except ValueError: + return False + return key_int in self.data + + def position_for(self, key: types.DeckLocation) -> types.Point: + key_int = self._check_name(key) + return self._positions[key_int] diff --git a/api/src/opentrons/protocol_api/labware.py b/api/src/opentrons/protocol_api/labware.py index cdb6c350b27..43298f32c68 100644 --- a/api/src/opentrons/protocol_api/labware.py +++ b/api/src/opentrons/protocol_api/labware.py @@ -123,7 +123,9 @@ def __init__(self, definition: dict, parent: Point) -> None: for well in col] self._wells = definition['wells'] offset = definition['cornerOffsetFromSlot'] - self._offset = Point(x=offset['x'], y=offset['y'], z=offset['z']) + self._offset = Point(x=offset['x'] + parent.x, + y=offset['y'] + parent.y, + z=offset['z'] + parent.z) self._pattern = re.compile(r'^([A-Z]+)([1-9][0-9]*)$', re.X) def wells(self) -> List[Well]: @@ -226,28 +228,18 @@ def _load_definition_by_name(name: str) -> dict: raise NotImplementedError -def _get_slot_position(slot: str) -> Point: - """ - :param slot: a string corresponding to a slot on the deck - :return: a Point representing the position of the lower-left corner of the - slot - """ - raise NotImplementedError - - -def load(name: str, slot: str) -> Labware: +def load(name: str, ll_at: Point) -> Labware: """ Return a labware object constructed from a labware definition dict looked up by name (definition must have been previously stored locally on the robot) """ definition = _load_definition_by_name(name) - return load_from_definition(definition, slot) + return load_from_definition(definition, ll_at) -def load_from_definition(definition: dict, slot: str) -> Labware: +def load_from_definition(definition: dict, ll_at: Point) -> Labware: """ Return a labware object constructed from a provided labware definition dict """ - point = _get_slot_position(slot) - return Labware(definition, point) + return Labware(definition, ll_at) diff --git a/api/src/opentrons/types.py b/api/src/opentrons/types.py index d971e43ddfb..2a0de24663f 100644 --- a/api/src/opentrons/types.py +++ b/api/src/opentrons/types.py @@ -1,4 +1,5 @@ import enum +from typing import Union from collections import namedtuple Point = namedtuple('Point', ['x', 'y', 'z']) @@ -7,3 +8,6 @@ class Mount(enum.Enum): LEFT = enum.auto() RIGHT = enum.auto() + + +DeckLocation = Union[int, str] diff --git a/api/tests/opentrons/protocol_api/test_context.py b/api/tests/opentrons/protocol_api/test_context.py new file mode 100644 index 00000000000..3c786e1ff31 --- /dev/null +++ b/api/tests/opentrons/protocol_api/test_context.py @@ -0,0 +1,24 @@ +""" Test the functions and classes in the protocol context """ + +from opentrons import protocol_api as papi + +import pytest + + +def test_slot_names(): + slots_by_int = list(range(1, 13)) + slots_by_str = [str(idx) for idx in slots_by_int] + for method in (slots_by_int, slots_by_str): + d = papi.Deck() + for idx, slot in enumerate(method): + assert slot in d + d[slot] = 'its real' + with pytest.raises(ValueError): + d[slot] = 'not this time boyo' + del d[slot] + assert slot in d + assert d[slot] is None + + assert 'hasasdaia' not in d + with pytest.raises(ValueError): + d['ahgoasia'] = 'nope' diff --git a/api/tests/opentrons/protocol_api/test_labware_load.py b/api/tests/opentrons/protocol_api/test_labware_load.py new file mode 100644 index 00000000000..a5a9fa265d8 --- /dev/null +++ b/api/tests/opentrons/protocol_api/test_labware_load.py @@ -0,0 +1,39 @@ +import json +import pkgutil +from opentrons import protocol_api as papi, types + +# TODO: Remove this when labware load is actually wired up +labware_name = 'generic_96_wellPlate_380_uL' +labware_def = json.loads( + pkgutil.get_data('opentrons', + 'shared_data/definitions2/{}.json'.format(labware_name))) + + +def test_load_to_slot(monkeypatch): + def dummy_load(labware): + return labware_def + monkeypatch.setattr(papi.labware, '_load_definition_by_name', dummy_load) + ctx = papi.ProtocolContext() + labware = ctx.load_labware_by_name(labware_name, '1') + assert labware._offset == types.Point(0, 0, 0) + other = ctx.load_labware_by_name(labware_name, 2) + assert other._offset == types.Point(132.5, 0, 0) + + +def test_loaded(monkeypatch): + def dummy_load(labware): + return labware_def + monkeypatch.setattr(papi.labware, '_load_definition_by_name', dummy_load) + ctx = papi.ProtocolContext() + labware = ctx.load_labware_by_name(labware_name, '1') + assert ctx.loaded_labwares[1] == labware + + +def test_from_backcompat(monkeypatch): + def dummy_load(labware): + return labware_def + monkeypatch.setattr(papi.labware, '_load_definition_by_name', dummy_load) + ctx = papi.ProtocolContext() + papi.back_compat.reset(ctx) + lw = papi.back_compat.labware.load(labware_name, 3) + assert lw == ctx.loaded_labwares[3] diff --git a/shared-data/definitions2/Opentrons_96_tiprack_300_uL.json b/shared-data/definitions2/Opentrons_96_tiprack_300_uL.json index 38fed45d34b..5e2e42f0f7e 100644 --- a/shared-data/definitions2/Opentrons_96_tiprack_300_uL.json +++ b/shared-data/definitions2/Opentrons_96_tiprack_300_uL.json @@ -135,6 +135,7 @@ "opentrons" ] }, + "cornerOffsetFromSlot": { "x": 0, "y": 0, "z": 0}, "dimensions": { "overallLength": 127.76, "overallWidth": 85.48, diff --git a/shared-data/definitions2/generic_96_wellPlate_380_uL.json b/shared-data/definitions2/generic_96_wellPlate_380_uL.json index 85d068ade56..ba237c53bf4 100644 --- a/shared-data/definitions2/generic_96_wellPlate_380_uL.json +++ b/shared-data/definitions2/generic_96_wellPlate_380_uL.json @@ -136,6 +136,7 @@ "generic" ] }, + "cornerOffsetFromSlot": { "x": 0, "y": 0, "z": 0}, "dimensions": { "overallLength": 127.76, "overallWidth": 85.48,