Skip to content

Commit

Permalink
Consume tips in failed pickUpTip commands.
Browse files Browse the repository at this point in the history
  • Loading branch information
SyntaxColoring committed Apr 1, 2024
1 parent b61913f commit 9322657
Show file tree
Hide file tree
Showing 6 changed files with 55 additions and 6 deletions.
4 changes: 0 additions & 4 deletions api/src/opentrons/protocol_api/core/engine/instrument.py
Original file line number Diff line number Diff line change
Expand Up @@ -414,10 +414,6 @@ def pick_up_tip(
well_name=well_name,
well_location=well_location,
)
# FIX BEFORE MERGE?: If the pickUpTip fails, the tip tracker (which is sort of
# kind of in Protocol Engine) doesn't think the tip has been consumed. So,
# the next PAPI pick_up_tip() after the error recovery will try to pick up
# the same tip, diverging from the protocol's analysis.

# FIX BEFORE MERGE: We should probably only set_last_location() if the
# pickUpTip succeeded.
Expand Down
17 changes: 17 additions & 0 deletions api/src/opentrons/protocol_engine/actions/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,11 +154,28 @@ class FailCommandAction:
"""

command_id: str
"""The command to fail."""

error_id: str
"""An ID to assign to the command's error.
Must be unique to this occurrence of the error.
"""

failed_at: datetime
"""When the command failed."""

error: EnumeratedError
"""The underlying exception that caused this command to fail."""

notes: List[CommandNote]
"""Overwrite the command's `.notes` with these."""

type: ErrorRecoveryType
"""How this error should be handled in the context of the overall run."""

running_command: Command
"""The command to fail, in its prior `running` state."""


@dataclass(frozen=True)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ async def execute(self, command_id: str) -> None:
FailCommandAction(
error=error,
command_id=running_command.id,
running_command=running_command,
error_id=self._model_utils.generate_id(),
failed_at=self._model_utils.get_timestamp(),
notes=note_tracker.get_notes(),
Expand Down
1 change: 1 addition & 0 deletions api/src/opentrons/protocol_engine/protocol_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,7 @@ def estop(
self._state_store.commands.get_running_command_id()
or self._state_store.commands.state.queued_command_ids.head(None)
)
# FIX BEFORE MERGE

if current_id is not None:
fail_action = FailCommandAction(
Expand Down
37 changes: 35 additions & 2 deletions api/src/opentrons/protocol_engine/state/tips.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@
from ..actions import (
Action,
SucceedCommandAction,
FailCommandAction,
ResetTipsAction,
)
from ..commands import (
Command,
LoadLabwareResult,
PickUpTip,
PickUpTipResult,
DropTipResult,
DropTipInPlaceResult,
Expand All @@ -20,6 +22,7 @@
PipetteConfigUpdateResultMixin,
PipetteNozzleLayoutResultMixin,
)
from ..error_recovery_policy import ErrorRecoveryType

from opentrons.hardware_control.nozzle_manager import NozzleMap

Expand Down Expand Up @@ -71,7 +74,7 @@ def handle_action(self, action: Action) -> None:
self._state.channels_by_pipette_id[pipette_id] = config.channels
self._state.active_channels_by_pipette_id[pipette_id] = config.channels
self._state.nozzle_map_by_pipette_id[pipette_id] = config.nozzle_map
self._handle_command(action.command)
self._handle_succeeded_command(action.command)

if isinstance(action.private_result, PipetteNozzleLayoutResultMixin):
pipette_id = action.private_result.pipette_id
Expand All @@ -86,6 +89,9 @@ def handle_action(self, action: Action) -> None:
pipette_id
] = self._state.channels_by_pipette_id[pipette_id]

elif isinstance(action, FailCommandAction):
self._handle_failed_command(action)

elif isinstance(action, ResetTipsAction):
labware_id = action.labware_id

Expand All @@ -94,7 +100,7 @@ def handle_action(self, action: Action) -> None:
well_name
] = TipRackWellState.CLEAN

def _handle_command(self, command: Command) -> None:
def _handle_succeeded_command(self, command: Command) -> None:
if (
isinstance(command.result, LoadLabwareResult)
and command.result.definition.parameters.isTiprack
Expand Down Expand Up @@ -124,6 +130,33 @@ def _handle_command(self, command: Command) -> None:
pipette_id = command.params.pipetteId
self._state.length_by_pipette_id.pop(pipette_id, None)

def _handle_failed_command(
self,
action: FailCommandAction,
) -> None:
# If a pickUpTip command fails recoverably, mark the tips as used. This way,
# when the protocol is resumed and the Python Protocol API calls
# `get_next_tip()`, we'll move on to other tips as expected.
#
# We don't attempt this for nonrecoverable errors because maybe the failure
# was due to a bad labware ID or well name.
if (
isinstance(action.running_command, PickUpTip)
and action.type != ErrorRecoveryType.FAIL_RUN
):
self._set_used_tips(
pipette_id=action.running_command.params.pipetteId,
labware_id=action.running_command.params.labwareId,
well_name=action.running_command.params.wellName,
)
# TODO(mm, 2024-04-01): We're logically removing the tip from the tip rack,
# but we're not logically updating the pipette to have that tip on it,
# which is inconsistent and confusing.
#
# To fix that, we need the tip length. But that traditionally comes to us
# through the command result, which we don't have if the command failed. We
# may need to expand failed commands to have a private result.

def _set_used_tips( # noqa: C901
self, pipette_id: str, well_name: str, labware_id: str
) -> None:
Expand Down
1 change: 1 addition & 0 deletions api/src/opentrons/protocol_runner/legacy_command_mapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,7 @@ def map_command( # noqa: C901
results.append(
pe_actions.FailCommandAction(
command_id=running_command.id,
running_command=running_command,
error_id=ModelUtils.generate_id(),
failed_at=now,
error=LegacyContextCommandError(command_error),
Expand Down

0 comments on commit 9322657

Please sign in to comment.