Skip to content
Merged
42 changes: 41 additions & 1 deletion api/src/opentrons/protocol_api/instrument_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,16 @@
AdvancedLiquidHandling = v1_transfer.AdvancedLiquidHandling


class _Unset:
"""A sentinel value when no value has been supplied for an argument.
User code should never use this explicitly."""

def __repr__(self) -> str:
# Without this, the generated docs render the argument as
# "<opentrons.protocol_api.instrument_context._Unset object at 0x1234>"
return self.__class__.__name__


class InstrumentContext(publisher.CommandPublisher):
"""
A context for a specific pipette or instrument.
Expand Down Expand Up @@ -644,12 +654,13 @@ def _determine_speed(self, speed: float) -> float:

@publisher.publish(command=cmds.touch_tip)
@requires_version(2, 0)
def touch_tip(
def touch_tip( # noqa: C901
self,
location: Optional[labware.Well] = None,
radius: float = 1.0,
v_offset: float = -1.0,
speed: float = 60.0,
mm_from_edge: Union[float, _Unset] = _Unset(),
) -> InstrumentContext:
"""
Touch the pipette tip to the sides of a well, with the intent of removing leftover droplets.
Expand All @@ -675,12 +686,28 @@ def touch_tip(
- Maximum: 80.0 mm/s
- Minimum: 1.0 mm/s
:type speed: float
:param mm_from_edge: How far to move inside the well, as a distance from the
well's edge.
When ``mm_from_edge=0``, the pipette tip will move all the
way to the edge of the target well. When ``mm_from_edge=1``,
the pipette tip will move to 1 mm from the well's edge.
Lower values will press the tip harder into the well's
walls; higher values will touch the well more lightly, or
not at all.
``mm_from_edge`` and ``radius`` are mutually exclusive: to
use ``mm_from_edge``, ``radius`` must be unspecified (left
to its default value of 1.0).
Comment on lines +689 to +699
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very nice documentation! No notes. Unless you want your single line breaks to be paragraph breaks, in which case you need to make them double line breaks.

We should also add this to the bottom of the docstring:

.. versionchanged:: 2.24
    Added the ``mm_from_edge`` parameter.

(I presume this is 2.24, since this PR is targeted on edge and 2.23 changes are going into chore_release-8.4.0)

:type mm_from_edge: float
:raises: ``UnexpectedTipRemovalError`` -- If no tip is attached to the pipette.
:raises RuntimeError: If no location is specified and the location cache is
``None``. This should happen if ``touch_tip`` is called
without first calling a method that takes a location, like
:py:meth:`.aspirate` or :py:meth:`dispense`.
:raises: ValueError: If both ``mm_to_edge`` and ``radius`` are specified.
:returns: This instance.

.. versionchanged:: 2.24
Added the ``mm_from_edge`` parameter.
"""
if not self._core.has_tip():
raise UnexpectedTipRemovalError("touch_tip", self.name, self.mount)
Expand All @@ -703,6 +730,18 @@ def touch_tip(
else:
raise TypeError(f"location should be a Well, but it is {location}")

if not isinstance(mm_from_edge, _Unset):
if self.api_version < APIVersion(2, 24):
raise APIVersionError(
api_element="mm_from_edge",
until_version="2.24",
current_version=f"{self.api_version}",
)
if radius != 1.0:
raise ValueError(
"radius must be set to 1.0 if mm_from_edge is specified"
)

if "touchTipDisabled" in parent_labware.quirks:
_log.info(f"Ignoring touch tip on labware {well}")
return self
Expand All @@ -722,6 +761,7 @@ def touch_tip(
radius=radius,
z_offset=v_offset,
speed=checked_speed,
mm_from_edge=mm_from_edge if not isinstance(mm_from_edge, _Unset) else None,
)
return self

Expand Down
2 changes: 1 addition & 1 deletion api/src/opentrons/protocols/api_support/definitions.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from .types import APIVersion

MAX_SUPPORTED_VERSION = APIVersion(2, 23)
MAX_SUPPORTED_VERSION = APIVersion(2, 24)
"""The maximum supported protocol API version in this release."""

MIN_SUPPORTED_VERSION = APIVersion(2, 0)
Expand Down
24 changes: 24 additions & 0 deletions api/tests/opentrons/protocol_api/test_instrument_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -1043,6 +1043,8 @@ def test_touch_tip(

decoy.when(mock_well.parent.quirks).then_return([])

# touch_tip() with the old `radius` argument:

subject.touch_tip(mock_well, radius=0.123, v_offset=4.56, speed=42.0)

decoy.verify(
Expand All @@ -1052,9 +1054,31 @@ def test_touch_tip(
radius=0.123,
z_offset=4.56,
speed=42.0,
mm_from_edge=None,
)
)

# touch_tip() with the new `mm_from_edge` argument:

subject.touch_tip(mock_well, v_offset=4.56, speed=42.0, mm_from_edge=0.5)

decoy.verify(
mock_instrument_core.touch_tip(
location=Location(point=Point(1, 2, 3), labware=mock_well),
well_core=mock_well._core,
radius=1,
z_offset=4.56,
speed=42.0,
mm_from_edge=0.5,
)
)

# `radius` and `mm_from_edge` are mutually exclusive, should raise if both specified:
with pytest.raises(ValueError):
subject.touch_tip(
mock_well, radius=0.75, v_offset=4.56, speed=42.0, mm_from_edge=0.5
)


def test_return_height(
decoy: Decoy, mock_instrument_core: InstrumentCore, subject: InstrumentContext
Expand Down
Loading