Skip to content

Commit

Permalink
refactor(api): Change motion target locations and add arcs (#2598)
Browse files Browse the repository at this point in the history
* refactor(api): Change motion target locations and add arcs

The motion commands in the protocol API take a new type, Location, which has a
reference to a labware or well it is related to. This labware or well is used by
a new function, geometry.plan_arc, to break down individual motion commands into
either arc or direct moves depending on if the target is in the same well or
labware as the target of the last move.

In addition, change the Well class position accessors (top(), bottom(),
center()) to return a Location instead of just a Point, and build Wells with
references to their parent Labwares.
  • Loading branch information
sfoster1 authored Nov 1, 2018
1 parent be54bd3 commit 2d97353
Show file tree
Hide file tree
Showing 12 changed files with 546 additions and 320 deletions.
11 changes: 11 additions & 0 deletions api/src/opentrons/hardware_control/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,17 @@ def current_position(self, mount: top_types.Mount) -> Dict[Axis, float]:
plunger_ax: self._current_position[plunger_ax]
}

def gantry_position(self, mount: top_types.Mount) -> top_types.Point:
""" Return the position of the critical point as pertains to the gantry
This ignores the plunger position and gives the Z-axis a predictable
name (as :py:attr:`.Point.z`).
"""
cur_pos = self.current_position(mount)
return top_types.Point(x=cur_pos[Axis.X],
y=cur_pos[Axis.Y],
z=cur_pos[Axis.by_mount(mount)])

