Skip to content

Commit

Permalink
feat(api, shared-data, app): Move lid PAPI and Core implementation (#…
Browse files Browse the repository at this point in the history
…17259)

Covers EXEC-1005
Introduce `move_lid()` command to  PAPI, allows the movement of lids from stacks and source labware to destination locations.
  • Loading branch information
CaseyBatten authored Jan 17, 2025
1 parent 2b75afa commit 7393c92
Show file tree
Hide file tree
Showing 60 changed files with 3,550 additions and 178 deletions.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

15 changes: 15 additions & 0 deletions api/src/opentrons/legacy_commands/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,18 @@ def stringify_labware_movement_command(
destination_text = _stringify_labware_movement_location(destination)
gripper_text = " with gripper" if use_gripper else ""
return f"Moving {source_labware_text} to {destination_text}{gripper_text}"


def stringify_lid_movement_command(
source: Union[
DeckLocation, OffDeckType, Labware, ModuleContext, WasteChute, TrashBin
],
destination: Union[
DeckLocation, OffDeckType, Labware, ModuleContext, WasteChute, TrashBin
],
use_gripper: bool,
) -> str:
source_labware_text = _stringify_labware_movement_location(source)
destination_text = _stringify_labware_movement_location(destination)
gripper_text = " with gripper" if use_gripper else ""
return f"Moving lid from {source_labware_text} to {destination_text}{gripper_text}"
6 changes: 5 additions & 1 deletion api/src/opentrons/protocol_api/core/engine/deck_conflict.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
OnLabwareLocation,
AddressableAreaLocation,
OFF_DECK_LOCATION,
SYSTEM_LOCATION,
)
from opentrons.protocol_engine.errors.exceptions import LabwareNotLoadedOnModuleError
from opentrons.types import DeckSlotName, StagingSlotName, Point
Expand Down Expand Up @@ -245,7 +246,10 @@ def _map_labware(
# TODO(jbl 2023-06-08) check if we need to do any logic here or if this is correct
return None

elif location_from_engine == OFF_DECK_LOCATION:
elif (
location_from_engine == OFF_DECK_LOCATION
or location_from_engine == SYSTEM_LOCATION
):
# This labware is off-deck. Exclude it from conflict checking.
# todo(mm, 2023-02-23): Move this logic into wrapped_deck_conflict.
return None
Expand Down
200 changes: 200 additions & 0 deletions api/src/opentrons/protocol_api/core/engine/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
from opentrons.protocol_engine.types import (
ModuleModel as ProtocolEngineModuleModel,
OFF_DECK_LOCATION,
SYSTEM_LOCATION,
LabwareLocation,
NonStackedLocation,
)
Expand Down Expand Up @@ -77,6 +78,7 @@
)
from .exceptions import InvalidModuleLocationError
from . import load_labware_params, deck_conflict, overlap_versions
from opentrons.protocol_engine.resources import labware_validation

if TYPE_CHECKING:
from ...labware import Labware
Expand Down Expand Up @@ -442,6 +444,203 @@ def move_labware(
existing_module_ids=list(self._module_cores_by_id.keys()),
)

def move_lid( # noqa: C901
self,
source_location: Union[DeckSlotName, StagingSlotName, LabwareCore],
new_location: Union[
DeckSlotName,
StagingSlotName,
LabwareCore,
OffDeckType,
WasteChute,
TrashBin,
],
use_gripper: bool,
pause_for_manual_move: bool,
pick_up_offset: Optional[Tuple[float, float, float]],
drop_offset: Optional[Tuple[float, float, float]],
) -> LabwareCore | None:
"""Move the given lid to a new location."""
if use_gripper:
strategy = LabwareMovementStrategy.USING_GRIPPER
elif pause_for_manual_move:
strategy = LabwareMovementStrategy.MANUAL_MOVE_WITH_PAUSE
else:
strategy = LabwareMovementStrategy.MANUAL_MOVE_WITHOUT_PAUSE

if isinstance(source_location, DeckSlotName) or isinstance(
source_location, StagingSlotName
):
# Find the source labware at the provided deck slot
labware_in_slot = self._engine_client.state.labware.get_by_slot(
source_location
)
if labware_in_slot is None:
raise LabwareNotLoadedOnLabwareError(
"Lid cannot be loaded on non-labware position."
)
else:
labware = LabwareCore(labware_in_slot.id, self._engine_client)
else:
labware = source_location

# if this is a labware stack, we need to find the labware at the top of the stack
if labware_validation.is_lid_stack(labware.load_name):
lid_id = self._engine_client.state.labware.get_highest_child_labware(
labware.labware_id
)
# if this is a labware with a lid, we just need to find its lid_id
else:
lid = self._engine_client.state.labware.get_lid_by_labware_id(
labware.labware_id
)
if lid is not None:
lid_id = lid.id
else:
raise ValueError("Cannot move a lid off of a labware with no lid.")

_pick_up_offset = (
LabwareOffsetVector(
x=pick_up_offset[0], y=pick_up_offset[1], z=pick_up_offset[2]
)
if pick_up_offset
else None
)
_drop_offset = (
LabwareOffsetVector(x=drop_offset[0], y=drop_offset[1], z=drop_offset[2])
if drop_offset
else None
)

create_new_lid_stack = False

if isinstance(new_location, DeckSlotName) or isinstance(
new_location, StagingSlotName
):
# Find the destination labware at the provided deck slot
destination_labware_in_slot = self._engine_client.state.labware.get_by_slot(
new_location
)
if destination_labware_in_slot is None:
to_location = self._convert_labware_location(location=new_location)
# absolutely must make a new lid stack
create_new_lid_stack = True
else:
highest_child_location = (
self._engine_client.state.labware.get_highest_child_labware(
destination_labware_in_slot.id
)
)
if labware_validation.validate_definition_is_adapter(
self._engine_client.state.labware.get_definition(
highest_child_location
)
):
# absolutely must make a new lid stack
create_new_lid_stack = True

