Skip to content

Commit

Permalink
feat(api, shared-data): Add support for labware lids and publish tc l…
Browse files Browse the repository at this point in the history
…id seal labware (#16345)

Covers PLAT-356, PLAT-264, PLAT-540

Publicizes "opentrons_tough_pcr_auto_sealing_lid" labware. Implements multilabware stacks, new labware allowedRole "lid" for labware, alongside "lidOffsets" subcategory of the gripper offsets.
  • Loading branch information
CaseyBatten authored Oct 10, 2024
1 parent 6012297 commit eeb8972
Show file tree
Hide file tree
Showing 17 changed files with 664 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ async def move_labware_with_gripper(
current_location=current_location,
)

current_labware = self._state_store.labware.get_definition(labware_id)
async with self._thermocycler_plate_lifter.lift_plate_for_labware_movement(
labware_location=current_location
):
Expand All @@ -134,6 +135,7 @@ async def move_labware_with_gripper(
from_location=current_location,
to_location=new_location,
additional_offset_vector=user_offset_data,
current_labware=current_labware,
)
)
from_labware_center = self._state_store.geometry.get_labware_grip_point(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ def validate_definition_is_adapter(definition: LabwareDefinition) -> bool:
return LabwareRole.adapter in definition.allowedRoles


def validate_definition_is_lid(definition: LabwareDefinition) -> bool:
"""Validate that one of the definition's allowed roles is `lid`."""
return LabwareRole.lid in definition.allowedRoles


def validate_labware_can_be_stacked(
top_labware_definition: LabwareDefinition, below_labware_load_name: str
) -> bool:
Expand Down
83 changes: 76 additions & 7 deletions api/src/opentrons/protocol_engine/state/geometry.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from opentrons_shared_data.deck.types import CutoutFixture
from opentrons_shared_data.pipette import PIPETTE_X_SPAN
from opentrons_shared_data.pipette.types import ChannelCount
from opentrons.protocols.models import LabwareDefinition

from .. import errors
from ..errors import (
Expand All @@ -20,7 +21,7 @@
LabwareMovementNotAllowedError,
InvalidWellDefinitionError,
)
from ..resources import fixture_validation
from ..resources import fixture_validation, labware_validation
from ..types import (
OFF_DECK_LOCATION,
LoadedLabware,
Expand All @@ -46,6 +47,7 @@
AddressableOffsetVector,
StagingSlotLocation,
LabwareOffsetLocation,
ModuleModel,
)
from .config import Config
from .labware import LabwareView
Expand Down Expand Up @@ -997,17 +999,22 @@ def get_final_labware_movement_offset_vectors(
from_location: OnDeckLabwareLocation,
to_location: OnDeckLabwareLocation,
additional_offset_vector: LabwareMovementOffsetData,
current_labware: LabwareDefinition,
) -> LabwareMovementOffsetData:
"""Calculate the final labware offset vector to use in labware movement."""
pick_up_offset = (
self.get_total_nominal_gripper_offset_for_move_type(
location=from_location, move_type=_GripperMoveType.PICK_UP_LABWARE
location=from_location,
move_type=_GripperMoveType.PICK_UP_LABWARE,
current_labware=current_labware,
)
+ additional_offset_vector.pickUpOffset
)
drop_offset = (
self.get_total_nominal_gripper_offset_for_move_type(
location=to_location, move_type=_GripperMoveType.DROP_LABWARE
location=to_location,
move_type=_GripperMoveType.DROP_LABWARE,
current_labware=current_labware,
)
+ additional_offset_vector.dropOffset
)
Expand Down Expand Up @@ -1038,7 +1045,10 @@ def ensure_valid_gripper_location(
return location

def get_total_nominal_gripper_offset_for_move_type(
self, location: OnDeckLabwareLocation, move_type: _GripperMoveType
self,
location: OnDeckLabwareLocation,
move_type: _GripperMoveType,
current_labware: LabwareDefinition,
) -> LabwareOffsetVector:
"""Get the total of the offsets to be used to pick up labware in its current location."""
if move_type == _GripperMoveType.PICK_UP_LABWARE:
Expand All @@ -1054,14 +1064,39 @@ def get_total_nominal_gripper_offset_for_move_type(
location
)
ancestor = self._labware.get_parent_location(location.labwareId)
extra_offset = LabwareOffsetVector(x=0, y=0, z=0)
if (
isinstance(ancestor, ModuleLocation)
and self._modules._state.requested_model_by_id[ancestor.moduleId]
== ModuleModel.THERMOCYCLER_MODULE_V2
and labware_validation.validate_definition_is_lid(current_labware)
):
if "lidOffsets" in current_labware.gripperOffsets.keys():
extra_offset = LabwareOffsetVector(
x=current_labware.gripperOffsets[
"lidOffsets"
].pickUpOffset.x,
y=current_labware.gripperOffsets[
"lidOffsets"
].pickUpOffset.y,
z=current_labware.gripperOffsets[
"lidOffsets"
].pickUpOffset.z,
)
else:
raise errors.LabwareOffsetDoesNotExistError(
f"Labware Definition {current_labware.parameters.loadName} does not contain required field 'lidOffsets' of 'gripperOffsets'."
)

assert isinstance(
ancestor, (DeckSlotLocation, ModuleLocation)
ancestor, (DeckSlotLocation, ModuleLocation, OnLabwareLocation)
), "No gripper offsets for off-deck labware"
return (
direct_parent_offset.pickUpOffset
+ self._nominal_gripper_offsets_for_location(
location=ancestor
).pickUpOffset
+ extra_offset
)
else:
if isinstance(
Expand All @@ -1076,14 +1111,39 @@ def get_total_nominal_gripper_offset_for_move_type(
location
)
ancestor = self._labware.get_parent_location(location.labwareId)
extra_offset = LabwareOffsetVector(x=0, y=0, z=0)
if (
isinstance(ancestor, ModuleLocation)
and self._modules._state.requested_model_by_id[ancestor.moduleId]
== ModuleModel.THERMOCYCLER_MODULE_V2
and labware_validation.validate_definition_is_lid(current_labware)
):
if "lidOffsets" in current_labware.gripperOffsets.keys():
extra_offset = LabwareOffsetVector(
x=current_labware.gripperOffsets[
"lidOffsets"
].pickUpOffset.x,
y=current_labware.gripperOffsets[
"lidOffsets"
].pickUpOffset.y,
z=current_labware.gripperOffsets[
"lidOffsets"
].pickUpOffset.z,
)
else:
raise errors.LabwareOffsetDoesNotExistError(
f"Labware Definition {current_labware.parameters.loadName} does not contain required field 'lidOffsets' of 'gripperOffsets'."
)

assert isinstance(
ancestor, (DeckSlotLocation, ModuleLocation)
ancestor, (DeckSlotLocation, ModuleLocation, OnLabwareLocation)
), "No gripper offsets for off-deck labware"
return (
direct_parent_offset.dropOffset
+ self._nominal_gripper_offsets_for_location(
location=ancestor
).dropOffset
+ extra_offset
)

def check_gripper_labware_tip_collision(
Expand Down Expand Up @@ -1147,11 +1207,20 @@ def _labware_gripper_offsets(
"""
parent_location = self._labware.get_parent_location(labware_id)
assert isinstance(
parent_location, (DeckSlotLocation, ModuleLocation)
parent_location,
(
DeckSlotLocation,
ModuleLocation,
AddressableAreaLocation,
),
), "No gripper offsets for off-deck labware"

if isinstance(parent_location, DeckSlotLocation):
slot_name = parent_location.slotName
elif isinstance(parent_location, AddressableAreaLocation):
slot_name = self._addressable_areas.get_addressable_area_base_slot(
parent_location.addressableAreaName
)
else:
module_loc = self._modules.get_location(parent_location.moduleId)
slot_name = module_loc.slotName
Expand Down
66 changes: 61 additions & 5 deletions api/src/opentrons/protocol_engine/state/labware.py
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,16 @@ def get_parent_location(self, labware_id: str) -> NonStackedLocation:
return self.get_parent_location(parent.labwareId)
return parent

def get_labware_stack(
self, labware_stack: List[LoadedLabware]
) -> List[LoadedLabware]:
"""Get the a stack of labware starting from a given labware or existing stack."""
parent = self.get_location(labware_stack[-1].id)
if isinstance(parent, OnLabwareLocation):
labware_stack.append(self.get(parent.labwareId))
return self.get_labware_stack(labware_stack)
return labware_stack

def get_all(self) -> List[LoadedLabware]:
"""Get a list of all labware entries in state."""
return list(self._state.labware_by_id.values())
Expand All @@ -429,6 +439,27 @@ def get_should_center_column_on_target_well(self, labware_id: str) -> bool:
and len(self.get_definition(labware_id).wells) < 96
)

def get_labware_stacking_maximum(self, labware: LabwareDefinition) -> int:
"""Returns the maximum number of labware allowed in a stack for a given labware definition.
If not defined within a labware, defaults to one.
"""
stacking_quirks = {
"stackingMaxFive": 5,
"stackingMaxFour": 4,
"stackingMaxThree": 3,
"stackingMaxTwo": 2,
"stackingMaxOne": 1,
"stackingMaxZero": 0,
}
for quirk in stacking_quirks.keys():
if (
labware.parameters.quirks is not None
and quirk in labware.parameters.quirks
):
return stacking_quirks[quirk]
return 1

def get_should_center_pipette_on_target_well(self, labware_id: str) -> bool:
"""True if a pipette moving to a well of this labware should center its body on the target.
Expand Down Expand Up @@ -596,9 +627,14 @@ def get_labware_overlap_offsets(
) -> OverlapOffset:
"""Get the labware's overlap with requested labware's load name."""
definition = self.get_definition(labware_id)
stacking_overlap = definition.stackingOffsetWithLabware.get(
below_labware_name, OverlapOffset(x=0, y=0, z=0)
)
if below_labware_name in definition.stackingOffsetWithLabware.keys():
stacking_overlap = definition.stackingOffsetWithLabware.get(
below_labware_name, OverlapOffset(x=0, y=0, z=0)
)
else:
stacking_overlap = definition.stackingOffsetWithLabware.get(
"default", OverlapOffset(x=0, y=0, z=0)
)
return OverlapOffset(
x=stacking_overlap.x, y=stacking_overlap.y, z=stacking_overlap.z
)
Expand Down Expand Up @@ -767,7 +803,7 @@ def raise_if_labware_in_location(
f"Labware {labware.loadName} is already present at {location}."
)

def raise_if_labware_cannot_be_stacked(
def raise_if_labware_cannot_be_stacked( # noqa: C901
self, top_labware_definition: LabwareDefinition, bottom_labware_id: str
) -> None:
"""Raise if the specified labware definition cannot be placed on top of the bottom labware."""
Expand All @@ -786,17 +822,37 @@ def raise_if_labware_cannot_be_stacked(
)
elif isinstance(below_labware.location, ModuleLocation):
below_definition = self.get_definition(labware_id=below_labware.id)
if not labware_validation.validate_definition_is_adapter(below_definition):
if not labware_validation.validate_definition_is_adapter(
below_definition
) and not labware_validation.validate_definition_is_lid(
top_labware_definition
):
raise errors.LabwareCannotBeStackedError(
f"Labware {top_labware_definition.parameters.loadName} cannot be loaded"
f" onto a labware on top of a module"
)
elif isinstance(below_labware.location, OnLabwareLocation):
labware_stack = self.get_labware_stack([below_labware])
stack_without_adapters = []
for lw in labware_stack:
if not labware_validation.validate_definition_is_adapter(
self.get_definition(lw.id)
):
stack_without_adapters.append(lw)
if len(stack_without_adapters) >= self.get_labware_stacking_maximum(
top_labware_definition
):
raise errors.LabwareCannotBeStackedError(
f"Labware {top_labware_definition.parameters.loadName} cannot be loaded to stack of more than {self.get_labware_stacking_maximum(top_labware_definition)} labware."
)

further_below_definition = self.get_definition(
labware_id=below_labware.location.labwareId
)
if labware_validation.validate_definition_is_adapter(
further_below_definition
) and not labware_validation.validate_definition_is_lid(
top_labware_definition
):
raise errors.LabwareCannotBeStackedError(
f"Labware {top_labware_definition.parameters.loadName} cannot be loaded"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -202,11 +202,16 @@ async def test_raise_error_if_gripper_pickup_failed(
)
).then_return(mock_tc_context_manager)

current_labware = state_store.labware.get_definition(
labware_id="my-teleporting-labware"
)

decoy.when(
state_store.geometry.get_final_labware_movement_offset_vectors(
from_location=starting_location,
to_location=to_location,
additional_offset_vector=user_offset_data,
current_labware=current_labware,
)
).then_return(final_offset_data)

Expand Down Expand Up @@ -316,12 +321,15 @@ async def test_move_labware_with_gripper(
await set_up_decoy_hardware_gripper(decoy, ot3_hardware_api, state_store)

user_offset_data, final_offset_data = hardware_gripper_offset_data

current_labware = state_store.labware.get_definition(
labware_id="my-teleporting-labware"
)
decoy.when(
state_store.geometry.get_final_labware_movement_offset_vectors(
from_location=from_location,
to_location=to_location,
additional_offset_vector=user_offset_data,
current_labware=current_labware,
)
).then_return(final_offset_data)

Expand Down
Loading

0 comments on commit eeb8972

Please sign in to comment.