@_log_call
async def move_to(
self, mount: top_types.Mount, abs_position: top_types.Point,
Expand Down
8 changes: 6 additions & 2 deletions api/src/opentrons/protocol_api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import logging
import os

from . import back_compat
from . import back_compat, labware
from .contexts import ProtocolContext, InstrumentContext


Expand Down Expand Up @@ -45,4 +45,8 @@ def run(protocol_bytes: bytes = None,
pass


__all__ = ['run', 'ProtocolContext', 'InstrumentContext', 'back_compat']
__all__ = ['run',
'ProtocolContext',
'InstrumentContext',
'back_compat',
'labware']
238 changes: 57 additions & 181 deletions api/src/opentrons/protocol_api/contexts.py

Large diffs are not rendered by default.

95 changes: 73 additions & 22 deletions api/src/opentrons/protocol_api/geometry.py
Original file line number Diff line number Diff line change
@@ -1,33 +1,84 @@
from collections import UserDict
import functools
import logging
from typing import Union, Tuple
from typing import List, Optional, Tuple

from opentrons.protocol_api.labware import Labware, Well
from opentrons import types
from .labware import Labware, Well


MODULE_LOG = logging.getLogger(__name__)

Location = Union[Labware, Well, types.Point, Tuple[float, float, float]]

def max_many(*args):
return functools.reduce(max, args[1:], args[0])


def plan_moves(from_loc: types.Location,
to_loc: types.Location,
deck: 'Deck',
well_z_margin: float = 5.0,
lw_z_margin: float = 20.0) -> List[types.Point]:
""" Plan moves between one :py:class:`.Location` and another.
Each :py:class:`.Location` instance might or might not have a specific
kind of geometry attached. This function is intended to return series
of moves that contain the minimum safe retractions to avoid (known)
labware on the specified :py:class:`Deck`.
:param from_loc: The last location.
:param to_loc: The location to move to.
:param deck: The :py:class:`Deck` instance describing the robot.
:param float well_z_margin: How much extra Z margin to raise the cp by over
the bare minimum to clear wells within the same
labware. Default: 5mm
:param float lw_z_margin: How much extra Z margin to raise the cp by over
the bare minimum to clear different pieces of
labware. Default: 20mm
:returns: A list of :py:class:`.Point` to move through.
"""

def _split_loc_labware(
loc: types.Location) -> Tuple[Optional[Labware], Optional[Well]]:
if isinstance(loc.labware, Labware):
return loc.labware, None
elif isinstance(loc.labware, Well):
return loc.labware.parent, loc.labware
else:
return None, None

def point_from_location(location: Location) -> types.Point:
""" Build a deck-abs point from anything the user passes in """
to_point = to_loc.point
to_lw, to_well = _split_loc_labware(to_loc)
from_point = from_loc.point
from_lw, from_well = _split_loc_labware(from_loc)

# Defined with an inner function like this to make logging the result
# a bit less tedious and reasonably mypy-compliant
def _point(loc: Location) -> types.Point:
if isinstance(location, Well):
return location.top()
elif isinstance(location, Labware):
return location.wells()[0].top()
elif isinstance(location, tuple):
return types.Point(*location[:3])
if to_lw and to_lw == from_lw:
# Two valid labwares. We’ll either raise to clear a well or go direct
if to_well and to_well == from_well:
return [to_point]
else:
return location

point = _point(location)
MODULE_LOG.debug("Location {} -> {}".format(location, point))
return point
if to_well:
to_safety = to_well.top().point.z + well_z_margin
else:
to_safety = to_lw.highest_z + well_z_margin
if from_well:
from_safety = from_well.top().point.z + well_z_margin
else:
from_safety = from_lw.highest_z + well_z_margin
safe = max_many(
to_point.z,
from_point.z,
to_safety,
from_safety)
else:
# For now, the only fallback is to clear all known labware
safe = max_many(to_point.z,
from_point.z,
deck.highest_z + lw_z_margin)
return [from_point._replace(z=safe),
to_point._replace(z=safe),
to_point]


class Deck(UserDict):
Expand Down Expand Up @@ -65,7 +116,7 @@ def _check_name(self, key: object) -> int:
else:
return key_int

def __getitem__(self, key: types.DeckLocation) -> Labware:
def __getitem__(self, key: types.DeckLocation) -> 'Labware':
return self.data[self._check_name(key)]

def __delitem__(self, key: types.DeckLocation) -> None:
Expand All @@ -77,13 +128,13 @@ def __delitem__(self, key: types.DeckLocation) -> None:
for item in [lw for lw in self.data.values() if lw]:
self._highest_z = max(item.wells()[0].top().z, self._highest_z)

def __setitem__(self, key: types.DeckLocation, val: Labware) -> 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
self._highest_z = max(val.wells()[0].top().z, self._highest_z)
self._highest_z = max(val.wells()[0].top().point.z, self._highest_z)

def __contains__(self, key: object) -> bool:
try:
Expand Down
78 changes: 52 additions & 26 deletions api/src/opentrons/protocol_api/labware.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
"""This module will replace Placeable"""
import re
import os
from collections import defaultdict
from enum import Enum, auto
import json
import os
import re
import time
from typing import List, Dict
from enum import Enum, auto
from opentrons.types import Point

from opentrons.types import Point, Location
from opentrons.util import environment as env
from collections import defaultdict


class WellShape(Enum):
Expand All @@ -25,7 +26,8 @@ class WellShape(Enum):

class Well:
def __init__(
self, well_props: dict, parent: Point, display_name: str) -> None:
self, well_props: dict, parent: Location, display_name: str)\
-> None:
"""
Create a well, and track the Point corresponding to the top-center of
the well (this Point is in absolute deck coordinates)
Expand All @@ -37,15 +39,19 @@ def __init__(
This is created by the caller and passed in, so here it is just
saved and made available.
:param well_props: a dict that conforms to the json-schema for a Well
:param parent: a Point representing the absolute position of the parent
of the Well (usually the lower-left corner of a labware)
:param parent: a :py:class:`.Location` Point representing the absolute
position of the parent of the Well (usually the
lower-left corner of a labware)
"""
self._display_name = display_name
self._position = Point(
x=well_props['x'] + parent.x,
y=well_props['y'] + parent.y,
z=well_props['z'] + well_props['depth'] + parent.z)

self._position\
= Point(well_props['x'],
well_props['y'],
well_props['z'] + well_props['depth']) + parent.point

if not parent.labware:
raise ValueError("Wells must have a parent")
self._parent = parent.labware
self._shape = well_shapes.get(well_props['shape'])
if self._shape is WellShape.RECTANGULAR:
self._length = well_props['length']
Expand All @@ -62,33 +68,37 @@ def __init__(

self._depth = well_props['depth']

def top(self) -> Point:
@property
def parent(self) -> 'Labware':
return self._parent

def top(self) -> Location:
"""
:return: a Point corresponding to the absolute position of the
top-center of the well relative to the deck (with the lower-left corner
of slot 1 as (0,0,0))
"""
return self._position
return Location(self._position, self)

def bottom(self) -> Point:
def bottom(self) -> Location:
"""
:return: a Point corresponding to the absolute position of the
bottom-center of the well (with the lower-left corner of slot 1 as
(0,0,0))
"""
top = self.top()
bottom_z = top.z - self._depth
return Point(x=top.x, y=top.y, z=bottom_z)
bottom_z = top.point.z - self._depth
return Location(Point(x=top.point.x, y=top.point.y, z=bottom_z), self)

def center(self) -> Point:
def center(self) -> Location:
"""
:return: a Point corresponding to the absolute position of the center
of the well relative to the deck (with the lower-left corner of slot 1
as (0,0,0))
"""
top = self.top()
center_z = top.z - (self._depth / 2.0)
return Point(x=top.x, y=top.y, z=center_z)
center_z = top.point.z - (self._depth / 2.0)
return Location(Point(x=top.point.x, y=top.point.y, z=center_z), self)

def _from_center_cartesian(
self, x: float, y: float, z: float) -> Point:
Expand Down Expand Up @@ -121,9 +131,9 @@ def _from_center_cartesian(
z_size = self._depth

return Point(
x=center.x + (x * (x_size / 2.0)),
y=center.y + (y * (y_size / 2.0)),
z=center.z + (z * (z_size / 2.0)))
x=center.point.x + (x * (x_size / 2.0)),
y=center.point.y + (y * (y_size / 2.0)),
z=center.point.z + (z * (z_size / 2.0)))

def __str__(self):
return self._display_name
Expand All @@ -135,7 +145,7 @@ def __eq__(self, other: object) -> bool:
"""
if not isinstance(other, Well):
return NotImplemented
return self.top() == other.top()
return self.top().point == other.top().point


class Labware:
Expand Down Expand Up @@ -170,6 +180,7 @@ def __init__(
self._id = definition['otId']
self._parameters = definition['parameters']
offset = definition['cornerOffsetFromSlot']
self._dimensions = definition['dimensions']
# Inferred from definition
self._ordering = [well
for col in definition['ordering']
Expand All @@ -179,6 +190,7 @@ def __init__(
z=offset['z'] + parent.z)
# Applied properties
self.set_calibration(self._calibrated_offset)

self._pattern = re.compile(r'^([A-Z]+)([1-9][0-9]*)$', re.X)

def _build_wells(self) -> List[Well]:
Expand All @@ -190,7 +202,7 @@ def _build_wells(self) -> List[Well]:
return [
Well(
self._well_definition[well],
self._calibrated_offset,
Location(self._calibrated_offset, self),
"{} of {}".format(well, self._display_name))
for well in self._ordering]

Expand All @@ -217,6 +229,10 @@ def set_calibration(self, delta: Point):
z=self._offset.z + delta.z)
self._wells = self._build_wells()

@property
def calibrated_offset(self) -> Point:
return self._calibrated_offset

def well(self, idx) -> Well:
"""Deprecated---use result of `wells` or `wells_by_index`"""
if isinstance(idx, int):
Expand Down Expand Up @@ -357,6 +373,16 @@ def cols(self, *args):
"""Deprecated--use `columns`"""
return self.columns(*args)

@property
def highest_z(self) -> float:
"""
The z-coordinate of the tallest single point anywhere on the labware.
This is drawn from the 'dimensions'/'overallHeight' elements of the
labware definition and takes into account the calibration offset.
"""
return self._dimensions['overallHeight'] + self._calibrated_offset.z

def __repr__(self):
return self._display_name

Expand Down
Loading

0 comments on commit 2d97353

Please sign in to comment.