Skip to content

Commit

Permalink
feat(api): return locations from MoveLabware
Browse files Browse the repository at this point in the history
MoveLabware is a little different than the other implementations.

It needs an origin and a destination sequence, since it's a move -
that's straightforward.

But it also needs to drive a distinction between an immediate
destination: where is the labware dropped off? and an eventual
destination: where does the labware end up? so that the necessary
information to figure out what happens when you move a labware to the
trash is there. We need to identify the trash, and which trash; but we
also need to indicate that the labware is going to get thrown out. So we
have three total locations.
  • Loading branch information
sfoster1 committed Feb 4, 2025
1 parent b8cbdde commit b453489
Show file tree
Hide file tree
Showing 3 changed files with 280 additions and 38 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,21 +22,3 @@ class LabwarePositionResultMixin(LabwareHandlingResultMixin):
None,
description="An ID referencing the labware offset that will apply to this labware in this location.",
)


class LabwareMotionResultMixin(BaseModel):
"""A result for commands that move a labware entity."""

labwareId: str = Field(..., description="The id of the labware.")
newLocationSequence: LabwareLocationSequence | None = Field(
None,
description="the full location down to the deck of the labware after this command.",
)
originalLocationSequence: LabwareLocationSequence | None = Field(
None,
description="The full location down to the deck of the labware before this command.",
)
offsetId: str | None = Field(
None,
description="An ID referencing the labware offset that will apply to this labware in the position this command leaves it in.",
)
114 changes: 99 additions & 15 deletions api/src/opentrons/protocol_engine/commands/move_labware.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from __future__ import annotations
from typing import TYPE_CHECKING, Optional, Type, Any, List
from typing_extensions import TypedDict # note: need this instead of typing for py<3.12

from pydantic.json_schema import SkipJsonSchema
from pydantic import BaseModel, Field
Expand All @@ -26,6 +27,9 @@
LabwareMovementStrategy,
LabwareOffsetVector,
LabwareMovementOffsetData,
LabwareLocationSequence,
NotOnDeckLocationSequenceComponent,
OFF_DECK_LOCATION,
)
from ..errors import (
LabwareMovementNotAllowedError,
Expand Down Expand Up @@ -100,6 +104,31 @@ class MoveLabwareResult(BaseModel):
" so the default of (0, 0, 0) will be used."
),
)
eventualDestinationLocationSequence: LabwareLocationSequence | None = Field(
None,
description=(
"The full location in which this labware will eventually reside. This will typically be the same as its "
"immediate destination, but if this labware is going to the trash then this field will be off deck."
),
)
immediateDestinationLocationSequence: LabwareLocationSequence | None = Field(
None,
description=(
"The full location to which this labware is being moved, right now."
),
)
originLocationSequence: LabwareLocationSequence | None = Field(
None,
description="The full location down to the deck of the labware before this command.",
)


class ErrorDetails(TypedDict):
"""Location details for a failed gripper move."""

originLocationSequence: LabwareLocationSequence
immediateDestinationLocationSequence: LabwareLocationSequence
eventualDestinationLocationSequence: LabwareLocationSequence


class GripperMovementError(ErrorOccurrence):
Expand All @@ -112,6 +141,8 @@ class GripperMovementError(ErrorOccurrence):

errorType: Literal["gripperMovement"] = "gripperMovement"

errorInfo: ErrorDetails


_ExecuteReturn = SuccessData[MoveLabwareResult] | DefinedErrorData[GripperMovementError]

Expand Down Expand Up @@ -152,6 +183,11 @@ async def execute(self, params: MoveLabwareParams) -> _ExecuteReturn: # noqa: C
f"Cannot move fixed trash labware '{current_labware_definition.parameters.loadName}'."
)

origin_location_sequence = self._state_view.geometry.get_location_sequence(
params.labwareId
)
eventual_destination_location_sequence: LabwareLocationSequence | None = None

