Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(api): tiplength from tiprack #2637

Merged
merged 6 commits into from
Nov 7, 2018
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions api/src/opentrons/hardware_control/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ async def cache_instruments(self,

@property
def attached_instruments(self):
configs = ['name', 'min_volume', 'max_volume',
configs = ['name', 'min_volume', 'max_volume', 'channels',
'aspirate_flow_rate', 'dispense_flow_rate',
'pipette_id', 'current_volume', 'display_name']
instruments = {top_types.Mount.LEFT: {},
Expand Down Expand Up @@ -671,7 +671,11 @@ async def blow_out(self, mount):
this_pipette.set_current_volume(0)

@_log_call
async def pick_up_tip(self, mount, presses: int = 3, increment: float = 1):
async def pick_up_tip(self,
mount,
tip_length: float,
presses: int = 3,
increment: float = 1):
"""
Pick up tip from current location
"""
Expand Down Expand Up @@ -700,7 +704,7 @@ async def pick_up_tip(self, mount, presses: int = 3, increment: float = 1):
# move nozzle back up
backup_pos = top_types.Point(0, 0, -dist)
await self.move_rel(mount, backup_pos)
instr.add_tip()
instr.add_tip(tip_length=tip_length)
instr.set_current_volume(0)

# neighboring tips tend to get stuck in the space between
Expand Down
45 changes: 32 additions & 13 deletions api/src/opentrons/hardware_control/pipette.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ def __init__(self, model: str, pipette_id: str = None) -> None:
self._config = pipette_config.load(model)
self._name = model
self._current_volume = 0.0
self._has_tip = False
self._current_tip_length = 0.0
self._pipette_id = pipette_id

@property
Expand All @@ -37,19 +37,25 @@ def pipette_id(self) -> Optional[str]:

@property
def critical_point(self) -> Point:
""" The vector from the pipette's origin to its critical point """
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)
"""
The vector from the pipette's origin to its critical point. The
critical point for a pipette is the end of the nozzle if no tip is
attached, or the end of the tip if a tip is attached.
"""
return Point(self.config.model_offset[0],
self.config.model_offset[1],
self.config.model_offset[2] - self.current_tip_length)

@property
def current_volume(self) -> float:
""" The amount of liquid currently aspirated """
return self._current_volume

@property
def current_tip_length(self) -> float:
""" The length of the current tip attached (0.0 if no tip) """
return self._current_tip_length

@property
def available_volume(self) -> float:
""" The amount of liquid possible to aspirate """
Expand All @@ -71,17 +77,30 @@ 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):
def add_tip(self, tip_length) -> None:
"""
Add a tip to the pipette for position tracking and validation
(effectively updates the pipette's critical point)

:param tip_length: a positive, non-zero float representing the distance
in Z from the end of the pipette nozzle to the end of the tip
:return:
"""
assert tip_length > 0.0, "tip_length must be greater than 0"
assert not self.has_tip
self._has_tip = True
self._current_tip_length = tip_length

def remove_tip(self):
def remove_tip(self) -> None:
"""
Remove the tip from the pipette (effectively updates the pipette's
critical point)
"""
assert self.has_tip
self._has_tip = False
self._current_tip_length = 0.0

@property
def has_tip(self) -> bool:
return self._has_tip
return self.current_tip_length != 0.0

def ul_per_mm(self, ul: float, action: str) -> float:
sequence = self._config.ul_per_mm[action]
Expand Down
141 changes: 121 additions & 20 deletions api/src/opentrons/protocol_api/contexts.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@
MODULE_LOG = logging.getLogger(__name__)


class OutOfTipsError(Exception):
pass


class ProtocolContext:
""" The Context class is a container for the state of a protocol.

Expand Down Expand Up @@ -100,6 +104,7 @@ def load_instrument(
self,
instrument_name: str,
mount: types.Mount,
tip_racks: List[Labware] = None,
sfoster1 marked this conversation as resolved.
Show resolved Hide resolved
replace: bool = False) -> 'InstrumentContext':
""" Load a specific instrument required by the protocol.

Expand Down Expand Up @@ -133,7 +138,11 @@ def load_instrument(
self._hardware.cache_instruments(attached)
# If the cache call didn’t raise, the instrument is attached
new_instr = InstrumentContext(
self, self._hardware, mount, [], self._log)
ctx=self,
hardware=self._hardware,
mount=mount,
tip_racks=tip_racks,
log_parent=self._log)
self._instruments[mount] = new_instr
self._log.info("Instrument {} loaded".format(new_instr))
return new_instr
Expand Down Expand Up @@ -246,12 +255,18 @@ class InstrumentContext:
def __init__(self,
ctx: ProtocolContext,
hardware: adapters.SynchronousAdapter,
mount: types.Mount, tip_racks,
mount: types.Mount,
tip_racks: Optional[List[Labware]],
btmorr marked this conversation as resolved.
Show resolved Hide resolved
log_parent: logging.Logger,
**config_kwargs) -> None:
self._hardware = hardware
self._ctx = ctx
self._mount = mount

self._tip_racks = tip_racks or list()
for tip_rack in self.tip_racks:
assert tip_rack.is_tiprack

self._last_location: Union[Labware, Well, None] = None
self._log = log_parent.getChild(repr(self))
self._log.info("attached")
Expand All @@ -260,7 +275,7 @@ def __init__(self,
def aspirate(self,
volume: float = None,
location: Union[types.Location, Well] = None,
rate: float = 1.0):
rate: float = 1.0) -> 'InstrumentContext':
"""
Aspirate a volume of liquid (in microliters/uL) using this pipette
from the specified location
Expand Down Expand Up @@ -310,7 +325,7 @@ def aspirate(self,
def dispense(self,
volume: float = None,
location: Union[types.Location, Well] = None,
rate: float = 1.0):
rate: float = 1.0) -> 'InstrumentContext':
"""
Dispense a volume of liquid (in microliters/uL) using this pipette
into the specified location.
Expand Down Expand Up @@ -362,37 +377,123 @@ def mix(self,
repetitions: int = 1,
volume: float = None,
location: Well = None,
rate: float = 1.0):
rate: float = 1.0) -> 'InstrumentContext':
raise NotImplementedError

