From eeb8972ab63e5c53df8f03b4193cc0748f1043b9 Mon Sep 17 00:00:00 2001 From: CaseyBatten Date: Thu, 10 Oct 2024 11:07:25 -0400 Subject: [PATCH] feat(api, shared-data): Add support for labware lids and publish tc lid 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. --- .../execution/labware_movement.py | 2 + .../resources/labware_validation.py | 5 + .../protocol_engine/state/geometry.py | 83 ++++++- .../protocol_engine/state/labware.py | 66 +++++- .../test_labware_movement_handler.py | 10 +- .../state/test_geometry_view.py | 31 +++ .../state/test_labware_view.py | 93 +++++++- .../LabwarePositionCheck/useLaunchLPC.tsx | 15 +- .../protocols/tc_auto_seal_lid/__init__.py | 1 + .../tc_auto_seal_lid/tc_lid_evap_test.py | 208 ++++++++++++++++++ .../tc_auto_seal_lid/tc_lid_movement.py | 73 ++++++ .../js/__tests__/labwareDefQuirks.test.ts | 1 + .../1.json | 93 ++++++++ shared-data/labware/schemas/2.json | 2 +- shared-data/labware/schemas/3.json | 2 +- .../labware/labware_definition.py | 1 + .../opentrons_shared_data/labware/types.py | 1 + 17 files changed, 664 insertions(+), 23 deletions(-) create mode 100644 hardware-testing/hardware_testing/protocols/tc_auto_seal_lid/__init__.py create mode 100644 hardware-testing/hardware_testing/protocols/tc_auto_seal_lid/tc_lid_evap_test.py create mode 100644 hardware-testing/hardware_testing/protocols/tc_auto_seal_lid/tc_lid_movement.py create mode 100644 shared-data/labware/definitions/2/opentrons_tough_pcr_auto_sealing_lid/1.json diff --git a/api/src/opentrons/protocol_engine/execution/labware_movement.py b/api/src/opentrons/protocol_engine/execution/labware_movement.py index 30feb6517ff..8ede6f6085b 100644 --- a/api/src/opentrons/protocol_engine/execution/labware_movement.py +++ b/api/src/opentrons/protocol_engine/execution/labware_movement.py @@ -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 ): @@ -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( diff --git a/api/src/opentrons/protocol_engine/resources/labware_validation.py b/api/src/opentrons/protocol_engine/resources/labware_validation.py index 3b4ed14166c..090723ffb7e 100644 --- a/api/src/opentrons/protocol_engine/resources/labware_validation.py +++ b/api/src/opentrons/protocol_engine/resources/labware_validation.py @@ -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: diff --git a/api/src/opentrons/protocol_engine/state/geometry.py b/api/src/opentrons/protocol_engine/state/geometry.py index 502f0d4d8eb..e37a460d226 100644 --- a/api/src/opentrons/protocol_engine/state/geometry.py +++ b/api/src/opentrons/protocol_engine/state/geometry.py @@ -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 ( @@ -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, @@ -46,6 +47,7 @@ AddressableOffsetVector, StagingSlotLocation, LabwareOffsetLocation, + ModuleModel, ) from .config import Config from .labware import LabwareView @@ -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 ) @@ -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: @@ -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( @@ -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( @@ -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 diff --git a/api/src/opentrons/protocol_engine/state/labware.py b/api/src/opentrons/protocol_engine/state/labware.py index 0db6b310e1e..78f2124bdb4 100644 --- a/api/src/opentrons/protocol_engine/state/labware.py +++ b/api/src/opentrons/protocol_engine/state/labware.py @@ -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()) @@ -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. @@ -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 ) @@ -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.""" @@ -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" diff --git a/api/tests/opentrons/protocol_engine/execution/test_labware_movement_handler.py b/api/tests/opentrons/protocol_engine/execution/test_labware_movement_handler.py index c434995ee52..6032bad81b8 100644 --- a/api/tests/opentrons/protocol_engine/execution/test_labware_movement_handler.py +++ b/api/tests/opentrons/protocol_engine/execution/test_labware_movement_handler.py @@ -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) @@ -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) diff --git a/api/tests/opentrons/protocol_engine/state/test_geometry_view.py b/api/tests/opentrons/protocol_engine/state/test_geometry_view.py index 427dececa7b..3f824da7193 100644 --- a/api/tests/opentrons/protocol_engine/state/test_geometry_view.py +++ b/api/tests/opentrons/protocol_engine/state/test_geometry_view.py @@ -2233,6 +2233,7 @@ def test_get_final_labware_movement_offset_vectors( mock_module_view: ModuleView, mock_labware_view: LabwareView, subject: GeometryView, + well_plate_def: LabwareDefinition, ) -> None: """It should provide the final labware movement offset data based on locations.""" decoy.when(mock_labware_view.get_deck_default_gripper_offsets()).then_return( @@ -2248,6 +2249,10 @@ def test_get_final_labware_movement_offset_vectors( ) ) + decoy.when(mock_labware_view.get_definition("labware-id")).then_return( + well_plate_def + ) + final_offsets = subject.get_final_labware_movement_offset_vectors( from_location=DeckSlotLocation(slotName=DeckSlotName("D2")), to_location=ModuleLocation(moduleId="module-id"), @@ -2255,6 +2260,7 @@ def test_get_final_labware_movement_offset_vectors( pickUpOffset=LabwareOffsetVector(x=100, y=200, z=300), dropOffset=LabwareOffsetVector(x=400, y=500, z=600), ), + current_labware=mock_labware_view.get_definition("labware-id"), ) assert final_offsets == LabwareMovementOffsetData( pickUpOffset=LabwareOffsetVector(x=101, y=202, z=303), @@ -2285,6 +2291,7 @@ def test_get_total_nominal_gripper_offset( mock_labware_view: LabwareView, mock_module_view: ModuleView, subject: GeometryView, + well_plate_def: LabwareDefinition, ) -> None: """It should calculate the correct gripper offsets given the location and move type..""" decoy.when(mock_labware_view.get_deck_default_gripper_offsets()).then_return( @@ -2301,10 +2308,15 @@ def test_get_total_nominal_gripper_offset( ) ) + decoy.when(mock_labware_view.get_definition("labware-id")).then_return( + well_plate_def + ) + # Case 1: labware on deck result1 = subject.get_total_nominal_gripper_offset_for_move_type( location=DeckSlotLocation(slotName=DeckSlotName.SLOT_3), move_type=_GripperMoveType.PICK_UP_LABWARE, + current_labware=mock_labware_view.get_definition("labware-d"), ) assert result1 == LabwareOffsetVector(x=1, y=2, z=3) @@ -2312,6 +2324,7 @@ def test_get_total_nominal_gripper_offset( result2 = subject.get_total_nominal_gripper_offset_for_move_type( location=ModuleLocation(moduleId="module-id"), move_type=_GripperMoveType.DROP_LABWARE, + current_labware=mock_labware_view.get_definition("labware-id"), ) assert result2 == LabwareOffsetVector(x=33, y=22, z=11) @@ -2321,6 +2334,7 @@ def test_get_stacked_labware_total_nominal_offset_slot_specific( mock_labware_view: LabwareView, mock_module_view: ModuleView, subject: GeometryView, + well_plate_def: LabwareDefinition, ) -> None: """Get nominal offset for stacked labware.""" # Case: labware on adapter on module, adapter has slot-specific offsets @@ -2346,15 +2360,23 @@ def test_get_stacked_labware_total_nominal_offset_slot_specific( decoy.when(mock_labware_view.get_parent_location("adapter-id")).then_return( ModuleLocation(moduleId="module-id") ) + decoy.when(mock_labware_view.get_definition("labware-id")).then_return( + well_plate_def + ) + decoy.when(mock_module_view._state.requested_model_by_id).then_return( + {"module-id": ModuleModel.HEATER_SHAKER_MODULE_V1} + ) result1 = subject.get_total_nominal_gripper_offset_for_move_type( location=OnLabwareLocation(labwareId="adapter-id"), move_type=_GripperMoveType.PICK_UP_LABWARE, + current_labware=mock_labware_view.get_definition("labware-id"), ) assert result1 == LabwareOffsetVector(x=111, y=222, z=333) result2 = subject.get_total_nominal_gripper_offset_for_move_type( location=OnLabwareLocation(labwareId="adapter-id"), move_type=_GripperMoveType.DROP_LABWARE, + current_labware=mock_labware_view.get_definition("labware-id"), ) assert result2 == LabwareOffsetVector(x=333, y=222, z=111) @@ -2364,6 +2386,7 @@ def test_get_stacked_labware_total_nominal_offset_default( mock_labware_view: LabwareView, mock_module_view: ModuleView, subject: GeometryView, + well_plate_def: LabwareDefinition, ) -> None: """Get nominal offset for stacked labware.""" # Case: labware on adapter on module, adapter has only default offsets @@ -2394,15 +2417,23 @@ def test_get_stacked_labware_total_nominal_offset_default( decoy.when(mock_labware_view.get_parent_location("adapter-id")).then_return( ModuleLocation(moduleId="module-id") ) + decoy.when(mock_labware_view.get_definition("labware-id")).then_return( + well_plate_def + ) + decoy.when(mock_module_view._state.requested_model_by_id).then_return( + {"module-id": ModuleModel.HEATER_SHAKER_MODULE_V1} + ) result1 = subject.get_total_nominal_gripper_offset_for_move_type( location=OnLabwareLocation(labwareId="adapter-id"), move_type=_GripperMoveType.PICK_UP_LABWARE, + current_labware=mock_labware_view.get_definition("labware-id"), ) assert result1 == LabwareOffsetVector(x=111, y=222, z=333) result2 = subject.get_total_nominal_gripper_offset_for_move_type( location=OnLabwareLocation(labwareId="adapter-id"), move_type=_GripperMoveType.DROP_LABWARE, + current_labware=mock_labware_view.get_definition("labware-id"), ) assert result2 == LabwareOffsetVector(x=333, y=222, z=111) diff --git a/api/tests/opentrons/protocol_engine/state/test_labware_view.py b/api/tests/opentrons/protocol_engine/state/test_labware_view.py index ab2f49cfb29..d461ddda4e6 100644 --- a/api/tests/opentrons/protocol_engine/state/test_labware_view.py +++ b/api/tests/opentrons/protocol_engine/state/test_labware_view.py @@ -1386,13 +1386,18 @@ def test_raise_if_labware_cannot_be_stacked_on_labware_on_adapter() -> None: ), }, definitions_by_uri={ + "def-uri-1": LabwareDefinition.construct( # type: ignore[call-arg] + allowedRoles=[LabwareRole.labware] + ), "def-uri-2": LabwareDefinition.construct( # type: ignore[call-arg] allowedRoles=[LabwareRole.adapter] - ) + ), }, ) - with pytest.raises(errors.LabwareCannotBeStackedError, match="on top of adapter"): + with pytest.raises( + errors.LabwareCannotBeStackedError, match="cannot be loaded to stack" + ): subject.raise_if_labware_cannot_be_stacked( top_labware_definition=LabwareDefinition.construct( # type: ignore[call-arg] parameters=Parameters.construct( # type: ignore[call-arg] @@ -1406,6 +1411,90 @@ def test_raise_if_labware_cannot_be_stacked_on_labware_on_adapter() -> None: ) +@pytest.mark.parametrize( + argnames=[ + "allowed_roles", + "stacking_quirks", + "exception", + ], + argvalues=[ + [ + [LabwareRole.labware], + [], + pytest.raises(errors.LabwareCannotBeStackedError), + ], + [ + [LabwareRole.lid], + ["stackingMaxFive"], + does_not_raise(), + ], + ], +) +def test_labware_stacking_height_passes_or_raises( + allowed_roles: List[LabwareRole], + stacking_quirks: List[str], + exception: ContextManager[None], +) -> None: + """It should raise if the labware is stacked too high, and pass if the labware definition allowed this.""" + subject = get_labware_view( + labware_by_id={ + "labware-id4": LoadedLabware( + id="labware-id4", + loadName="test", + definitionUri="def-uri-1", + location=OnLabwareLocation(labwareId="labware-id3"), + ), + "labware-id3": LoadedLabware( + id="labware-id3", + loadName="test", + definitionUri="def-uri-1", + location=OnLabwareLocation(labwareId="labware-id2"), + ), + "labware-id2": LoadedLabware( + id="labware-id2", + loadName="test", + definitionUri="def-uri-1", + location=OnLabwareLocation(labwareId="labware-id1"), + ), + "labware-id1": LoadedLabware( + id="labware-id1", + loadName="test", + definitionUri="def-uri-1", + location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), + ), + }, + definitions_by_uri={ + "def-uri-1": LabwareDefinition.construct( # type: ignore[call-arg] + allowedRoles=allowed_roles, + parameters=Parameters.construct( + format="irregular", + quirks=stacking_quirks, + isTiprack=False, + loadName="name", + isMagneticModuleCompatible=False, + ), + ) + }, + ) + + with exception: + subject.raise_if_labware_cannot_be_stacked( + top_labware_definition=LabwareDefinition.construct( # type: ignore[call-arg] + parameters=Parameters.construct( + format="irregular", + quirks=stacking_quirks, + isTiprack=False, + loadName="name", + isMagneticModuleCompatible=False, + ), + stackingOffsetWithLabware={ + "test": SharedDataOverlapOffset(x=0, y=0, z=0) + }, + ), + bottom_labware_id="labware-id4", + ) + + def test_get_deck_gripper_offsets(ot3_standard_deck_def: DeckDefinitionV5) -> None: """It should get the deck's gripper offsets.""" subject = get_labware_view(deck_definition=ot3_standard_deck_def) diff --git a/app/src/organisms/LabwarePositionCheck/useLaunchLPC.tsx b/app/src/organisms/LabwarePositionCheck/useLaunchLPC.tsx index c3262af8225..0ad5ea06a50 100644 --- a/app/src/organisms/LabwarePositionCheck/useLaunchLPC.tsx +++ b/app/src/organisms/LabwarePositionCheck/useLaunchLPC.tsx @@ -15,6 +15,8 @@ import { getLabwareDefinitionsFromCommands } from '/app/molecules/Command/utils/ import type { RobotType } from '@opentrons/shared-data' +const filteredLabware = ['opentrons_tough_pcr_auto_sealing_lid'] + export function useLaunchLPC( runId: string, robotType: RobotType, @@ -61,12 +63,13 @@ export function useLaunchLPC( Promise.all( getLabwareDefinitionsFromCommands( mostRecentAnalysis?.commands ?? [] - ).map(def => - createLabwareDefinition({ - maintenanceRunId: maintenanceRun?.data?.id, - labwareDef: def, - }) - ) + ).map(def => { + if (!filteredLabware.includes(def.parameters.loadName)) + createLabwareDefinition({ + maintenanceRunId: maintenanceRun?.data?.id, + labwareDef: def, + }) + }) ).then(() => { setMaintenanceRunId(maintenanceRun.data.id) }) diff --git a/hardware-testing/hardware_testing/protocols/tc_auto_seal_lid/__init__.py b/hardware-testing/hardware_testing/protocols/tc_auto_seal_lid/__init__.py new file mode 100644 index 00000000000..848fb967ae2 --- /dev/null +++ b/hardware-testing/hardware_testing/protocols/tc_auto_seal_lid/__init__.py @@ -0,0 +1 @@ +"""Tough Auto Sealing Lid Tests.""" diff --git a/hardware-testing/hardware_testing/protocols/tc_auto_seal_lid/tc_lid_evap_test.py b/hardware-testing/hardware_testing/protocols/tc_auto_seal_lid/tc_lid_evap_test.py new file mode 100644 index 00000000000..5a02624f08f --- /dev/null +++ b/hardware-testing/hardware_testing/protocols/tc_auto_seal_lid/tc_lid_evap_test.py @@ -0,0 +1,208 @@ +"""Protocol to Test Evaporation % of the Tough Auto Seal Lid.""" +from typing import List +from opentrons.hardware_control.modules.types import ThermocyclerStep +from opentrons.protocol_api import ( + ParameterContext, + ProtocolContext, + Labware, + InstrumentContext, + Well, +) +from opentrons.protocol_api.module_contexts import ThermocyclerContext +from opentrons.protocol_api.disposal_locations import WasteChute + +metadata = {"protocolName": "Tough Auto Seal Lid Evaporation Test"} +requirements = {"robotType": "Flex", "apiLevel": "2.20"} + + +def _long_hold_test(thermocycler: ThermocyclerContext, tc_lid_temp: float) -> None: + """Holds TC lid in Thermocycler for 5 min at high temp before evap test.""" + thermocycler.set_block_temperature(4, hold_time_minutes=5) + thermocycler.set_lid_temperature(tc_lid_temp) + thermocycler.set_block_temperature(98, hold_time_minutes=5) + thermocycler.set_block_temperature(4, hold_time_minutes=5) + thermocycler.open_lid() + + +def _fill_with_liquid_and_measure( + protocol: ProtocolContext, + pipette: InstrumentContext, + reservoir: Labware, + plate_in_cycler: Labware, +) -> None: + """Fill plate with 10 ul per well.""" + locations: List[Well] = [ + plate_in_cycler["A1"], + plate_in_cycler["A2"], + plate_in_cycler["A3"], + plate_in_cycler["A4"], + plate_in_cycler["A5"], + plate_in_cycler["A6"], + plate_in_cycler["A7"], + plate_in_cycler["A8"], + plate_in_cycler["A9"], + plate_in_cycler["A10"], + plate_in_cycler["A11"], + plate_in_cycler["A12"], + ] + volumes = [10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10] + protocol.pause("Weight Armadillo Plate, place on thermocycler") + # pipette 10uL into Armadillo wells + source_well: Well = reservoir["A1"] + pipette.distribute( + volume=volumes, + source=source_well, + dest=locations, + return_tips=True, + blow_out=False, + ) + protocol.pause("Weight Armadillo Plate, place on thermocycler, put on lid") + + +def _pcr_cycle(thermocycler: ThermocyclerContext) -> None: + """30x cycles of: 70° for 30s 72° for 30s 95° for 10s.""" + profile_TAG2: List[ThermocyclerStep] = [ + {"temperature": 70, "hold_time_seconds": 30}, + {"temperature": 72, "hold_time_seconds": 30}, + {"temperature": 95, "hold_time_seconds": 10}, + ] + thermocycler.execute_profile( + steps=profile_TAG2, repetitions=30, block_max_volume=50 + ) + + +def _move_lid( + thermocycler: ThermocyclerContext, + protocol: ProtocolContext, + top_lid: Labware, + bottom_lid: Labware, + wasteChute: WasteChute, +) -> None: + """Move lid from tc to deck.""" + # Move lid from thermocycler to deck to stack to waste chute + thermocycler.open_lid() + # Move Lid to Deck + protocol.move_labware(top_lid, "B2", use_gripper=True) + # Move Lid to Stack + protocol.move_labware(top_lid, bottom_lid, use_gripper=True) + # Move Lid to Waste Chute + protocol.move_labware(top_lid, wasteChute, use_gripper=True) + + +def add_parameters(parameters: ParameterContext) -> None: + """Add parameters.""" + parameters.add_str( + variable_name="mount_pos", + display_name="Mount Position", + description="What mount to use", + choices=[ + {"display_name": "left_mount", "value": "left"}, + {"display_name": "right_mount", "value": "right"}, + ], + default="left", + ) + parameters.add_str( + variable_name="pipette_type", + display_name="Pipette Type", + description="What pipette to use", + choices=[ + {"display_name": "8ch 50 uL", "value": "flex_8channel_50"}, + {"display_name": "8ch 1000 uL", "value": "flex_8channel_1000"}, + ], + default="flex_8channel_50", + ) + parameters.add_float( + variable_name="tc_lid_temp", + display_name="TC Lid Temp", + description="Max temp of TC Lid", + default=105, + choices=[ + {"display_name": "105", "value": 105}, + {"display_name": "107", "value": 107}, + {"display_name": "110", "value": 110}, + ], + ) + parameters.add_str( + variable_name="test_type", + display_name="Test Type", + description="Type of test to run", + default="evap_test", + choices=[ + {"display_name": "Evaporation Test", "value": "evap_test"}, + {"display_name": "Long Hold Test", "value": "long_hold_test"}, + ], + ) + + +def run(protocol: ProtocolContext) -> None: + """Run protocol.""" + # LOAD PARAMETERS + pipette_type = protocol.params.pipette_type # type: ignore[attr-defined] + mount_position = protocol.params.mount_pos # type: ignore[attr-defined] + tc_lid_temp = protocol.params.tc_lid_temp # type: ignore[attr-defined] + test_type = protocol.params.test_type # type: ignore[attr-defined] + # SETUP + # Thermocycler + thermocycler: ThermocyclerContext = protocol.load_module( + "thermocyclerModuleV2" + ) # type: ignore[assignment] + + plate_in_cycler = thermocycler.load_labware( + "armadillo_96_wellplate_200ul_pcr_full_skirt" + ) + thermocycler.open_lid() + # Labware + tiprack_50_1 = protocol.load_labware("opentrons_flex_96_tiprack_50ul", "C3") + reservoir = protocol.load_labware("nest_12_reservoir_15ml", "A2") + lids: List[Labware] = [ + protocol.load_labware("opentrons_tough_pcr_auto_sealing_lid", "D2") + ] + for i in range(4): + lids.append(lids[-1].load_labware("opentrons_tough_pcr_auto_sealing_lid")) + lids.reverse() + top_lid = lids[0] + bottom_lid = lids[1] + # Pipette + pipette = protocol.load_instrument( + pipette_type, mount_position, tip_racks=[tiprack_50_1] + ) + # Waste Chute + wasteChute = protocol.load_waste_chute() + + # DEFINE TESTS # + thermocycler.set_block_temperature(4) + thermocycler.set_lid_temperature(105) + + # hold at 95° for 3 minutes + profile_TAG: List[ThermocyclerStep] = [{"temperature": 95, "hold_time_minutes": 3}] + # hold at 72° for 5min + profile_TAG3: List[ThermocyclerStep] = [{"temperature": 72, "hold_time_minutes": 5}] + + if test_type == "long_hold_test": + protocol.move_labware(top_lid, plate_in_cycler, use_gripper=True) + _long_hold_test(thermocycler, tc_lid_temp) + protocol.move_labware(top_lid, "B2", use_gripper=True) + _long_hold_test(thermocycler, tc_lid_temp) + _fill_with_liquid_and_measure(protocol, pipette, reservoir, plate_in_cycler) + thermocycler.close_lid() + _pcr_cycle(thermocycler) + + # Go through PCR cycle + if test_type == "evap_test": + _fill_with_liquid_and_measure(protocol, pipette, reservoir, plate_in_cycler) + protocol.move_labware(top_lid, plate_in_cycler, use_gripper=True) + thermocycler.close_lid() + thermocycler.execute_profile( + steps=profile_TAG, repetitions=1, block_max_volume=50 + ) + _pcr_cycle(thermocycler) + thermocycler.execute_profile( + steps=profile_TAG3, repetitions=1, block_max_volume=50 + ) + # # # Cool to 4° + thermocycler.set_block_temperature(4) + thermocycler.set_lid_temperature(tc_lid_temp) + # Open lid + thermocycler.open_lid() + _move_lid(thermocycler, protocol, top_lid, bottom_lid, wasteChute) + protocol.pause("Weigh armadillo plate.") diff --git a/hardware-testing/hardware_testing/protocols/tc_auto_seal_lid/tc_lid_movement.py b/hardware-testing/hardware_testing/protocols/tc_auto_seal_lid/tc_lid_movement.py new file mode 100644 index 00000000000..475c84e6516 --- /dev/null +++ b/hardware-testing/hardware_testing/protocols/tc_auto_seal_lid/tc_lid_movement.py @@ -0,0 +1,73 @@ +"""Protocol to Test the Stacking and Movement of Tough Auto Seal Lid.""" +from typing import List, Union +from opentrons.protocol_api import ( + ParameterContext, + ProtocolContext, + Labware, +) +from opentrons.protocol_api.module_contexts import ThermocyclerContext + + +metadata = {"protocolName": "Tough Auto Seal Lid Stacking Test"} +requirements = {"robotType": "Flex", "apiLevel": "2.20"} + + +def add_parameters(parameters: ParameterContext) -> None: + """Add parameters.""" + parameters.add_int( + variable_name="num_of_stack_ups", + display_name="Number of Stack Ups", + choices=[ + {"display_name": "1", "value": 1}, + {"display_name": "10", "value": 10}, + {"display_name": "20", "value": 20}, + {"display_name": "30", "value": 30}, + {"display_name": "40", "value": 40}, + ], + default=20, + ) + + +def run(protocol: ProtocolContext) -> None: + """Runs protocol that moves lids and stacks them.""" + # Load Parameters + iterations = protocol.params.num_of_stack_ups # type: ignore[attr-defined] + # Thermocycler + thermocycler: ThermocyclerContext = protocol.load_module( + "thermocyclerModuleV2" + ) # type: ignore[assignment] + plate_in_cycler = thermocycler.load_labware( + "armadillo_96_wellplate_200ul_pcr_full_skirt" + ) + thermocycler.open_lid() + + lids: List[Labware] = [ + protocol.load_labware("opentrons_tough_pcr_auto_sealing_lid", "D2") + ] + for i in range(4): + lids.append(lids[-1].load_labware("opentrons_tough_pcr_auto_sealing_lid")) + lids.reverse() + stack_locations = ["C2", "D2"] + slot = 0 + for iteration in range(iterations - 1): + protocol.comment(f"Stack up {iteration}") + locations_for_lid = ["D1", "C1", "C3", "B2", "B3"] + loc = 0 + for lid in lids: + # move lid to plate in thermocycler + protocol.move_labware(lid, plate_in_cycler, use_gripper=True) + # move lid to deck slot + location_to_move: Union[int, str] = locations_for_lid[loc] + protocol.move_labware(lid, location_to_move, use_gripper=True) + # move lid to lid stack + if loc == 0: + protocol.move_labware(lid, stack_locations[slot], use_gripper=True) + prev_moved_lid: Labware = lid + else: + protocol.move_labware(lid, prev_moved_lid, use_gripper=True) + prev_moved_lid = lid + loc += 1 + slot = (slot + 1) % 2 # Switch between 0 and 1 to rotate stack locations + + # reverse lid list to restart stacking exercise + lids.reverse() diff --git a/shared-data/js/__tests__/labwareDefQuirks.test.ts b/shared-data/js/__tests__/labwareDefQuirks.test.ts index 6ebc39f9f17..6251c894647 100644 --- a/shared-data/js/__tests__/labwareDefQuirks.test.ts +++ b/shared-data/js/__tests__/labwareDefQuirks.test.ts @@ -13,6 +13,7 @@ const EXPECTED_VALID_QUIRKS = [ 'fixedTrash', 'gripperIncompatible', 'tiprackAdapterFor96Channel', + 'stackingMaxFive', ] describe('check quirks for all labware defs', () => { diff --git a/shared-data/labware/definitions/2/opentrons_tough_pcr_auto_sealing_lid/1.json b/shared-data/labware/definitions/2/opentrons_tough_pcr_auto_sealing_lid/1.json new file mode 100644 index 00000000000..ca39c122b47 --- /dev/null +++ b/shared-data/labware/definitions/2/opentrons_tough_pcr_auto_sealing_lid/1.json @@ -0,0 +1,93 @@ +{ + "allowedRoles": ["labware", "lid"], + "ordering": [], + "brand": { + "brand": "Opentrons", + "brandId": [] + }, + "metadata": { + "displayName": "Opentrons Tough PCR Auto-Sealing Lid", + "displayCategory": "other", + "displayVolumeUnits": "\u00b5L", + "tags": [] + }, + "dimensions": { + "xDimension": 127.7, + "yDimension": 85.48, + "zDimension": 12.8 + }, + "wells": {}, + "groups": [], + "cornerOffsetFromSlot": { + "x": 0, + "y": 0, + "z": -0.71 + }, + "parameters": { + "format": "irregular", + "quirks": ["stackingMaxFive"], + "isTiprack": false, + "isMagneticModuleCompatible": false, + "loadName": "opentrons_tough_pcr_auto_sealing_lid" + }, + "namespace": "opentrons", + "version": 1, + "schemaVersion": 2, + "stackingOffsetWithModule": { + "thermocyclerModuleV2": { + "x": 0, + "y": 0, + "z": 0 + } + }, + "stackingOffsetWithLabware": { + "default": { + "x": 0, + "y": 0, + "z": 8.193 + }, + "opentrons_tough_pcr_auto_sealing_lid": { + "x": 0, + "y": 0, + "z": 6.492 + }, + "armadillo_96_wellplate_200ul_pcr_full_skirt": { + "x": 0, + "y": 0, + "z": 8.193 + }, + "opentrons_96_wellplate_200ul_pcr_full_skirt": { + "x": 0, + "y": 0, + "z": 8.193 + } + }, + "gripForce": 15, + "gripHeightFromLabwareBottom": 7.91, + "gripperOffsets": { + "default": { + "pickUpOffset": { + "x": 0, + "y": 0, + "z": 1.5 + }, + "dropOffset": { + "x": 0, + "y": 0.52, + "z": -6 + } + }, + "lidOffsets": { + "pickUpOffset": { + "x": 0.5, + "y": 0, + "z": -5 + }, + "dropOffset": { + "x": 0.5, + "y": 0, + "z": -1 + } + } + } +} diff --git a/shared-data/labware/schemas/2.json b/shared-data/labware/schemas/2.json index 01931d2c2a1..203009be9f5 100644 --- a/shared-data/labware/schemas/2.json +++ b/shared-data/labware/schemas/2.json @@ -323,7 +323,7 @@ "description": "Allowed behaviors and usage of a labware in a protocol.", "items": { "type": "string", - "enum": ["labware", "adapter", "fixture", "maintenance"] + "enum": ["labware", "adapter", "fixture", "maintenance", "lid"] } }, "stackingOffsetWithLabware": { diff --git a/shared-data/labware/schemas/3.json b/shared-data/labware/schemas/3.json index ecd285c554a..e38c070919a 100644 --- a/shared-data/labware/schemas/3.json +++ b/shared-data/labware/schemas/3.json @@ -519,7 +519,7 @@ "description": "Allowed behaviors and usage of a labware in a protocol.", "items": { "type": "string", - "enum": ["labware", "adapter", "fixture", "maintenance"] + "enum": ["labware", "adapter", "fixture", "maintenance", "lid"] } }, "stackingOffsetWithLabware": { diff --git a/shared-data/python/opentrons_shared_data/labware/labware_definition.py b/shared-data/python/opentrons_shared_data/labware/labware_definition.py index a818afc106a..be4c1a17d01 100644 --- a/shared-data/python/opentrons_shared_data/labware/labware_definition.py +++ b/shared-data/python/opentrons_shared_data/labware/labware_definition.py @@ -119,6 +119,7 @@ class LabwareRole(str, Enum): fixture = "fixture" adapter = "adapter" maintenance = "maintenance" + lid = "lid" class Metadata(BaseModel): diff --git a/shared-data/python/opentrons_shared_data/labware/types.py b/shared-data/python/opentrons_shared_data/labware/types.py index d3f6599848c..5a6aebf4ff7 100644 --- a/shared-data/python/opentrons_shared_data/labware/types.py +++ b/shared-data/python/opentrons_shared_data/labware/types.py @@ -37,6 +37,7 @@ Literal["fixture"], Literal["adapter"], Literal["maintenance"], + Literal["lid"], ]