Skip to content

Commit

Permalink
feat(engine): add mmFromEdge parameter to touchTip (#17107)
Browse files Browse the repository at this point in the history
Adds a new optional parameter `mmFromEdge` to the protocol engine `touchTip` command, primarily for usage for transfering liquids defined by a liquid class.
  • Loading branch information
jbleon95 authored Dec 16, 2024
1 parent 0540c01 commit f22ed41
Show file tree
Hide file tree
Showing 9 changed files with 180 additions and 20 deletions.
19 changes: 18 additions & 1 deletion api/src/opentrons/protocol_engine/commands/touch_tip.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@

from opentrons.types import Point

from ..errors import TouchTipDisabledError, LabwareIsTipRackError
from ..errors import (
TouchTipDisabledError,
TouchTipIncompatibleArgumentsError,
LabwareIsTipRackError,
)
from ..types import DeckPoint
from .command import (
AbstractCommandImpl,
Expand Down Expand Up @@ -45,6 +49,12 @@ class TouchTipParams(PipetteIdMixin, WellLocationMixin):
),
)

mmFromEdge: Optional[float] = Field(
None,
description="Offset away from the the well edge, in millimeters."
"Incompatible when a radius is included as a non 1.0 value.",
)

speed: Optional[float] = Field(
None,
description=(
Expand Down Expand Up @@ -89,6 +99,11 @@ async def execute(
labware_id = params.labwareId
well_name = params.wellName

if params.radius != 1.0 and params.mmFromEdge is not None:
raise TouchTipIncompatibleArgumentsError(
"Cannot use mmFromEdge with a radius that is not 1.0"
)

if self._state_view.labware.get_has_quirk(labware_id, "touchTipDisabled"):
raise TouchTipDisabledError(
f"Touch tip not allowed on labware {labware_id}"
Expand All @@ -112,11 +127,13 @@ async def execute(
pipette_id, params.speed
)

mm_from_edge = params.mmFromEdge if params.mmFromEdge is not None else 0
touch_waypoints = self._state_view.motion.get_touch_tip_waypoints(
pipette_id=pipette_id,
labware_id=labware_id,
well_name=well_name,
radius=params.radius,
mm_from_edge=mm_from_edge,
center_point=Point(
center_result.public.position.x,
center_result.public.position.y,
Expand Down
2 changes: 2 additions & 0 deletions api/src/opentrons/protocol_engine/errors/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
LabwareIsTipRackError,
LabwareIsAdapterError,
TouchTipDisabledError,
TouchTipIncompatibleArgumentsError,
WellDoesNotExistError,
PipetteNotLoadedError,
ModuleNotLoadedError,
Expand Down Expand Up @@ -110,6 +111,7 @@
"LabwareIsTipRackError",
"LabwareIsAdapterError",
"TouchTipDisabledError",
"TouchTipIncompatibleArgumentsError",
"WellDoesNotExistError",
"PipetteNotLoadedError",
"ModuleNotLoadedError",
Expand Down
13 changes: 13 additions & 0 deletions api/src/opentrons/protocol_engine/errors/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,19 @@ def __init__(
super().__init__(ErrorCodes.GENERAL_ERROR, message, details, wrapping)


class TouchTipIncompatibleArgumentsError(ProtocolEngineError):
"""Raised when touch tip is used with both a custom radius and a mmFromEdge argument."""

def __init__(
self,
message: Optional[str] = None,
details: Optional[Dict[str, Any]] = None,
wrapping: Optional[Sequence[EnumeratedError]] = None,
) -> None:
"""Build a TouchTipIncompatibleArgumentsError."""
super().__init__(ErrorCodes.GENERAL_ERROR, message, details, wrapping)


class WellDoesNotExistError(ProtocolEngineError):
"""Raised when referencing a well that does not exist."""

Expand Down
14 changes: 9 additions & 5 deletions api/src/opentrons/protocol_engine/state/_move_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,15 +53,19 @@ def get_move_type_to_well(


def get_edge_point_list(
center: Point, x_radius: float, y_radius: float, edge_path_type: EdgePathType
center: Point,
x_radius: float,
y_radius: float,
mm_from_edge: float,
edge_path_type: EdgePathType,
) -> List[Point]:
"""Get list of edge points dependent on edge path type."""
edges = EdgeList(
right=center + Point(x=x_radius, y=0, z=0),
left=center + Point(x=-x_radius, y=0, z=0),
right=center + Point(x=x_radius - mm_from_edge, y=0, z=0),
left=center + Point(x=-x_radius + mm_from_edge, y=0, z=0),
center=center,
forward=center + Point(x=0, y=y_radius, z=0),
back=center + Point(x=0, y=-y_radius, z=0),
forward=center + Point(x=0, y=y_radius - mm_from_edge, z=0),
back=center + Point(x=0, y=-y_radius + mm_from_edge, z=0),
)

if edge_path_type == EdgePathType.LEFT:
Expand Down
7 changes: 6 additions & 1 deletion api/src/opentrons/protocol_engine/state/motion.py
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,7 @@ def get_touch_tip_waypoints(
labware_id: str,
well_name: str,
center_point: Point,
mm_from_edge: float = 0,
radius: float = 1.0,
) -> List[motion_planning.Waypoint]:
"""Get a list of touch points for a touch tip operation."""
Expand All @@ -346,7 +347,11 @@ def get_touch_tip_waypoints(
)

positions = _move_types.get_edge_point_list(
center_point, x_offset, y_offset, edge_path_type
center=center_point,
x_radius=x_offset,
y_radius=y_offset,
mm_from_edge=mm_from_edge,
edge_path_type=edge_path_type,
)
critical_point: Optional[CriticalPoint] = None

Expand Down
110 changes: 109 additions & 1 deletion api/tests/opentrons/protocol_engine/commands/test_touch_tip.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,99 @@ async def test_touch_tip_implementation(
pipette_id="abc",
labware_id="123",
well_name="A3",
center_point=Point(x=1, y=2, z=3),
radius=0.456,
mm_from_edge=0,
center_point=Point(x=1, y=2, z=3),
)
).then_return(
[
Waypoint(
position=Point(x=11, y=22, z=33),
critical_point=CriticalPoint.XY_CENTER,
),
Waypoint(
position=Point(x=44, y=55, z=66),
critical_point=CriticalPoint.XY_CENTER,
),
]
)

decoy.when(
await mock_gantry_mover.move_to(
pipette_id="abc",
waypoints=[
Waypoint(
position=Point(x=11, y=22, z=33),
critical_point=CriticalPoint.XY_CENTER,
),
Waypoint(
position=Point(x=44, y=55, z=66),
critical_point=CriticalPoint.XY_CENTER,
),
],
speed=9001,
)
).then_return(Point(x=4, y=5, z=6))

result = await subject.execute(params)

assert result == SuccessData(
public=TouchTipResult(position=DeckPoint(x=4, y=5, z=6)),
state_update=update_types.StateUpdate(
pipette_location=update_types.PipetteLocationUpdate(
pipette_id="abc",
new_location=update_types.Well(labware_id="123", well_name="A3"),
new_deck_point=DeckPoint(x=4, y=5, z=6),
)
),
)


async def test_touch_tip_implementation_with_mm_to_edge(
decoy: Decoy,
mock_state_view: StateView,
mock_movement_handler: MovementHandler,
mock_gantry_mover: GantryMover,
subject: TouchTipImplementation,
) -> None:
"""A TouchTip command should use mmFromEdge if provided."""
params = TouchTipParams(
pipetteId="abc",
labwareId="123",
wellName="A3",
wellLocation=WellLocation(offset=WellOffset(x=1, y=2, z=3)),
mmFromEdge=0.789,
speed=42.0,
)

decoy.when(
await mock_movement_handler.move_to_well(
pipette_id="abc",
labware_id="123",
well_name="A3",
well_location=WellLocation(offset=WellOffset(x=1, y=2, z=3)),
current_well=None,
force_direct=False,
minimum_z_height=None,
speed=None,
operation_volume=None,
)
).then_return(Point(x=1, y=2, z=3))

decoy.when(
mock_state_view.pipettes.get_movement_speed(
pipette_id="abc", requested_speed=42.0
)
).then_return(9001)

decoy.when(
mock_state_view.motion.get_touch_tip_waypoints(
pipette_id="abc",
labware_id="123",
well_name="A3",
radius=1.0,
mm_from_edge=0.789,
center_point=Point(x=1, y=2, z=3),
)
).then_return(
[
Expand Down Expand Up @@ -183,3 +274,20 @@ async def test_touch_tip_no_tip_racks(

with pytest.raises(errors.LabwareIsTipRackError):
await subject.execute(params)


async def test_touch_tip_incompatible_arguments(
decoy: Decoy, mock_state_view: StateView, subject: TouchTipImplementation
) -> None:
"""It should disallow touch tip if radius and mmFromEdge is provided."""
params = TouchTipParams(
pipetteId="abc",
labwareId="123",
wellName="A3",
wellLocation=WellLocation(),
radius=1.23,
mmFromEdge=4.56,
)

with pytest.raises(errors.TouchTipIncompatibleArgumentsError):
await subject.execute(params)
2 changes: 2 additions & 0 deletions api/tests/opentrons/protocol_engine/state/test_motion_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -928,6 +928,7 @@ def test_get_touch_tip_waypoints(
x_radius=1.2,
y_radius=3.4,
edge_path_type=_move_types.EdgePathType.RIGHT,
mm_from_edge=0.456,
)
).then_return([Point(x=11, y=22, z=33), Point(x=44, y=55, z=66)])

Expand All @@ -937,6 +938,7 @@ def test_get_touch_tip_waypoints(
well_name="B2",
center_point=center_point,
radius=0.123,
mm_from_edge=0.456,
)

assert result == [
Expand Down
28 changes: 16 additions & 12 deletions api/tests/opentrons/protocol_engine/state/test_move_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,43 +53,47 @@ def test_get_move_type_to_well(
(
subject.EdgePathType.LEFT,
[
Point(5, 20, 30),
Point(8, 20, 30),
Point(10, 20, 30),
Point(10, 30, 30),
Point(10, 10, 30),
Point(10, 27, 30),
Point(10, 13, 30),
Point(10, 20, 30),
],
),
(
subject.EdgePathType.RIGHT,
[
Point(15, 20, 30),
Point(12, 20, 30),
Point(10, 20, 30),
Point(10, 30, 30),
Point(10, 10, 30),
Point(10, 27, 30),
Point(10, 13, 30),
Point(10, 20, 30),
],
),
(
subject.EdgePathType.DEFAULT,
[
Point(15, 20, 30),
Point(5, 20, 30),
Point(12, 20, 30),
Point(8, 20, 30),
Point(10, 20, 30),
Point(10, 30, 30),
Point(10, 10, 30),
Point(10, 27, 30),
Point(10, 13, 30),
Point(10, 20, 30),
],
),
],
)
def get_edge_point_list(
def test_get_edge_point_list(
edge_path_type: subject.EdgePathType,
expected_result: List[Point],
) -> None:
"""It should get a list of well edge points."""
result = subject.get_edge_point_list(
Point(x=10, y=20, z=30), x_radius=5, y_radius=10, edge_path_type=edge_path_type
Point(x=10, y=20, z=30),
x_radius=5,
y_radius=10,
mm_from_edge=3,
edge_path_type=edge_path_type,
)

assert result == expected_result
5 changes: 5 additions & 0 deletions shared-data/command/schemas/11.json
Original file line number Diff line number Diff line change
Expand Up @@ -3924,6 +3924,11 @@
"default": 1.0,
"type": "number"
},
"mmFromEdge": {
"title": "Mmfromedge",
"description": "Offset away from the the well edge, in millimeters.Incompatible when a radius is included as a non 1.0 value.",
"type": "number"
},
"speed": {
"title": "Speed",
"description": "Override the travel speed in mm/s. This controls the straight linear speed of motion.",
Expand Down

0 comments on commit f22ed41

Please sign in to comment.