diff --git a/api/release-notes-internal.md b/api/release-notes-internal.md index e55e1050c4c..ceaea9e390c 100644 --- a/api/release-notes-internal.md +++ b/api/release-notes-internal.md @@ -19,6 +19,7 @@ For more details about this release, please see the full [technical change log][ - Support for PVT (v1.1) grippers - Update progress should get displayed after restart for firmware updates - Removed `use_pick_up_location_lpc_offset` and `use_drop_location_lpc_offset` from `protocol_context.move_labware` arguments. So they should be removed from any protocols that used them. This change also requires resetting the protocol run database on the robot. +- Added 'contextual' gripper offsets to deck, labware and module definitions. So, any labware movement offsets that were previously being specified in the protocol should now be removed or adjusted or they will get added twice. ## Big Things That Don't Work Yet So Don't Report Bugs About Them diff --git a/api/src/opentrons/motion_planning/waypoints.py b/api/src/opentrons/motion_planning/waypoints.py index fc1690ea1de..0f3634e449d 100644 --- a/api/src/opentrons/motion_planning/waypoints.py +++ b/api/src/opentrons/motion_planning/waypoints.py @@ -128,8 +128,8 @@ def get_gripper_labware_movement_waypoints( offset_data: LabwareMovementOffsetData, ) -> List[GripperMovementWaypointsWithJawStatus]: """Get waypoints for moving labware using a gripper.""" - pick_up_offset = offset_data.pick_up_offset - drop_offset = offset_data.drop_offset + pick_up_offset = offset_data.pickUpOffset + drop_offset = offset_data.dropOffset pick_up_location = from_labware_center + Point( pick_up_offset.x, pick_up_offset.y, pick_up_offset.z diff --git a/api/src/opentrons/protocol_engine/commands/move_labware.py b/api/src/opentrons/protocol_engine/commands/move_labware.py index 250cca15c01..31d16feb11b 100644 --- a/api/src/opentrons/protocol_engine/commands/move_labware.py +++ b/api/src/opentrons/protocol_engine/commands/move_labware.py @@ -136,9 +136,8 @@ async def execute(self, params: MoveLabwareParams) -> MoveLabwareResult: available_new_location, ) user_offset_data = LabwareMovementOffsetData( - pick_up_offset=params.pickUpOffset - or LabwareOffsetVector(x=0, y=0, z=0), - drop_offset=params.dropOffset or LabwareOffsetVector(x=0, y=0, z=0), + pickUpOffset=params.pickUpOffset or LabwareOffsetVector(x=0, y=0, z=0), + dropOffset=params.dropOffset or LabwareOffsetVector(x=0, y=0, z=0), ) # Skips gripper moves when using virtual gripper await self._labware_movement.move_labware_with_gripper( diff --git a/api/src/opentrons/protocol_engine/execution/labware_movement.py b/api/src/opentrons/protocol_engine/execution/labware_movement.py index afb1028cec8..3c5cd2f42a6 100644 --- a/api/src/opentrons/protocol_engine/execution/labware_movement.py +++ b/api/src/opentrons/protocol_engine/execution/labware_movement.py @@ -1,7 +1,7 @@ """Labware movement command handling.""" from __future__ import annotations -from typing import Optional, Union, TYPE_CHECKING +from typing import Optional, TYPE_CHECKING from opentrons_shared_data.gripper.constants import ( LABWARE_GRIP_FORCE, IDLE_STATE_GRIP_FORCE, @@ -26,11 +26,10 @@ ) from ..types import ( - DeckSlotLocation, - ModuleLocation, OnLabwareLocation, LabwareLocation, LabwareMovementOffsetData, + OnDeckLabwareLocation, ) if TYPE_CHECKING: @@ -84,8 +83,8 @@ def __init__( async def move_labware_with_gripper( self, labware_id: str, - current_location: Union[DeckSlotLocation, ModuleLocation, OnLabwareLocation], - new_location: Union[DeckSlotLocation, ModuleLocation, OnLabwareLocation], + current_location: OnDeckLabwareLocation, + new_location: OnDeckLabwareLocation, user_offset_data: LabwareMovementOffsetData, ) -> None: """Move a loaded labware from one location to another using gripper.""" diff --git a/api/src/opentrons/protocol_engine/state/geometry.py b/api/src/opentrons/protocol_engine/state/geometry.py index 2464d1aa0ca..c028ac8b3f2 100644 --- a/api/src/opentrons/protocol_engine/state/geometry.py +++ b/api/src/opentrons/protocol_engine/state/geometry.py @@ -6,7 +6,6 @@ from opentrons_shared_data.labware.constants import WELL_NAME_PATTERN from .. import errors -from ..errors import LabwareMovementNotAllowedError from ..types import ( OFF_DECK_LOCATION, LoadedLabware, @@ -26,7 +25,7 @@ CurrentWell, TipGeometry, LabwareMovementOffsetData, - ModuleModel, + OnDeckLabwareLocation, ) from .config import Config from .labware import LabwareView @@ -38,7 +37,6 @@ SLOT_WIDTH = 128 -_ADDITIONAL_TC2_PICKUP_OFFSET = 3.5 class _TipDropSection(enum.Enum): @@ -48,6 +46,13 @@ class _TipDropSection(enum.Enum): RIGHT = "right" +class _GripperMoveType(enum.Enum): + """Types of gripper movement.""" + + PICK_UP_LABWARE = enum.auto() + DROP_LABWARE = enum.auto() + + # TODO(mc, 2021-06-03): continue evaluation of which selectors should go here # vs which selectors should be in LabwareView class GeometryView: @@ -623,27 +628,26 @@ def _get_drop_tip_well_x_offset( def get_final_labware_movement_offset_vectors( self, - from_location: LabwareLocation, - to_location: LabwareLocation, + from_location: OnDeckLabwareLocation, + to_location: OnDeckLabwareLocation, additional_offset_vector: LabwareMovementOffsetData, ) -> LabwareMovementOffsetData: """Calculate the final labware offset vector to use in labware movement.""" - # TODO (fps, 2022-05-30): Update this once RLAB-295 is merged - # Get location-based offsets from deck/module/adapter definitions, - # then add additional offsets - pick_up_offset = additional_offset_vector.pick_up_offset - drop_offset = additional_offset_vector.drop_offset - - if isinstance(from_location, ModuleLocation): - module_id = from_location.moduleId - if ( - self._modules.get_connected_model(module_id) - == ModuleModel.THERMOCYCLER_MODULE_V2 - ): - pick_up_offset.z += _ADDITIONAL_TC2_PICKUP_OFFSET + pick_up_offset = ( + self.get_total_nominal_gripper_offset_for_move_type( + location=from_location, move_type=_GripperMoveType.PICK_UP_LABWARE + ) + + additional_offset_vector.pickUpOffset + ) + drop_offset = ( + self.get_total_nominal_gripper_offset_for_move_type( + location=to_location, move_type=_GripperMoveType.DROP_LABWARE + ) + + additional_offset_vector.dropOffset + ) return LabwareMovementOffsetData( - pick_up_offset=pick_up_offset, drop_offset=drop_offset + pickUpOffset=pick_up_offset, dropOffset=drop_offset ) @staticmethod @@ -654,7 +658,98 @@ def ensure_valid_gripper_location( if not isinstance( location, (DeckSlotLocation, ModuleLocation, OnLabwareLocation) ): - raise LabwareMovementNotAllowedError( + raise errors.LabwareMovementNotAllowedError( "Off-deck labware movements are not supported using the gripper." ) return location + + def get_total_nominal_gripper_offset_for_move_type( + self, location: OnDeckLabwareLocation, move_type: _GripperMoveType + ) -> 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: + if isinstance(location, (ModuleLocation, DeckSlotLocation)): + return self._nominal_gripper_offsets_for_location(location).pickUpOffset + else: + # If it's a labware on a labware (most likely an adapter), + # we calculate the offset as sum of offsets for the direct parent labware + # and the underlying non-labware parent location. + direct_parent_offset = self._nominal_gripper_offsets_for_location( + location + ) + ancestor = self._labware.get_parent_location(location.labwareId) + assert isinstance( + ancestor, (DeckSlotLocation, ModuleLocation) + ), "No gripper offsets for off-deck labware" + return ( + direct_parent_offset.pickUpOffset + + self._nominal_gripper_offsets_for_location( + location=ancestor + ).pickUpOffset + ) + else: + if isinstance(location, (ModuleLocation, DeckSlotLocation)): + return self._nominal_gripper_offsets_for_location(location).dropOffset + else: + # If it's a labware on a labware (most likely an adapter), + # we calculate the offset as sum of offsets for the direct parent labware + # and the underlying non-labware parent location. + direct_parent_offset = self._nominal_gripper_offsets_for_location( + location + ) + ancestor = self._labware.get_parent_location(location.labwareId) + assert isinstance( + ancestor, (DeckSlotLocation, ModuleLocation) + ), "No gripper offsets for off-deck labware" + return ( + direct_parent_offset.dropOffset + + self._nominal_gripper_offsets_for_location( + location=ancestor + ).dropOffset + ) + + def _nominal_gripper_offsets_for_location( + self, location: OnDeckLabwareLocation + ) -> LabwareMovementOffsetData: + """Provide the default gripper offset data for the given location type.""" + if isinstance(location, DeckSlotLocation): + offsets = self._labware.get_deck_default_gripper_offsets() + elif isinstance(location, ModuleLocation): + offsets = self._modules.get_default_gripper_offsets(location.moduleId) + else: + # Labware is on a labware/adapter + offsets = self._labware_gripper_offsets(location.labwareId) + return offsets or LabwareMovementOffsetData( + pickUpOffset=LabwareOffsetVector(x=0, y=0, z=0), + dropOffset=LabwareOffsetVector(x=0, y=0, z=0), + ) + + def _labware_gripper_offsets( + self, labware_id: str + ) -> Optional[LabwareMovementOffsetData]: + """Provide the most appropriate gripper offset data for the specified labware. + + We check the types of gripper offsets available for the labware ("default" or slot-based) + and return the most appropriate one for the overall location of the labware. + Currently, only module adapters (specifically, the H/S universal flat adapter) + have non-default offsets that are specific to location of the module on deck, + so, this code only checks for the presence of those known offsets. + """ + parent_location = self._labware.get_parent_location(labware_id) + assert isinstance( + parent_location, (DeckSlotLocation, ModuleLocation) + ), "No gripper offsets for off-deck labware" + + if isinstance(parent_location, DeckSlotLocation): + slot_name = parent_location.slotName + else: + module_loc = self._modules.get_location(parent_location.moduleId) + slot_name = module_loc.slotName + + slot_based_offset = self._labware.get_labware_gripper_offsets( + labware_id=labware_id, slot_name=slot_name.to_ot3_equivalent() + ) + + return slot_based_offset or self._labware.get_labware_gripper_offsets( + labware_id=labware_id, slot_name=None + ) diff --git a/api/src/opentrons/protocol_engine/state/labware.py b/api/src/opentrons/protocol_engine/state/labware.py index e17446d0096..4ca64efdd5d 100644 --- a/api/src/opentrons/protocol_engine/state/labware.py +++ b/api/src/opentrons/protocol_engine/state/labware.py @@ -46,6 +46,7 @@ ModuleLocation, ModuleModel, OverlapOffset, + LabwareMovementOffsetData, ) from ..actions import ( Action, @@ -719,3 +720,46 @@ def _is_magnetic_module_uri_in_half_millimeter(self, labware_id: str) -> bool: """Check whether the labware uri needs to be calculated in half a millimeter.""" uri = self.get_uri_from_definition(self.get_definition(labware_id)) return uri in _MAGDECK_HALF_MM_LABWARE + + def get_deck_default_gripper_offsets(self) -> Optional[LabwareMovementOffsetData]: + """Get the deck's default gripper offsets.""" + parsed_offsets = ( + self.get_deck_definition().get("gripperOffsets", {}).get("default") + ) + return ( + LabwareMovementOffsetData( + pickUpOffset=LabwareOffsetVector( + x=parsed_offsets["pickUpOffset"]["x"], + y=parsed_offsets["pickUpOffset"]["y"], + z=parsed_offsets["pickUpOffset"]["z"], + ), + dropOffset=LabwareOffsetVector( + x=parsed_offsets["dropOffset"]["x"], + y=parsed_offsets["dropOffset"]["y"], + z=parsed_offsets["dropOffset"]["z"], + ), + ) + if parsed_offsets + else None + ) + + def get_labware_gripper_offsets( + self, + labware_id: str, + slot_name: Optional[DeckSlotName], + ) -> Optional[LabwareMovementOffsetData]: + """Get the labware's gripper offsets of the specified type.""" + parsed_offsets = self.get_definition(labware_id).gripperOffsets + offset_key = slot_name.name if slot_name else "default" + return ( + LabwareMovementOffsetData( + pickUpOffset=cast( + LabwareOffsetVector, parsed_offsets[offset_key].pickUpOffset + ), + dropOffset=cast( + LabwareOffsetVector, parsed_offsets[offset_key].dropOffset + ), + ) + if parsed_offsets + else None + ) diff --git a/api/src/opentrons/protocol_engine/state/modules.py b/api/src/opentrons/protocol_engine/state/modules.py index d0956a888dd..438fba9aaa0 100644 --- a/api/src/opentrons/protocol_engine/state/modules.py +++ b/api/src/opentrons/protocol_engine/state/modules.py @@ -44,6 +44,7 @@ HeaterShakerMovementRestrictors, ModuleLocation, DeckType, + LabwareMovementOffsetData, ) from .. import errors from ..commands import ( @@ -945,3 +946,10 @@ def raise_if_module_in_location( raise errors.LocationIsOccupiedError( f"Module {module.model} is already present at {location}." ) + + def get_default_gripper_offsets( + self, module_id: str + ) -> Optional[LabwareMovementOffsetData]: + """Get the deck's default gripper offsets.""" + offsets = self.get_definition(module_id).gripperOffsets + return offsets.get("default") if offsets else None diff --git a/api/src/opentrons/protocol_engine/types.py b/api/src/opentrons/protocol_engine/types.py index 0b15d47b344..9287696049a 100644 --- a/api/src/opentrons/protocol_engine/types.py +++ b/api/src/opentrons/protocol_engine/types.py @@ -80,6 +80,8 @@ class OnLabwareLocation(BaseModel): ] """Union of all locations where it's legal to keep a labware.""" +OnDeckLabwareLocation = Union[DeckSlotLocation, ModuleLocation, OnLabwareLocation] + NonStackedLocation = Union[DeckSlotLocation, ModuleLocation, _OffDeckLocationType] """Union of all locations where it's legal to keep a labware that can't be stacked on another labware""" @@ -382,6 +384,13 @@ class OverlapOffset(Vec3f): """Offset representing overlap space of one labware on top of another labware or module.""" +class LabwareMovementOffsetData(BaseModel): + """Offsets to be used during labware movement.""" + + pickUpOffset: LabwareOffsetVector + dropOffset: LabwareOffsetVector + + # TODO(mm, 2023-04-13): Move to shared-data, so this binding can be maintained alongside the JSON # schema that it's sourced from. We already do that for labware definitions and JSON protocols. class ModuleDefinition(BaseModel): @@ -440,6 +449,10 @@ class ModuleDefinition(BaseModel): ..., description="List of module models this model is compatible with.", ) + gripperOffsets: Optional[Dict[str, LabwareMovementOffsetData]] = Field( + default_factory=dict, + description="Offsets to use for labware movement using gripper", + ) class LoadedModule(BaseModel): @@ -616,10 +629,3 @@ class LabwareMovementStrategy(str, Enum): USING_GRIPPER = "usingGripper" MANUAL_MOVE_WITH_PAUSE = "manualMoveWithPause" MANUAL_MOVE_WITHOUT_PAUSE = "manualMoveWithoutPause" - - -class LabwareMovementOffsetData(BaseModel): - """Offsets to be used during labware movement.""" - - pick_up_offset: LabwareOffsetVector - drop_offset: LabwareOffsetVector diff --git a/api/tests/opentrons/motion_planning/test_waypoints.py b/api/tests/opentrons/motion_planning/test_waypoints.py index 42df6f257cd..4930d9a1e70 100644 --- a/api/tests/opentrons/motion_planning/test_waypoints.py +++ b/api/tests/opentrons/motion_planning/test_waypoints.py @@ -272,8 +272,8 @@ def test_get_gripper_labware_movement_waypoints() -> None: to_labware_center=Point(201, 202, 219.5), gripper_home_z=999, offset_data=LabwareMovementOffsetData( - pick_up_offset=LabwareOffsetVector(x=-1, y=-2, z=-3), - drop_offset=LabwareOffsetVector(x=1, y=2, z=3), + pickUpOffset=LabwareOffsetVector(x=-1, y=-2, z=-3), + dropOffset=LabwareOffsetVector(x=1, y=2, z=3), ), ) assert result == [ diff --git a/api/tests/opentrons/protocol_api/core/engine/test_labware_core.py b/api/tests/opentrons/protocol_api/core/engine/test_labware_core.py index 21f1d44a6c1..cfd97644a22 100644 --- a/api/tests/opentrons/protocol_api/core/engine/test_labware_core.py +++ b/api/tests/opentrons/protocol_api/core/engine/test_labware_core.py @@ -94,6 +94,7 @@ def test_get_definition(subject: LabwareCore) -> None: "allowedRoles": [], "stackingOffsetWithLabware": {}, "stackingOffsetWithModule": {}, + "gripperOffsets": {}, }, ) assert subject.get_parameters() == cast(LabwareParamsDict, {"loadName": "world"}) diff --git a/api/tests/opentrons/protocol_engine/commands/test_move_labware.py b/api/tests/opentrons/protocol_engine/commands/test_move_labware.py index 26ebcfcf65c..a0ab7f9d7f4 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_move_labware.py +++ b/api/tests/opentrons/protocol_engine/commands/test_move_labware.py @@ -225,8 +225,8 @@ async def test_gripper_move_labware_implementation( current_location=validated_from_location, new_location=validated_new_location, user_offset_data=LabwareMovementOffsetData( - pick_up_offset=LabwareOffsetVector(x=1, y=2, z=3), - drop_offset=LabwareOffsetVector(x=0, y=0, z=0), + pickUpOffset=LabwareOffsetVector(x=1, y=2, z=3), + dropOffset=LabwareOffsetVector(x=0, y=0, z=0), ), ), ) diff --git a/api/tests/opentrons/protocol_engine/conftest.py b/api/tests/opentrons/protocol_engine/conftest.py index cd7d57cb1b8..1888f06affd 100644 --- a/api/tests/opentrons/protocol_engine/conftest.py +++ b/api/tests/opentrons/protocol_engine/conftest.py @@ -100,6 +100,14 @@ def well_plate_def() -> LabwareDefinition: ) +@pytest.fixture(scope="session") +def adapter_plate_def() -> LabwareDefinition: + """Get the definition of a h/s adapter plate.""" + return LabwareDefinition.parse_obj( + load_definition("opentrons_universal_flat_adapter", 1) + ) + + @pytest.fixture(scope="session") def reservoir_def() -> LabwareDefinition: """Get the definition of single-row reservoir.""" 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 11661e2a921..b332c1bf0a7 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 @@ -92,8 +92,8 @@ def heater_shaker_movement_flagger(decoy: Decoy) -> HeaterShakerMovementFlagger: def default_experimental_movement_data() -> LabwareMovementOffsetData: """Experimental movement data with default values.""" return LabwareMovementOffsetData( - pick_up_offset=LabwareOffsetVector(x=0, y=0, z=0), - drop_offset=LabwareOffsetVector(x=0, y=0, z=0), + pickUpOffset=LabwareOffsetVector(x=0, y=0, z=0), + dropOffset=LabwareOffsetVector(x=0, y=0, z=0), ) @@ -160,12 +160,12 @@ async def test_move_labware_with_gripper( # smoke test for gripper labware movement with actual labware and make this a unit test. user_offset_data = LabwareMovementOffsetData( - pick_up_offset=LabwareOffsetVector(x=123, y=234, z=345), - drop_offset=LabwareOffsetVector(x=111, y=222, z=333), + pickUpOffset=LabwareOffsetVector(x=123, y=234, z=345), + dropOffset=LabwareOffsetVector(x=111, y=222, z=333), ) final_offset_data = LabwareMovementOffsetData( - pick_up_offset=LabwareOffsetVector(x=-1, y=-2, z=-3), - drop_offset=LabwareOffsetVector(x=1, y=2, z=3), + pickUpOffset=LabwareOffsetVector(x=-1, y=-2, z=-3), + dropOffset=LabwareOffsetVector(x=1, y=2, z=3), ) decoy.when(state_store.config.use_virtual_gripper).then_return(False) 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 fe4432c4437..13dd5d1e1df 100644 --- a/api/tests/opentrons/protocol_engine/state/test_geometry_view.py +++ b/api/tests/opentrons/protocol_engine/state/test_geometry_view.py @@ -32,7 +32,6 @@ OverlapOffset, DeckType, CurrentWell, - LabwareLocation, LabwareMovementOffsetData, ) from opentrons.protocol_engine.state import move_types @@ -40,7 +39,7 @@ from opentrons.protocol_engine.state.labware import LabwareView from opentrons.protocol_engine.state.modules import ModuleView from opentrons.protocol_engine.state.pipettes import PipetteView, StaticPipetteConfig -from opentrons.protocol_engine.state.geometry import GeometryView +from opentrons.protocol_engine.state.geometry import GeometryView, _GripperMoveType @pytest.fixture @@ -1415,48 +1414,38 @@ def test_get_next_drop_tip_location_in_non_trash_labware( ) -@pytest.mark.parametrize( - argnames=["from_location", "location_based_pick_up_offset"], - argvalues=[ - ( - DeckSlotLocation(slotName=DeckSlotName("D2")), - LabwareOffsetVector(x=0, y=0, z=0), - ), - ( - ModuleLocation(moduleId="thermocycler-id"), - LabwareOffsetVector(x=0, y=0, z=3.5), - ), - (OnLabwareLocation(labwareId="adapter-id"), LabwareOffsetVector(x=0, y=0, z=0)), - ], -) def test_get_final_labware_movement_offset_vectors( decoy: Decoy, module_view: ModuleView, + labware_view: LabwareView, subject: GeometryView, - from_location: LabwareLocation, - location_based_pick_up_offset: LabwareOffsetVector, ) -> None: """It should provide the final labware movement offset data based on locations.""" - # TODO: update once built-in offsets are being fetched from definitions - decoy.when(module_view.get_connected_model("thermocycler-id")).then_return( - ModuleModel.THERMOCYCLER_MODULE_V2 + decoy.when(labware_view.get_deck_default_gripper_offsets()).then_return( + LabwareMovementOffsetData( + pickUpOffset=LabwareOffsetVector(x=1, y=2, z=3), + dropOffset=LabwareOffsetVector(x=3, y=2, z=1), + ) + ) + decoy.when(module_view.get_default_gripper_offsets("module-id")).then_return( + LabwareMovementOffsetData( + pickUpOffset=LabwareOffsetVector(x=11, y=22, z=33), + dropOffset=LabwareOffsetVector(x=33, y=22, z=11), + ) ) - in_protocol_pickup_offset = LabwareOffsetVector(x=1, y=2, z=4.0) - in_protocol_drop_offset = LabwareOffsetVector(x=4, y=5, z=6) final_offsets = subject.get_final_labware_movement_offset_vectors( - from_location=from_location, - to_location=DeckSlotLocation(slotName=DeckSlotName("D2")), + from_location=DeckSlotLocation(slotName=DeckSlotName("D2")), + to_location=ModuleLocation(moduleId="module-id"), additional_offset_vector=LabwareMovementOffsetData( - pick_up_offset=in_protocol_pickup_offset, - drop_offset=in_protocol_drop_offset, + pickUpOffset=LabwareOffsetVector(x=100, y=200, z=300), + dropOffset=LabwareOffsetVector(x=400, y=500, z=600), ), ) - assert ( - final_offsets.pick_up_offset - == location_based_pick_up_offset + in_protocol_pickup_offset + assert final_offsets == LabwareMovementOffsetData( + pickUpOffset=LabwareOffsetVector(x=101, y=202, z=303), + dropOffset=LabwareOffsetVector(x=433, y=522, z=611), ) - assert final_offsets.drop_offset == in_protocol_drop_offset def test_ensure_valid_gripper_location(subject: GeometryView) -> None: @@ -1475,3 +1464,130 @@ def test_ensure_valid_gripper_location(subject: GeometryView) -> None: with pytest.raises(errors.LabwareMovementNotAllowedError): subject.ensure_valid_gripper_location(off_deck_location) + + +def test_get_total_nominal_gripper_offset( + decoy: Decoy, + labware_view: LabwareView, + module_view: ModuleView, + subject: GeometryView, +) -> None: + """It should calculate the correct gripper offsets given the location and move type..""" + decoy.when(labware_view.get_deck_default_gripper_offsets()).then_return( + LabwareMovementOffsetData( + pickUpOffset=LabwareOffsetVector(x=1, y=2, z=3), + dropOffset=LabwareOffsetVector(x=3, y=2, z=1), + ) + ) + + decoy.when(module_view.get_default_gripper_offsets("module-id")).then_return( + LabwareMovementOffsetData( + pickUpOffset=LabwareOffsetVector(x=11, y=22, z=33), + dropOffset=LabwareOffsetVector(x=33, y=22, z=11), + ) + ) + + # 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, + ) + assert result1 == LabwareOffsetVector(x=1, y=2, z=3) + + # Case 2: labware on module + result2 = subject.get_total_nominal_gripper_offset_for_move_type( + location=ModuleLocation(moduleId="module-id"), + move_type=_GripperMoveType.DROP_LABWARE, + ) + assert result2 == LabwareOffsetVector(x=33, y=22, z=11) + + +def test_get_stacked_labware_total_nominal_offset_slot_specific( + decoy: Decoy, + labware_view: LabwareView, + module_view: ModuleView, + subject: GeometryView, +) -> None: + """Get nominal offset for stacked labware.""" + # Case: labware on adapter on module, adapter has slot-specific offsets + decoy.when(module_view.get_default_gripper_offsets("module-id")).then_return( + LabwareMovementOffsetData( + pickUpOffset=LabwareOffsetVector(x=11, y=22, z=33), + dropOffset=LabwareOffsetVector(x=33, y=22, z=11), + ) + ) + decoy.when(module_view.get_location("module-id")).then_return( + DeckSlotLocation(slotName=DeckSlotName.SLOT_C1) + ) + decoy.when( + labware_view.get_labware_gripper_offsets( + labware_id="adapter-id", slot_name=DeckSlotName.SLOT_C1 + ) + ).then_return( + LabwareMovementOffsetData( + pickUpOffset=LabwareOffsetVector(x=100, y=200, z=300), + dropOffset=LabwareOffsetVector(x=300, y=200, z=100), + ) + ) + decoy.when(labware_view.get_parent_location("adapter-id")).then_return( + ModuleLocation(moduleId="module-id") + ) + result1 = subject.get_total_nominal_gripper_offset_for_move_type( + location=OnLabwareLocation(labwareId="adapter-id"), + move_type=_GripperMoveType.PICK_UP_LABWARE, + ) + 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, + ) + assert result2 == LabwareOffsetVector(x=333, y=222, z=111) + + +def test_get_stacked_labware_total_nominal_offset_default( + decoy: Decoy, + labware_view: LabwareView, + module_view: ModuleView, + subject: GeometryView, +) -> None: + """Get nominal offset for stacked labware.""" + # Case: labware on adapter on module, adapter has only default offsets + decoy.when(module_view.get_default_gripper_offsets("module-id")).then_return( + LabwareMovementOffsetData( + pickUpOffset=LabwareOffsetVector(x=11, y=22, z=33), + dropOffset=LabwareOffsetVector(x=33, y=22, z=11), + ) + ) + decoy.when(module_view.get_location("module-id")).then_return( + DeckSlotLocation(slotName=DeckSlotName.SLOT_4) + ) + decoy.when( + labware_view.get_labware_gripper_offsets( + labware_id="adapter-id", slot_name=DeckSlotName.SLOT_C1 + ) + ).then_return(None) + decoy.when( + labware_view.get_labware_gripper_offsets( + labware_id="adapter-id", slot_name=None + ) + ).then_return( + LabwareMovementOffsetData( + pickUpOffset=LabwareOffsetVector(x=100, y=200, z=300), + dropOffset=LabwareOffsetVector(x=300, y=200, z=100), + ) + ) + decoy.when(labware_view.get_parent_location("adapter-id")).then_return( + ModuleLocation(moduleId="module-id") + ) + result1 = subject.get_total_nominal_gripper_offset_for_move_type( + location=OnLabwareLocation(labwareId="adapter-id"), + move_type=_GripperMoveType.PICK_UP_LABWARE, + ) + 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, + ) + 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 35e070ebb51..d60179995b6 100644 --- a/api/tests/opentrons/protocol_engine/state/test_labware_view.py +++ b/api/tests/opentrons/protocol_engine/state/test_labware_view.py @@ -28,6 +28,7 @@ LabwareLocation, OFF_DECK_LOCATION, OverlapOffset, + LabwareMovementOffsetData, ) from opentrons.protocol_engine.state.move_types import EdgePathType from opentrons.protocol_engine.state.labware import ( @@ -78,6 +79,14 @@ offsetId=None, ) +adapter_plate = LoadedLabware( + id="adapter-plate-id", + loadName="adapter-load-name", + location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), + definitionUri="some-adapter-uri", + offsetId=None, +) + def get_labware_view( labware_by_id: Optional[Dict[str, LoadedLabware]] = None, @@ -1271,3 +1280,38 @@ def test_raise_if_labware_cannot_be_stacked_on_labware_on_adapter() -> None: ), bottom_labware_id="labware-id", ) + + +def test_get_deck_gripper_offsets(ot3_standard_deck_def: DeckDefinitionV3) -> None: + """It should get the deck's gripper offsets.""" + subject = get_labware_view(deck_definition=ot3_standard_deck_def) + + assert subject.get_deck_default_gripper_offsets() == LabwareMovementOffsetData( + pickUpOffset=LabwareOffsetVector(x=0, y=0, z=0), + dropOffset=LabwareOffsetVector(x=0, y=0, z=-0.25), + ) + + +def test_get_labware_gripper_offsets( + well_plate_def: LabwareDefinition, + adapter_plate_def: LabwareDefinition, +) -> None: + """It should get the labware's gripper offsets.""" + subject = get_labware_view( + labware_by_id={"plate-id": plate, "adapter-plate-id": adapter_plate}, + definitions_by_uri={ + "some-plate-uri": well_plate_def, + "some-adapter-uri": adapter_plate_def, + }, + ) + + assert ( + subject.get_labware_gripper_offsets(labware_id="plate-id", slot_name=None) + is None + ) + assert subject.get_labware_gripper_offsets( + labware_id="adapter-plate-id", slot_name=DeckSlotName.SLOT_D1 + ) == LabwareMovementOffsetData( + pickUpOffset=LabwareOffsetVector(x=0, y=0, z=0), + dropOffset=LabwareOffsetVector(x=2, y=0, z=0), + ) diff --git a/api/tests/opentrons/protocol_engine/state/test_module_view.py b/api/tests/opentrons/protocol_engine/state/test_module_view.py index f3a6af34217..7c9a3a9dc90 100644 --- a/api/tests/opentrons/protocol_engine/state/test_module_view.py +++ b/api/tests/opentrons/protocol_engine/state/test_module_view.py @@ -18,6 +18,7 @@ DeckType, ModuleOffsetVector, HeaterShakerLatchStatus, + LabwareMovementOffsetData, ) from opentrons.protocol_engine.state.modules import ( ModuleView, @@ -1743,3 +1744,48 @@ def test_is_edge_move_unsafe( result = subject.is_edge_move_unsafe(mount=mount, target_slot=target_slot) assert result is expected_result + + +@pytest.mark.parametrize( + argnames=["module_def", "expected_offset_data"], + argvalues=[ + ( + lazy_fixture("thermocycler_v2_def"), + LabwareMovementOffsetData( + pickUpOffset=LabwareOffsetVector(x=0, y=0, z=4.6), + dropOffset=LabwareOffsetVector(x=0, y=0, z=4.6), + ), + ), + ( + lazy_fixture("heater_shaker_v1_def"), + LabwareMovementOffsetData( + pickUpOffset=LabwareOffsetVector(x=0, y=0, z=0), + dropOffset=LabwareOffsetVector(x=0, y=0, z=0.5), + ), + ), + ( + lazy_fixture("tempdeck_v1_def"), + None, + ), + ], +) +def test_get_default_gripper_offsets( + module_def: ModuleDefinition, + expected_offset_data: Optional[LabwareMovementOffsetData], +) -> None: + """It should return the correct gripper offsets, if present.""" + subject = make_module_view( + slot_by_module_id={ + "module-1": DeckSlotName.SLOT_1, + }, + requested_model_by_module_id={ + "module-1": ModuleModel.TEMPERATURE_MODULE_V1, # Does not matter + }, + hardware_by_module_id={ + "module-1": HardwareModule( + serial_number="serial-1", + definition=module_def, + ), + }, + ) + assert subject.get_default_gripper_offsets("module-1") == expected_offset_data diff --git a/shared-data/deck/definitions/3/ot3_standard.json b/shared-data/deck/definitions/3/ot3_standard.json index 28a5d6272a9..6583519c0b7 100644 --- a/shared-data/deck/definitions/3/ot3_standard.json +++ b/shared-data/deck/definitions/3/ot3_standard.json @@ -982,5 +982,19 @@ } ] } - ] + ], + "gripperOffsets": { + "default": { + "pickUpOffset": { + "x": 0, + "y": 0, + "z": 0 + }, + "dropOffset": { + "x": 0, + "y": 0, + "z": -0.25 + } + } + } } diff --git a/shared-data/deck/schemas/3.json b/shared-data/deck/schemas/3.json index 5158b98e6b2..46088aabb33 100644 --- a/shared-data/deck/schemas/3.json +++ b/shared-data/deck/schemas/3.json @@ -12,6 +12,22 @@ "minItems": 3, "maxItems": 3 }, + "coordinates": { + "type": "object", + "additionalProperties": false, + "required": ["x", "y", "z"], + "properties": { + "x": { + "type": "number" + }, + "y": { + "type": "number" + }, + "z": { + "type": "number" + } + } + }, "unitVector": { "type": "array", "description": "Array of 3 unit directions, [x, y, z]", @@ -79,32 +95,26 @@ "description": "Unique internal ID generated using UUID", "type": "string" }, - "schemaVersion": { "description": "Schema version of a deck is a single integer", "enum": [3] }, - "cornerOffsetFromOrigin": { "$ref": "#/definitions/xyzArray", "description": "Position of left-front-bottom corner of entire deck to robot coordinate system origin" }, - "dimensions": { "$ref": "#/definitions/xyzArray", "description": "Outer dimensions of a deck bounding box" }, - "metadata": { "description": "Optional metadata about the Deck", "type": "object", - "properties": { "displayName": { "description": "A short, human-readable name for the deck", "type": "string" }, - "tags": { "description": "Tags to be used in searching for this deck", "type": "array", @@ -114,7 +124,6 @@ } } }, - "robot": { "type": "object", "required": ["model"], @@ -126,7 +135,6 @@ } } }, - "locations": { "type": "object", "required": ["orderedSlots", "calibrationPoints"], @@ -148,12 +156,16 @@ "description": "Unique identifier for slot", "type": "string" }, - "position": { "$ref": "#/definitions/xyzArray" }, + "position": { + "$ref": "#/definitions/xyzArray" + }, "matingSurfaceUnitVector": { "$ref": "#/definitions/unitVector", "description": "An optional diagonal direction of force, defined by spring location, which governs the mating surface of objects placed in slot." }, - "boundingBox": { "$ref": "#/definitions/boundingBox" }, + "boundingBox": { + "$ref": "#/definitions/boundingBox" + }, "displayName": { "description": "A human-readable nickname for this slot Eg \"Slot 1\" or \"Fixed Trash\"", "type": "string" @@ -174,7 +186,6 @@ } } }, - "calibrationPoints": { "type": "array", "description": "Key points for deck calibration", @@ -186,7 +197,9 @@ "description": "Unique identifier for calibration point", "type": "string" }, - "position": { "$ref": "#/definitions/xyzArray" }, + "position": { + "$ref": "#/definitions/xyzArray" + }, "displayName": { "description": "An optional human-readable nickname for this point Eg \"Slot 3 Cross\" or \"Slot 1 Dot\"", "type": "string" @@ -194,7 +207,6 @@ } } }, - "fixtures": { "type": "array", "description": "Fixed position objects on the deck.", @@ -210,12 +222,16 @@ "description": "Valid labware loadName for fixed object", "type": "string" }, - "boundingBox": { "$ref": "#/definitions/boundingBox" }, + "boundingBox": { + "$ref": "#/definitions/boundingBox" + }, "slot": { "description": "Slot location of the fixed object", "type": "string" }, - "position": { "$ref": "#/definitions/xyzArray" }, + "position": { + "$ref": "#/definitions/xyzArray" + }, "displayName": { "description": "An optional human-readable nickname for this fixture Eg \"Tall Fixed Trash\" or \"Short Fixed Trash\"", "type": "string" @@ -250,7 +266,30 @@ "layers": { "type": "array", "description": "Layered feature groups of the deck.", - "items": { "$ref": "#/definitions/svgsonNode" } + "items": { + "$ref": "#/definitions/svgsonNode" + } + }, + "gripperOffsets": { + "type": "object", + "description": "Offsets to be added when calculating the coordinates a gripper should go to when picking up or dropping a labware on this deck.", + "properties": { + "default": { + "type": "object", + "properties": { + "pickUpOffset": { + "$ref": "#/definitions/coordinates", + "description": "Offset added to calculate pick-up coordinates of a labware placed on this deck." + }, + "dropOffset": { + "$ref": "#/definitions/coordinates", + "description": "Offset added to calculate drop coordinates of a labware placed on this deck." + } + }, + "required": ["pickUpOffset", "dropOffset"] + } + }, + "required": ["default"] } } } diff --git a/shared-data/js/__tests__/deckSchemas.test.ts b/shared-data/js/__tests__/deckSchemas.test.ts index 3e74d14fb95..57dc9cb0a50 100644 --- a/shared-data/js/__tests__/deckSchemas.test.ts +++ b/shared-data/js/__tests__/deckSchemas.test.ts @@ -41,7 +41,7 @@ describe('validate deck defs and fixtures', () => { defs.forEach(defPath => { const deckDef = require(defPath) - it('deck validates against v1 schema', () => { + it('deck validates against v3 schema', () => { const valid = validateSchema(deckDef) const validationErrors = validateSchema.errors diff --git a/shared-data/labware/definitions/2/opentrons_universal_flat_adapter/1.json b/shared-data/labware/definitions/2/opentrons_universal_flat_adapter/1.json index 7027cae96de..3d99ffe7cf2 100644 --- a/shared-data/labware/definitions/2/opentrons_universal_flat_adapter/1.json +++ b/shared-data/labware/definitions/2/opentrons_universal_flat_adapter/1.json @@ -37,5 +37,115 @@ "x": 0, "y": 0, "z": 0 + }, + "gripperOffsets": { + "default": { + "pickUpOffset": { + "x": 0, + "y": 0, + "z": 0 + }, + "dropOffset": { + "x": 0, + "y": 0, + "z": 0 + } + }, + "SLOT_A1": { + "pickUpOffset": { + "x": 0, + "y": 0, + "z": 0 + }, + "dropOffset": { + "x": 2.0, + "y": 0, + "z": 0 + } + }, + "SLOT_B1": { + "pickUpOffset": { + "x": 0, + "y": 0, + "z": 0 + }, + "dropOffset": { + "x": 2.0, + "y": 0, + "z": 0 + } + }, + "SLOT_C1": { + "pickUpOffset": { + "x": 0, + "y": 0, + "z": 0 + }, + "dropOffset": { + "x": 2.0, + "y": 0, + "z": 0 + } + }, + "SLOT_D1": { + "pickUpOffset": { + "x": 0, + "y": 0, + "z": 0 + }, + "dropOffset": { + "x": 2.0, + "y": 0, + "z": 0 + } + }, + "SLOT_A3": { + "pickUpOffset": { + "x": 0, + "y": 0, + "z": 0 + }, + "dropOffset": { + "x": -2.0, + "y": 0, + "z": 0 + } + }, + "SLOT_B3": { + "pickUpOffset": { + "x": 0, + "y": 0, + "z": 0 + }, + "dropOffset": { + "x": -2.0, + "y": 0, + "z": 0 + } + }, + "SLOT_C3": { + "pickUpOffset": { + "x": 0, + "y": 0, + "z": 0 + }, + "dropOffset": { + "x": -2.0, + "y": 0, + "z": 0 + } + }, + "SLOT_D3": { + "pickUpOffset": { + "x": 0, + "y": 0, + "z": 0 + }, + "dropOffset": { + "x": -2.0, + "y": 0, + "z": 0 + } + } } } diff --git a/shared-data/labware/schemas/2.json b/shared-data/labware/schemas/2.json index 56866d06cb8..aeaa881b54d 100644 --- a/shared-data/labware/schemas/2.json +++ b/shared-data/labware/schemas/2.json @@ -339,6 +339,27 @@ "additionalProperties": { "$ref": "#/definitions/coordinates" } + }, + "gripperOffsets": { + "type": "object", + "description": "Offsets to be added when calculating the coordinates a gripper should go to when picking up or dropping a labware on this labware.", + "properties": { + "default": { + "type": "object", + "properties": { + "pickUpOffset": { + "$ref": "#/definitions/coordinates", + "description": "Offset added to calculate pick-up coordinates of a labware placed on this labware." + }, + "dropOffset": { + "$ref": "#/definitions/coordinates", + "description": "Offset added to calculate drop coordinates of a labware placed on this labware." + } + }, + "required": ["pickUpOffset", "dropOffset"] + } + }, + "required": ["default"] } } } diff --git a/shared-data/module/definitions/3/heaterShakerModuleV1.json b/shared-data/module/definitions/3/heaterShakerModuleV1.json index 6f3cac096ac..63d6dd31998 100644 --- a/shared-data/module/definitions/3/heaterShakerModuleV1.json +++ b/shared-data/module/definitions/3/heaterShakerModuleV1.json @@ -33,6 +33,20 @@ "minShakeSpeed": 200, "maxShakeSpeed": 3000 }, + "gripperOffsets": { + "default": { + "pickUpOffset": { + "x": 0, + "y": 0, + "z": 0 + }, + "dropOffset": { + "x": 0, + "y": 0, + "z": 0.5 + } + } + }, "displayName": "Heater-Shaker Module GEN1", "quirks": [], "slotTransforms": { diff --git a/shared-data/module/definitions/3/magneticBlockV1.json b/shared-data/module/definitions/3/magneticBlockV1.json index a1518880e6b..2dea6a9d2ca 100644 --- a/shared-data/module/definitions/3/magneticBlockV1.json +++ b/shared-data/module/definitions/3/magneticBlockV1.json @@ -28,6 +28,20 @@ "z": 0 }, "config": {}, + "gripperOffsets": { + "default": { + "pickUpOffset": { + "x": 0, + "y": 0, + "z": 0 + }, + "dropOffset": { + "x": 0, + "y": 0, + "z": 0.5 + } + } + }, "displayName": "Magnetic Block GEN1", "quirks": [], "slotTransforms": { diff --git a/shared-data/module/definitions/3/temperatureModuleV2.json b/shared-data/module/definitions/3/temperatureModuleV2.json index c23e179e06b..cc16e0b1ec4 100644 --- a/shared-data/module/definitions/3/temperatureModuleV2.json +++ b/shared-data/module/definitions/3/temperatureModuleV2.json @@ -31,6 +31,20 @@ "minTemperature": 9, "maxTemperature": 99 }, + "gripperOffsets": { + "default": { + "pickUpOffset": { + "x": 0, + "y": 0, + "z": 0 + }, + "dropOffset": { + "x": 0, + "y": 0, + "z": 0.5 + } + } + }, "displayName": "Temperature Module GEN2", "quirks": [], "slotTransforms": { diff --git a/shared-data/module/definitions/3/thermocyclerModuleV2.json b/shared-data/module/definitions/3/thermocyclerModuleV2.json index ea3826b03e4..90f65108b6f 100644 --- a/shared-data/module/definitions/3/thermocyclerModuleV2.json +++ b/shared-data/module/definitions/3/thermocyclerModuleV2.json @@ -34,6 +34,20 @@ "minBlockVolume": 0, "maxBlockVolume": 100 }, + "gripperOffsets": { + "default": { + "pickUpOffset": { + "x": 0, + "y": 0, + "z": 4.6 + }, + "dropOffset": { + "x": 0, + "y": 0, + "z": 4.6 + } + } + }, "displayName": "Thermocycler Module GEN2", "quirks": [], "slotTransforms": { diff --git a/shared-data/module/schemas/3.json b/shared-data/module/schemas/3.json index 789d6684602..6ccd1bb8eab 100644 --- a/shared-data/module/schemas/3.json +++ b/shared-data/module/schemas/3.json @@ -137,6 +137,27 @@ "maxBlockVolume": { "type": "number" } } }, + "gripperOffsets": { + "type": "object", + "description": "Offsets to be added when calculating the coordinates a gripper should go to when picking up or dropping a labware on a module.", + "properties": { + "default": { + "type": "object", + "properties": { + "pickUpOffset": { + "$ref": "#/definitions/coordinates", + "description": "Offset added to calculate pick-up coordinates of a labware placed on this module." + }, + "dropOffset": { + "$ref": "#/definitions/coordinates", + "description": "Offset added to calculate drop coordinates of a labware placed on this module." + } + }, + "required": ["pickUpOffset", "dropOffset"] + } + }, + "required": ["default"] + }, "displayName": { "type": "string" }, "quirks": { "type": "array", diff --git a/shared-data/python/opentrons_shared_data/deck/dev_types.py b/shared-data/python/opentrons_shared_data/deck/dev_types.py index e68b3a1addd..f550d1a5ee0 100644 --- a/shared-data/python/opentrons_shared_data/deck/dev_types.py +++ b/shared-data/python/opentrons_shared_data/deck/dev_types.py @@ -98,7 +98,18 @@ class LocationsV3(TypedDict): fixtures: List[Fixture] -class DeckDefinitionV3(TypedDict): +class NamedOffset(TypedDict): + x: float + y: float + z: float + + +class GripperOffsets(TypedDict): + pickUpOffset: NamedOffset + dropOffset: NamedOffset + + +class _RequiredDeckDefinitionV3(TypedDict): otId: str schemaVersion: Literal[3] cornerOffsetFromOrigin: List[float] @@ -109,4 +120,8 @@ class DeckDefinitionV3(TypedDict): layers: List[INode] +class DeckDefinitionV3(_RequiredDeckDefinitionV3, total=False): + gripperOffsets: Dict[str, GripperOffsets] + + DeckDefinition = DeckDefinitionV3 diff --git a/shared-data/python/opentrons_shared_data/labware/dev_types.py b/shared-data/python/opentrons_shared_data/labware/dev_types.py index 516d619c3bf..58e9588d882 100644 --- a/shared-data/python/opentrons_shared_data/labware/dev_types.py +++ b/shared-data/python/opentrons_shared_data/labware/dev_types.py @@ -46,6 +46,11 @@ class NamedOffset(TypedDict): z: float +class GripperOffsets(TypedDict): + pickUpOffset: NamedOffset + dropOffset: NamedOffset + + class LabwareParameters(TypedDict, total=False): format: LabwareFormat isTiprack: bool @@ -130,3 +135,4 @@ class LabwareDefinition(_RequiredLabwareDefinition, total=False): stackingOffsetWithLabware: Dict[str, NamedOffset] stackingOffsetWithModule: Dict[str, NamedOffset] allowedRoles: List[LabwareRoles] + gripperOffsets: Dict[str, GripperOffsets] 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 390f90143d8..687cd2765e0 100644 --- a/shared-data/python/opentrons_shared_data/labware/labware_definition.py +++ b/shared-data/python/opentrons_shared_data/labware/labware_definition.py @@ -64,6 +64,25 @@ class OverlapOffset(BaseModel): z: _Number +class OffsetVector(BaseModel): + """ + A generic 3-D offset vector. + """ + + x: _Number + y: _Number + z: _Number + + +class GripperOffsets(BaseModel): + """ + Offsets used when calculating coordinates for gripping labware during labware movement. + """ + + pickUpOffset: OffsetVector + dropOffset: OffsetVector + + class BrandData(BaseModel): brand: str = Field(..., description="Brand/manufacturer name") brandId: Optional[List[str]] = Field( @@ -294,3 +313,8 @@ class LabwareDefinition(BaseModel): description="Supported module that can be stacked upon," " with overlap vector offset between labware and module.", ) + gripperOffsets: Dict[str, GripperOffsets] = Field( + default_factory=dict, + description="Offsets use when calculating coordinates for gripping labware " + "during labware movement.", + ) diff --git a/shared-data/python/opentrons_shared_data/module/dev_types.py b/shared-data/python/opentrons_shared_data/module/dev_types.py index eef616c94cb..326d9b6b764 100644 --- a/shared-data/python/opentrons_shared_data/module/dev_types.py +++ b/shared-data/python/opentrons_shared_data/module/dev_types.py @@ -77,6 +77,18 @@ "CornerOffsetFromSlot", {"x": float, "y": float, "z": float} ) + +class NamedOffset(TypedDict): + x: float + y: float + z: float + + +class GripperOffsets(TypedDict): + pickUpOffset: NamedOffset + dropOffset: NamedOffset + + # TODO(mc, 2022-03-18): potentially move from typed-dict to Pydantic ModuleDefinitionV3 = TypedDict( "ModuleDefinitionV3", @@ -94,7 +106,9 @@ "slotTransforms": Dict[str, Dict[str, Dict[str, List[List[float]]]]], "compatibleWith": List[ModuleModel], "twoDimensionalRendering": Dict[str, Any], + "gripperOffsets": Dict[str, GripperOffsets], }, + total=False, ) # V2 is not used anymore. This type is preserved for historical purposes