Skip to content

Commit

Permalink
feat(api): Add critical point tracking to hardware control
Browse files Browse the repository at this point in the history
The hardware control API now tracks whether a pipette has a tip or not and
considers its "critical point", the point moved in a move command, to be either
the end of the tip, the end of the nozzle, or the mount depending on the status
of the pipette.

Closes #2239
  • Loading branch information
sfoster1 committed Oct 22, 2018
1 parent db60922 commit 88716e7
Show file tree
Hide file tree
Showing 5 changed files with 151 additions and 47 deletions.
44 changes: 35 additions & 9 deletions api/src/opentrons/hardware_control/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -277,10 +277,11 @@ def current_position(self, mount: top_types.Mount) -> Dict[Axis, float]:
offset = top_types.Point(*self.config.mount_offset)
z_ax = Axis.by_mount(mount)
plunger_ax = Axis.of_plunger(mount)
cp = self._critical_point_for(mount)
return {
Axis.X: self._current_position[Axis.X] + offset[0],
Axis.Y: self._current_position[Axis.Y] + offset[1],
z_ax: self._current_position[z_ax] + offset[2],
Axis.X: self._current_position[Axis.X] + offset[0] + cp.x,
Axis.Y: self._current_position[Axis.Y] + offset[1] + cp.y,
z_ax: self._current_position[z_ax] + offset[2] + cp.z,
plunger_ax: self._current_position[plunger_ax]
}

Expand Down Expand Up @@ -309,10 +310,11 @@ async def move_to(
offset = top_types.Point(*self.config.mount_offset)
else:
offset = top_types.Point(0, 0, 0)
cp = self._critical_point_for(mount)
target_position = OrderedDict(
((Axis.X, abs_position.x - offset.x),
(Axis.Y, abs_position.y - offset.y),
(z_axis, abs_position.z - offset.z))
((Axis.X, abs_position.x - offset.x - cp.x),
(Axis.Y, abs_position.y - offset.y - cp.y),
(z_axis, abs_position.z - offset.z - cp.z))
)
await self._move(target_position)

Expand Down Expand Up @@ -392,6 +394,24 @@ async def _move(self, target_position: 'OrderedDict[Axis, float]'):
else:
self._current_position.update(target_position)

def _critical_point_for(self, mount: top_types.Mount) -> top_types.Point:
""" Return the current critical point of the specified mount.
The mount's critical point is the position of the mount itself, if no
pipette is attached, or the pipette's critical point (which depends on
tip status).
"""
pip = self._attached_instruments[mount]
if pip is not None:
return pip.critical_point
else:
# TODO: The smoothie’s z/a home position is calculated to provide
# the offset for a P300 single. Here we should decide whether we
# implicitly accept this as correct (by returning a null offset)
# or not (by returning an offset calculated to move back up the
# length of the P300 single).
return top_types.Point(0, 0, 0)

# Gantry/frame (i.e. not pipette) config API
@property
def config(self) -> robot_configs.robot_config:
Expand Down Expand Up @@ -535,12 +555,18 @@ async def air_gap(self, mount, volume=None):
pass

@_log_call
async def pick_up_tip(self, mount, tip_length):
pass
async def pick_up_tip(self, mount):
instr = self._attached_instruments[mount]
assert instr
# TODO: Move commands to pick up tip(s)
instr.add_tip()

@_log_call
async def drop_tip(self, mount):
pass
instr = self._attached_instruments[mount]
assert instr
# TODO: Move commands to drop tip(s)
instr.remove_tip()

# Pipette config api
@_log_call
Expand Down
28 changes: 18 additions & 10 deletions api/src/opentrons/hardware_control/pipette.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
""" Classes and functions for pipette state tracking
"""
from typing import Any, Dict, Optional, Union
from typing import Any, Dict, Union

from opentrons.types import Point
from opentrons.config import pipette_config
Expand All @@ -16,8 +16,8 @@ class Pipette:
def __init__(self, model: str) -> None:
self._config = pipette_config.load(model)
self._name = model
self._tip_length: Optional[float] = None
self._current_volume = 0.0
self._has_tip = False

@property
def config(self) -> pipette_config.pipette_config:
Expand All @@ -33,7 +33,12 @@ def name(self) -> str:
@property
def critical_point(self) -> Point:
""" The vector from the pipette's origin to its critical point """
pass
if not self.has_tip:
return Point(*self.config.model_offset)
else:
return Point(self.config.model_offset[0],
self.config.model_offset[1],
self.config.model_offset[2] - self.config.tip_length)

@property
def current_volume(self) -> float:
Expand Down Expand Up @@ -61,25 +66,27 @@ def remove_current_volume(self, volume_incr: float):
def ok_to_add_volume(self, volume_incr: float) -> bool:
return self.current_volume + volume_incr <= self.config.max_volume

def add_tip(self, length: float):
self._tip_length = length
def add_tip(self):
assert not self.has_tip
self._has_tip = True

def remove_tip(self):
self._tip_length = None
assert self.has_tip
self._has_tip = False

@property
def has_tip(self) -> bool:
return self._tip_length is not None
return self._has_tip

def ul_per_mm(self, ul: float, action: str) -> float:
sequence = self._config.ul_per_mm[action]
return pipette_config.piecewise_volume_conversion(ul, sequence)

def __str__(self) -> str:
return '{} current volume {}ul critical point {} at {}'\
return '{} current volume {}ul critical point: {} at {}'\
.format(self._config.display_name,
self.current_volume,
'<unknown>',
'tip end' if self.has_tip else 'nozzle end',
0)

def __repr__(self) -> str:
Expand All @@ -90,5 +97,6 @@ def __repr__(self) -> str:
def as_dict(self) -> Dict[str, Union[str, float]]:
config_dict = self.config._asdict()
config_dict.update({'current_volume': self.current_volume,
'name': self.name})
'name': self.name,
'has_tip': self.has_tip})
return config_dict
28 changes: 0 additions & 28 deletions api/tests/opentrons/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,34 +23,6 @@
from opentrons.util import environment
from opentrons import hardware_control as hc

