Skip to content

Commit

Permalink
feat(api): Add labware load to protocol API
Browse files Browse the repository at this point in the history
Closes #2240
  • Loading branch information
sfoster1 committed Oct 12, 2018
1 parent 241a8a3 commit 481f401
Show file tree
Hide file tree
Showing 6 changed files with 149 additions and 25 deletions.
1 change: 1 addition & 0 deletions api/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
84 changes: 74 additions & 10 deletions api/src/opentrons/protocol_api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand All @@ -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) \
Expand Down Expand Up @@ -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:
Expand All @@ -308,3 +315,60 @@ 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: types.DeckLocation) -> int:
if isinstance(key, str):
return int(key)
elif isinstance(key, int):
return key
else:
raise TypeError(type(key))

def _check_name(self, key: types.DeckLocation) -> int:
try:
key_int = Deck._assure_int(key)
except Exception as e:
MODULE_LOG.exception("Bad slot name: {}".format(key))
key_int = None
if key_int not in self.data:
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: types.DeckLocation) -> bool:
try:
key_int = self._check_name(key)
except ValueError:
key_int = None
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]
22 changes: 7 additions & 15 deletions api/src/opentrons/protocol_api/labware.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]:
Expand Down Expand Up @@ -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)
4 changes: 4 additions & 0 deletions api/src/opentrons/types.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import enum
from typing import Union
from collections import namedtuple

Point = namedtuple('Point', ['x', 'y', 'z'])
Expand All @@ -7,3 +8,6 @@
class Mount(enum.Enum):
LEFT = enum.auto()
RIGHT = enum.auto()


DeckLocation = Union[int, str]
24 changes: 24 additions & 0 deletions api/tests/opentrons/protocol_api/test_context.py
Original file line number Diff line number Diff line change
@@ -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'
39 changes: 39 additions & 0 deletions api/tests/opentrons/protocol_api/test_labware_load.py
Original file line number Diff line number Diff line change
@@ -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]

0 comments on commit 481f401

Please sign in to comment.