def blow_out(self, location: Well = None):
def blow_out(self, location: Well = None) -> 'InstrumentContext':
raise NotImplementedError

def touch_tip(self,
location: Well = None,
radius: float = 1.0,
v_offset: float = -1.0,
speed: float = 60.0):
speed: float = 60.0) -> 'InstrumentContext':
raise NotImplementedError

def air_gap(self,
volume: float = None,
height: float = None):
height: float = None) -> 'InstrumentContext':
raise NotImplementedError

def return_tip(self, home_after: bool = True):
def return_tip(self, home_after: bool = True) -> 'InstrumentContext':
raise NotImplementedError

def pick_up_tip(self, location: Well = None,
btmorr marked this conversation as resolved.
Show resolved Hide resolved
btmorr marked this conversation as resolved.
Show resolved Hide resolved
presses: int = 3,
increment: int = 1):
raise NotImplementedError
increment: int = 1) -> 'InstrumentContext':
"""
btmorr marked this conversation as resolved.
Show resolved Hide resolved
Pick up a tip for the Pipette to run liquid-handling commands with

Notes
-----
A tip can be manually set by passing a `location`. If no location
is passed, the Pipette will pick up the next available tip in
it's `tip_racks` list (see :any:`Pipette`)
btmorr marked this conversation as resolved.
Show resolved Hide resolved

Parameters
----------
:param location: The `Well` to perform the pick_up_tip.
btmorr marked this conversation as resolved.
Show resolved Hide resolved
:type location: `Labware`, `Well`, or None
btmorr marked this conversation as resolved.
Show resolved Hide resolved

:param presses: The number of times to lower and then raise the pipette
when picking up a tip, to ensure a good seal (0 [zero]
will result in the pipette hovering over the tip but
not picking it up--generally not desireable, but could
be used for dry-run)
:type presses: int

:param increment: The additional distance to travel on each successive
press (e.g.: if presses=3 and increment=1, then the
first press will travel down into the tip by 3.5mm,
the second by 4.5mm, and the third by 5.5mm
:type increment: int or float

:returns: This instance
"""
num_channels = \
self._hardware.attached_instruments[self._mount]['channels']