# Uncomment to enable logging during tests

# logging_config = dict(
# version=1,
# formatters={
# 'basic': {
# 'format':
# '[Line %(lineno)s] %(message)s'
# }
# },
# handlers={
# 'debug': {
# 'class': 'logging.StreamHandler',
# 'formatter': 'basic',
# }
# },
# loggers={
# '__main__': {
# 'handlers': ['debug'],
# 'level': logging.DEBUG
# },
# 'opentrons.server': {
# 'handlers': ['debug'],
# 'level': logging.DEBUG
# },
# }
# )
# dictConfig(logging_config)

Session = namedtuple(
'Session',
Expand Down
42 changes: 42 additions & 0 deletions api/tests/opentrons/hardware_control/test_moves.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,48 @@ async def test_mount_offset_applied(hardware_api):
assert hardware_api._current_position == target_position


async def test_critical_point_applied(hardware_api, monkeypatch):
await hardware_api.home()
hardware_api._backend._attached_instruments\
= {types.Mount.LEFT: None,
types.Mount.RIGHT: 'p10_single_v1'}
await hardware_api.cache_instruments()
# Our critical point is now the tip of the nozzle
await hardware_api.move_to(types.Mount.RIGHT, types.Point(0, 0, 0))
target_no_offset = {Axis.X: 0,
Axis.Y: 0,
Axis.Z: 218,
Axis.A: 13, # from pipette-config.json model offset
Axis.B: 19,
Axis.C: 19}
assert hardware_api._current_position == target_no_offset
target = {Axis.X: 0,
Axis.Y: 0,
Axis.A: 0,
Axis.C: 19}
assert hardware_api.current_position(types.Mount.RIGHT) == target
await hardware_api.pick_up_tip(types.Mount.RIGHT)
# Now the current position (with offset applied) should change
target[Axis.A] = -33
assert hardware_api.current_position(types.Mount.RIGHT) == target
# This move should take the new critical point into account
await hardware_api.move_to(types.Mount.RIGHT, types.Point(0, 0, 0))
target_no_offset[Axis.A] = 46
assert hardware_api._current_position == target_no_offset
# But the position with offset should be back to the original
target[Axis.A] = 0
assert hardware_api.current_position(types.Mount.RIGHT) == target
# And removing the tip should move us back to the original
await hardware_api.drop_tip(types.Mount.RIGHT)
target[Axis.A] = 33
assert hardware_api.current_position(types.Mount.RIGHT) == target
await hardware_api.move_to(types.Mount.RIGHT, types.Point(0, 0, 0))
target_no_offset[Axis.A] = 13
target[Axis.A] = 0
assert hardware_api._current_position == target_no_offset
assert hardware_api.current_position(types.Mount.RIGHT) == target


async def test_deck_cal_applied(monkeypatch, loop):
new_gantry_cal = [[1, 0, 0, 10],
[0, 1, 0, 20],
Expand Down
56 changes: 56 additions & 0 deletions api/tests/opentrons/hardware_control/test_pipette.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import pytest
from opentrons.types import Point
from opentrons.hardware_control import pipette
from opentrons.config import pipette_config


def test_tip_tracking():
pip = pipette.Pipette('p10_single_v1')
with pytest.raises(AssertionError):
pip.remove_tip()
assert not pip.has_tip
pip.add_tip()
assert pip.has_tip
with pytest.raises(AssertionError):
pip.add_tip()
pip.remove_tip()
assert not pip.has_tip
with pytest.raises(AssertionError):
pip.remove_tip()


def test_critical_points():
for config in pipette_config.configs:
loaded = pipette_config.load(config)
pip = pipette.Pipette(config)
mod_offset = Point(*loaded.model_offset)
assert pip.critical_point == mod_offset
pip.add_tip()
new = mod_offset._replace(z=mod_offset.z - loaded.tip_length)
assert pip.critical_point == new
pip.remove_tip()
assert pip.critical_point == mod_offset


def test_volume_tracking():
for config in pipette_config.configs:
loaded = pipette_config.load(config)
pip = pipette.Pipette(config)
assert pip.current_volume == 0.0
assert pip.available_volume == loaded.max_volume
assert pip.ok_to_add_volume(loaded.max_volume - 0.1)
pip.set_current_volume(0.1)
with pytest.raises(AssertionError):
pip.set_current_volume(loaded.max_volume + 0.1)
with pytest.raises(AssertionError):
pip.set_current_volume(-1)
assert pip.current_volume == 0.1
pip.remove_current_volume(0.1)
with pytest.raises(AssertionError):
pip.remove_current_volume(0.1)
assert pip.current_volume == 0.0
pip.set_current_volume(loaded.max_volume)
assert not pip.ok_to_add_volume(0.1)
with pytest.raises(AssertionError):
pip.add_current_volume(0.1)
assert pip.current_volume == loaded.max_volume

0 comments on commit 88716e7

Please sign in to comment.