if isinstance(params.newLocation, AddressableAreaLocation):
area_name = params.newLocation.addressableAreaName
if (
Expand Down Expand Up @@ -181,9 +217,19 @@ async def execute(self, params: MoveLabwareParams) -> _ExecuteReturn: # noqa: C
y=0,
z=0,
)
eventual_destination_location_sequence = [
NotOnDeckLocationSequenceComponent(
logicalLocationName=OFF_DECK_LOCATION
)
]
elif fixture_validation.is_trash(area_name):
# When dropping labware in the trash bins we want to ensure they are lids
# and enforce a y-axis drop offset to ensure they fall within the trash bin
eventual_destination_location_sequence = [
NotOnDeckLocationSequenceComponent(
logicalLocationName=OFF_DECK_LOCATION
)
]
if labware_validation.validate_definition_is_lid(
self._state_view.labware.get_definition(params.labwareId)
):
Expand Down Expand Up @@ -298,6 +344,16 @@ async def execute(self, params: MoveLabwareParams) -> _ExecuteReturn: # noqa: C
if trash_lid_drop_offset:
user_offset_data.dropOffset += trash_lid_drop_offset

immediate_destination_location_sequence = (
self._state_view.geometry.get_predicted_location_sequence(
validated_new_loc
)
)
if eventual_destination_location_sequence is None:
eventual_destination_location_sequence = (
immediate_destination_location_sequence
)

try:
# Skips gripper moves when using virtual gripper
await self._labware_movement.move_labware_with_gripper(
Expand All @@ -314,20 +370,23 @@ async def execute(self, params: MoveLabwareParams) -> _ExecuteReturn: # noqa: C
# todo(mm, 2024-09-26): Catch LabwareNotPickedUpError when that exists and
# move_labware_with_gripper() raises it.
) as exception:
gripper_movement_error: GripperMovementError | None = (
GripperMovementError(
id=self._model_utils.generate_id(),
createdAt=self._model_utils.get_timestamp(),
errorCode=exception.code.value.code,
detail=exception.code.value.detail,
wrappedErrors=[
ErrorOccurrence.from_failed(
id=self._model_utils.generate_id(),
createdAt=self._model_utils.get_timestamp(),
error=exception,
)
],
)
gripper_movement_error: GripperMovementError | None = GripperMovementError(
id=self._model_utils.generate_id(),
createdAt=self._model_utils.get_timestamp(),
errorCode=exception.code.value.code,
detail=exception.code.value.detail,
errorInfo={
"originLocationSequence": origin_location_sequence,
"immediateDestinationLocationSequence": immediate_destination_location_sequence,
"eventualDestinationLocationSequence": eventual_destination_location_sequence,
},
wrappedErrors=[
ErrorOccurrence.from_failed(
id=self._model_utils.generate_id(),
createdAt=self._model_utils.get_timestamp(),
error=exception,
)
],
)
else:
gripper_movement_error = None
Expand All @@ -343,7 +402,27 @@ async def execute(self, params: MoveLabwareParams) -> _ExecuteReturn: # noqa: C

elif params.strategy == LabwareMovementStrategy.MANUAL_MOVE_WITH_PAUSE:
# Pause to allow for manual labware movement
immediate_destination_location_sequence = (
self._state_view.geometry.get_predicted_location_sequence(
params.newLocation
)
)
if eventual_destination_location_sequence is None:
eventual_destination_location_sequence = (
immediate_destination_location_sequence
)

await self._run_control.wait_for_resume()
else:
immediate_destination_location_sequence = (
self._state_view.geometry.get_predicted_location_sequence(
params.newLocation
)
)
if eventual_destination_location_sequence is None:
eventual_destination_location_sequence = (
immediate_destination_location_sequence
)

# We may have just moved the labware that contains the current well out from
# under the pipette. Clear the current location to reflect the fact that the
Expand Down Expand Up @@ -398,7 +477,12 @@ async def execute(self, params: MoveLabwareParams) -> _ExecuteReturn: # noqa: C
)

return SuccessData(
public=MoveLabwareResult(offsetId=new_offset_id),
public=MoveLabwareResult(
offsetId=new_offset_id,
originLocationSequence=origin_location_sequence,
immediateDestinationLocationSequence=immediate_destination_location_sequence,
eventualDestinationLocationSequence=eventual_destination_location_sequence,
),
state_update=state_update,
)

Expand Down
Loading

0 comments on commit b453489

Please sign in to comment.