Skip to content

Commit

Permalink
feat(api): raise an error when the gripper pickup fails (#14130)
Browse files Browse the repository at this point in the history
  • Loading branch information
caila-marashaj authored Dec 8, 2023
1 parent e6f7a59 commit 4c26c6a
Show file tree
Hide file tree
Showing 14 changed files with 265 additions and 41 deletions.
26 changes: 25 additions & 1 deletion api/src/opentrons/hardware_control/instruments/ot3/gripper.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@
)
from ..instrument_abc import AbstractInstrument
from opentrons.hardware_control.dev_types import AttachedGripper, GripperDict
from opentrons_shared_data.errors.exceptions import CommandPreconditionViolated
from opentrons_shared_data.errors.exceptions import (
CommandPreconditionViolated,
FailedGripperPickupError,
)

from opentrons_shared_data.gripper import (
GripperDefinition,
Expand Down Expand Up @@ -101,6 +104,10 @@ def remove_probe(self) -> None:
assert self.attached_probe
self._attached_probe = None

@property
def max_allowed_grip_error(self) -> float:
return self._geometry.max_allowed_grip_error

@property
def jaw_width(self) -> float:
jaw_max = self.geometry.jaw_width["max"]
Expand Down Expand Up @@ -196,6 +203,23 @@ def check_calibration_pin_location_is_accurate(self) -> None:
},
)

def check_labware_pickup(self, labware_width: float) -> None:
"""Ensure that a gripper pickup succeeded."""
# check if the gripper is at an acceptable position after attempting to
# pick up labware
expected_gripper_position = labware_width
current_gripper_position = self.jaw_width
if (
abs(current_gripper_position - expected_gripper_position)
> self.max_allowed_grip_error
):
raise FailedGripperPickupError(
details={
"expected jaw width": expected_gripper_position,
"actual jaw width": current_gripper_position,
},
)

def critical_point(self, cp_override: Optional[CriticalPoint] = None) -> Point:
"""
The vector from the gripper mount to the critical point, which is selectable
Expand Down
1 change: 1 addition & 0 deletions api/src/opentrons/protocol_engine/errors/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,4 +138,5 @@
"NotSupportedOnRobotType",
# error occurrence models
"ErrorOccurrence",
"FailedGripperPickupError",
]
12 changes: 11 additions & 1 deletion api/src/opentrons/protocol_engine/execution/labware_movement.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ async def move_labware_with_gripper(
post_drop_slide_offset=post_drop_slide_offset,
)
labware_grip_force = self._state_store.labware.get_grip_force(labware_id)

gripper_opened = False
for waypoint_data in movement_waypoints:
if waypoint_data.jaw_open:
if waypoint_data.dropping:
Expand All @@ -146,12 +146,22 @@ async def move_labware_with_gripper(
# on the side of a falling tiprack catches the jaw.
await ot3api.disengage_axes([Axis.Z_G])
await ot3api.ungrip()
gripper_opened = True
if waypoint_data.dropping:
# We lost the position estimation after disengaging the axis, so
# it is necessary to home it next
await ot3api.home_z(OT3Mount.GRIPPER)
else:
await ot3api.grip(force_newtons=labware_grip_force)
# we only want to check position after the gripper has opened and
# should be holding labware
if gripper_opened:
assert ot3api.hardware_gripper
ot3api.hardware_gripper.check_labware_pickup(
labware_width=self._state_store.labware.get_dimensions(
labware_id
).y
)
await ot3api.move_to(
mount=gripper_mount, abs_position=waypoint_data.position
)
Expand Down
49 changes: 48 additions & 1 deletion api/tests/opentrons/hardware_control/test_gripper.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
from typing import Optional, Callable, TYPE_CHECKING
from typing import Optional, Callable, TYPE_CHECKING, Any, Generator
import pytest
from contextlib import nullcontext
from unittest.mock import MagicMock, patch, PropertyMock

from opentrons.types import Point
from opentrons.calibration_storage import types as cal_types
from opentrons.hardware_control.instruments.ot3 import gripper, instrument_calibration
from opentrons.hardware_control.types import CriticalPoint
from opentrons.config import gripper_config
from opentrons_shared_data.gripper import GripperModel
from opentrons_shared_data.errors.exceptions import FailedGripperPickupError

if TYPE_CHECKING:
from opentrons.hardware_control.instruments.ot3.instrument_calibration import (
Expand All @@ -26,6 +29,24 @@ def fake_offset() -> "GripperCalibrationOffset":
return load_gripper_calibration_offset("fakeid123")


@pytest.fixture
def mock_jaw_width() -> Generator[MagicMock, None, None]:
with patch(
"opentrons.hardware_control.instruments.ot3.gripper.Gripper.jaw_width",
new_callable=PropertyMock,
) as jaw_width:
yield jaw_width


@pytest.fixture
def mock_max_grip_error() -> Generator[MagicMock, None, None]:
with patch(
"opentrons.hardware_control.instruments.ot3.gripper.Gripper.max_allowed_grip_error",
new_callable=PropertyMock,
) as max_error:
yield max_error


@pytest.mark.ot3_only
def test_id_get_added_to_dict(fake_offset: "GripperCalibrationOffset") -> None:
gripr = gripper.Gripper(fake_gripper_conf, fake_offset, "fakeid123")
Expand Down Expand Up @@ -67,6 +88,32 @@ def test_load_gripper_cal_offset(fake_offset: "GripperCalibrationOffset") -> Non
)


@pytest.mark.ot3_only
@pytest.mark.parametrize(
argnames=["jaw_width_val", "error_context"],
argvalues=[
(89, nullcontext()),
(100, pytest.raises(FailedGripperPickupError)),
(50, pytest.raises(FailedGripperPickupError)),
(85, nullcontext()),
],
)
def test_check_labware_pickup(
mock_jaw_width: Any,
mock_max_grip_error: Any,
jaw_width_val: float,
error_context: Any,
) -> None:
"""Test that FailedGripperPickupError is raised correctly."""
# This should only be triggered when the difference between the
# gripper jaw and labware widths is greater than the max allowed error.
gripr = gripper.Gripper(fake_gripper_conf, fake_offset, "fakeid123")
mock_jaw_width.return_value = jaw_width_val
mock_max_grip_error.return_value = 6
with error_context:
gripr.check_labware_pickup(85)


@pytest.mark.ot3_only
def test_reload_instrument_cal_ot3(fake_offset: "GripperCalibrationOffset") -> None:
old_gripper = gripper.Gripper(
Expand Down
Loading

0 comments on commit 4c26c6a

Please sign in to comment.