From b59f51c43c4dac68703d15c1714a30c445305c90 Mon Sep 17 00:00:00 2001 From: Max Marrone Date: Mon, 21 Oct 2024 14:32:03 -0400 Subject: [PATCH] Recover from pickUpTip false-positives. Missing tests. --- .../protocol_engine/commands/pick_up_tip.py | 10 ++++++-- .../protocol_engine/errors/__init__.py | 2 ++ .../protocol_engine/errors/exceptions.py | 23 ++++++++++++++++++- .../protocol_engine/execution/tip_handler.py | 23 +++++++++++-------- 4 files changed, 46 insertions(+), 12 deletions(-) diff --git a/api/src/opentrons/protocol_engine/commands/pick_up_tip.py b/api/src/opentrons/protocol_engine/commands/pick_up_tip.py index 5ccdcfc6f3a..bf8492cc74b 100644 --- a/api/src/opentrons/protocol_engine/commands/pick_up_tip.py +++ b/api/src/opentrons/protocol_engine/commands/pick_up_tip.py @@ -6,7 +6,7 @@ from typing_extensions import Literal -from ..errors import ErrorOccurrence, TipNotAttachedError +from ..errors import ErrorOccurrence, PickUpTipTipNotAttachedError from ..resources import ModelUtils from ..state import update_types from ..types import PickUpTipWellLocation, DeckPoint @@ -140,7 +140,12 @@ async def execute( labware_id=labware_id, well_name=well_name, ) - except TipNotAttachedError as e: + except PickUpTipTipNotAttachedError as e: + state_update_if_false_positive = update_types.StateUpdate() + state_update_if_false_positive.update_pipette_tip_state( + pipette_id=pipette_id, + tip_geometry=e.tip_geometry, + ) state_update.mark_tips_as_used( pipette_id=pipette_id, labware_id=labware_id, well_name=well_name ) @@ -157,6 +162,7 @@ async def execute( ], ), state_update=state_update, + state_update_if_false_positive=state_update_if_false_positive, ) else: state_update.update_pipette_tip_state( diff --git a/api/src/opentrons/protocol_engine/errors/__init__.py b/api/src/opentrons/protocol_engine/errors/__init__.py index 304f7db1fff..d281baecb83 100644 --- a/api/src/opentrons/protocol_engine/errors/__init__.py +++ b/api/src/opentrons/protocol_engine/errors/__init__.py @@ -8,6 +8,7 @@ InvalidSpecificationForRobotTypeError, InvalidLoadPipetteSpecsError, TipNotAttachedError, + PickUpTipTipNotAttachedError, TipAttachedError, CommandDoesNotExistError, LabwareNotLoadedError, @@ -88,6 +89,7 @@ "InvalidSpecificationForRobotTypeError", "InvalidLoadPipetteSpecsError", "TipNotAttachedError", + "PickUpTipTipNotAttachedError", "TipAttachedError", "CommandDoesNotExistError", "LabwareNotLoadedError", diff --git a/api/src/opentrons/protocol_engine/errors/exceptions.py b/api/src/opentrons/protocol_engine/errors/exceptions.py index dd9dc6e1d51..c89b0f24f2d 100644 --- a/api/src/opentrons/protocol_engine/errors/exceptions.py +++ b/api/src/opentrons/protocol_engine/errors/exceptions.py @@ -1,11 +1,17 @@ """Protocol engine exceptions.""" +from __future__ import annotations + from logging import getLogger -from typing import Any, Dict, Optional, Union, Iterator, Sequence +from typing import Any, Dict, Final, Optional, Union, Iterator, Sequence, TYPE_CHECKING from opentrons_shared_data.errors import ErrorCodes from opentrons_shared_data.errors.exceptions import EnumeratedError, PythonException +if TYPE_CHECKING: + from opentrons.protocol_engine.types import TipGeometry + + log = getLogger(__name__) @@ -132,6 +138,21 @@ def __init__( super().__init__(ErrorCodes.UNEXPECTED_TIP_REMOVAL, message, details, wrapping) +class PickUpTipTipNotAttachedError(TipNotAttachedError): + """Raised from TipHandler.pick_up_tip(). + + This is like TipNotAttachedError except that it carries some extra information + about the attempted operation. + """ + + tip_geometry: Final[TipGeometry] + """The tip geometry that would have been on the pipette, had the operation succeeded.""" + + def __init__(self, tip_geometry: TipGeometry) -> None: + super().__init__() + self.tip_geometry = tip_geometry + + class TipAttachedError(ProtocolEngineError): """Raised when a tip shouldn't be attached, but is.""" diff --git a/api/src/opentrons/protocol_engine/execution/tip_handler.py b/api/src/opentrons/protocol_engine/execution/tip_handler.py index d5c18200095..447c61c5f75 100644 --- a/api/src/opentrons/protocol_engine/execution/tip_handler.py +++ b/api/src/opentrons/protocol_engine/execution/tip_handler.py @@ -1,9 +1,10 @@ """Tip pickup and drop procedures.""" -from typing import Optional, Dict +from typing import Final, Optional, Dict from typing_extensions import Protocol as TypingProtocol from opentrons.hardware_control import HardwareControlAPI from opentrons.hardware_control.types import FailedTipStateCheck, InstrumentProbeType +from opentrons.protocol_engine.errors.exceptions import PickUpTipTipNotAttachedError from opentrons.types import Mount from opentrons_shared_data.errors.exceptions import ( @@ -72,7 +73,7 @@ async def pick_up_tip( Tip geometry of the picked up tip. Raises: - TipNotAttachedError + PickUpTipTipNotAttachedError """ ... @@ -245,11 +246,19 @@ async def pick_up_tip( nominal_fallback=nominal_tip_geometry.length, ) + tip_geometry = TipGeometry( + length=actual_tip_length, + diameter=nominal_tip_geometry.diameter, + volume=nominal_tip_geometry.volume, + ) + await self._hardware_api.tip_pickup_moves( mount=hw_mount, presses=None, increment=None ) - # Allow TipNotAttachedError to propagate. - await self.verify_tip_presence(pipette_id, TipPresenceStatus.PRESENT) + try: + await self.verify_tip_presence(pipette_id, TipPresenceStatus.PRESENT) + except TipNotAttachedError as e: + raise PickUpTipTipNotAttachedError(tip_geometry=tip_geometry) from e # todo(mm, 2024-10-21): This sequence of cache_tip(), set_current_tiprack_diameter(), # and set_working_volume() is almost the same as self.add_tip(), except one uses @@ -268,11 +277,7 @@ async def pick_up_tip( tip_volume=nominal_tip_geometry.volume, ) - return TipGeometry( - length=actual_tip_length, - diameter=nominal_tip_geometry.diameter, - volume=nominal_tip_geometry.volume, - ) + return tip_geometry async def drop_tip(self, pipette_id: str, home_after: Optional[bool]) -> None: """See documentation on abstract base class."""