def _select_tiprack_from_list(tip_racks) -> Labware:
btmorr marked this conversation as resolved.
Show resolved Hide resolved
try:
tr = tip_racks[0]
except IndexError:
raise OutOfTipsError
next_tip = tr.next_tip(num_channels)
if next_tip:
return tr
btmorr marked this conversation as resolved.
Show resolved Hide resolved
else:
return _select_tiprack_from_list(tip_racks[1:])

if isinstance(location, Labware):
tiprack = location
target: Optional[Well] = tiprack.next_tip(num_channels)
elif isinstance(location, Well):
tiprack = location.parent
target = location
else:
tiprack = _select_tiprack_from_list(self.tip_racks)
target = tiprack.next_tip(num_channels)
if target is None:
# This is primarily for type checking--should raise earlier
raise OutOfTipsError

assert tiprack.is_tiprack

self.move_to(target.top())

self._hardware.pick_up_tip(
self._mount, tiprack.tip_length, presses, increment)
# Note that the hardware API pick_up_tip action includes homing z after

tiprack.use_tips(target, num_channels)

return self

def drop_tip(self, location: Well = None,
home_after: bool = True):
raise NotImplementedError
home_after: bool = True) -> 'InstrumentContext':
"""
Drop the current tip. If a location is specified, drop the tip there,
otherwise drop it into the fixed trash.

def home(self):
:param location: The location to drop the tip
:type location: `Well` or None
btmorr marked this conversation as resolved.
Show resolved Hide resolved

:param home_after: if True, home the robot after dropping tip
btmorr marked this conversation as resolved.
Show resolved Hide resolved
:param home_after: bool
btmorr marked this conversation as resolved.
Show resolved Hide resolved

:returns: This instance
"""
if location:
self.move_to(location.top())
else:
self.move_to(self.trash_container.wells()[0].top())
self._hardware.drop_tip(self.mount)
if home_after:
self.home()
btmorr marked this conversation as resolved.
Show resolved Hide resolved

return self

def home(self) -> 'InstrumentContext':
""" Home the robot.

:returns: This instance.
Expand All @@ -404,24 +505,24 @@ def distribute(self,
volume: float,
source: Well,
dest: Well,
*args, **kwargs):
*args, **kwargs) -> 'InstrumentContext':
raise NotImplementedError

def consolidate(self,
volume: float,
source: Well,
dest: Well,
*args, **kwargs):
*args, **kwargs) -> 'InstrumentContext':
raise NotImplementedError

def transfer(self,
volume: float,
source: Well,
dest: Well,
**kwargs):
**kwargs) -> 'InstrumentContext':
raise NotImplementedError

def move_to(self, location: types.Location):
def move_to(self, location: types.Location) -> 'InstrumentContext':
""" Move the instrument.

:param location: The location to move to.
Expand Down Expand Up @@ -521,11 +622,11 @@ def type(self) -> str:
@property
def tip_racks(self) -> List[Labware]:
""" Query which tipracks have been linked to this PipetteContext"""
btmorr marked this conversation as resolved.
Show resolved Hide resolved
raise NotImplementedError
return self._tip_racks

@tip_racks.setter
def tip_racks(self, racks: List[Labware]):
raise NotImplementedError
self._tip_racks = racks

@property
def trash_container(self) -> Labware:
Expand Down
Loading