to_location = self._convert_labware_location(
location=LabwareCore(highest_child_location, self._engine_client)
)
elif isinstance(new_location, LabwareCore):
highest_child_location = (
self._engine_client.state.labware.get_highest_child_labware(
new_location.labware_id
)
)
if labware_validation.validate_definition_is_adapter(
self._engine_client.state.labware.get_definition(highest_child_location)
):
# absolutely must make a new lid stack
create_new_lid_stack = True
to_location = self._convert_labware_location(
location=LabwareCore(highest_child_location, self._engine_client)
)
else:
to_location = self._convert_labware_location(location=new_location)

output_result = None
if create_new_lid_stack:
# Make a new lid stack object that is empty
result = self._engine_client.execute_command_without_recovery(
cmd.LoadLidStackParams(
location=SYSTEM_LOCATION,
loadName="empty",
version=1,
namespace="empty",
quantity=0,
)
)

# Move the lid stack object from the SYSTEM_LOCATION space to the desired deck location
self._engine_client.execute_command(
cmd.MoveLabwareParams(
labwareId=result.stackLabwareId,
newLocation=to_location,
strategy=LabwareMovementStrategy.MANUAL_MOVE_WITHOUT_PAUSE,
pickUpOffset=None,
dropOffset=None,
)
)

output_result = LabwareCore(
labware_id=result.stackLabwareId, engine_client=self._engine_client
)
destination = self._convert_labware_location(location=output_result)
else:
destination = to_location

self._engine_client.execute_command(
cmd.MoveLabwareParams(
labwareId=lid_id,
newLocation=destination,
strategy=strategy,
pickUpOffset=_pick_up_offset,
dropOffset=_drop_offset,
)
)

# Handle leftover empty lid stack if there is one
if (
labware_validation.is_lid_stack(labware.load_name)
and self._engine_client.state.labware.get_highest_child_labware(
labware_id=labware.labware_id
)
== labware.labware_id
):
# The originating lid stack is now empty, so we need to move it to the SYSTEM_LOCATION
self._engine_client.execute_command(
cmd.MoveLabwareParams(
labwareId=labware.labware_id,
newLocation=SYSTEM_LOCATION,
strategy=LabwareMovementStrategy.MANUAL_MOVE_WITHOUT_PAUSE,
pickUpOffset=None,
dropOffset=None,
)
)

if strategy == LabwareMovementStrategy.USING_GRIPPER:
# Clear out last location since it is not relevant to pipetting
# and we only use last location for in-place pipetting commands
self.set_last_location(location=None, mount=Mount.EXTENSION)

# FIXME(jbl, 2024-01-04) deck conflict after execution logic issue, read notes in load_labware for more info:
deck_conflict.check(
engine_state=self._engine_client.state,
new_labware_id=lid_id,
existing_disposal_locations=self._disposal_locations,
# TODO: We can now fetch these IDs from engine too.
# See comment in self.load_labware().
existing_labware_ids=[
labware_id
for labware_id in self._labware_cores_by_id
if labware_id != labware_id
],
existing_module_ids=list(self._module_cores_by_id.keys()),
)

return output_result

def _resolve_module_hardware(
self, serial_number: str, model: ModuleModel
) -> AbstractModule:
Expand Down Expand Up @@ -734,6 +933,7 @@ def load_lid_stack(
)

# FIXME(CHB, 2024-12-04) just like load labware and load adapter we have a validating after loading the object issue
assert load_result.definition is not None
validation.ensure_definition_is_lid(load_result.definition)

deck_conflict.check(
Expand Down
3 changes: 3 additions & 0 deletions api/src/opentrons/protocol_api/core/engine/stringify.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ def _labware_location_string(
elif location == "offDeck":
return "[off-deck]"

elif location == "systemLocation":
return "[systemLocation]"


def _labware_name(engine_client: SyncClient, labware_id: str) -> str:
"""Return the user-specified labware label, or fall back to the display name from the def."""
Expand Down
19 changes: 19 additions & 0 deletions api/src/opentrons/protocol_api/core/legacy/legacy_protocol_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,25 @@ def move_labware(
"""Move labware to new location."""
raise APIVersionError(api_element="Labware movement")

def move_lid(
self,
source_location: Union[DeckSlotName, StagingSlotName, LegacyLabwareCore],
new_location: Union[
DeckSlotName,
StagingSlotName,
LegacyLabwareCore,
OffDeckType,
WasteChute,
TrashBin,
],
use_gripper: bool,
pause_for_manual_move: bool,
pick_up_offset: Optional[Tuple[float, float, float]],
drop_offset: Optional[Tuple[float, float, float]],
) -> LegacyLabwareCore | None:
"""Move lid to new location."""
raise APIVersionError(api_element="Lid movement")

def load_module(
self,
model: ModuleModel,
Expand Down
19 changes: 19 additions & 0 deletions api/src/opentrons/protocol_api/core/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,25 @@ def move_labware(
) -> None:
...

@abstractmethod
def move_lid(
self,
source_location: Union[DeckSlotName, StagingSlotName, LabwareCoreType],
new_location: Union[
DeckSlotName,
StagingSlotName,
LabwareCoreType,
OffDeckType,
WasteChute,
TrashBin,
],
use_gripper: bool,
pause_for_manual_move: bool,
pick_up_offset: Optional[Tuple[float, float, float]],
drop_offset: Optional[Tuple[float, float, float]],
) -> LabwareCoreType | None:
...

@abstractmethod
def load_module(
self,
Expand Down
Loading

0 comments on commit 7393c92

Please sign in to comment.