From 21c7adb51f676849ebeccc28ad52dc954f0f8585 Mon Sep 17 00:00:00 2001 From: CaseyBatten Date: Mon, 26 Aug 2024 18:07:05 -0400 Subject: [PATCH] fix(api, shared-date): Deck extents fix to utilize padding for conflict checking in place of mount offsets (#16119) Cover RQA-3067 Extent padding numbers to limit deck extent movement across deck were measured from the extent limits to ensure the following truths: - The front most nozzle must not cross the rear padding "line" - The rear most nozzle must not cross the front padding "line" - The left most nozzle must not cross the right padding "line" - the right most nozzle must not cross the left padding "line" These lines were identified on Flex as: - Rear line is row G of labware in deck row A - Front line is row F of labware in deck row D - Left line is column 2 of labware in Thermocycler - Right line is column 11 of labware in deck column 2 --- .../protocol_api/core/engine/deck_conflict.py | 57 +++++++++++++------ .../state/addressable_areas.py | 14 +++++ .../protocol_engine/state/geometry.py | 13 ++++- .../protocol_engine/state/pipettes.py | 2 - .../core/engine/test_deck_conflict.py | 12 +++- .../test_pipette_movement_deck_conflicts.py | 7 --- .../state/test_addressable_area_state.py | 6 ++ .../state/test_addressable_area_store.py | 18 ++++++ .../state/test_addressable_area_view.py | 6 ++ .../state/test_geometry_view.py | 6 ++ .../state/test_module_store.py | 6 ++ .../protocol_engine/state/test_module_view.py | 6 ++ .../protocol_engine/state/test_state_store.py | 6 ++ .../opentrons_shared_data/robot/types.py | 10 ++++ shared-data/robot/definitions/1/ot2.json | 6 ++ shared-data/robot/definitions/1/ot3.json | 6 ++ shared-data/robot/schemas/1.json | 23 ++++++++ 17 files changed, 177 insertions(+), 27 deletions(-) diff --git a/api/src/opentrons/protocol_api/core/engine/deck_conflict.py b/api/src/opentrons/protocol_api/core/engine/deck_conflict.py index 405aa2256a7..6ebb47f0ac8 100644 --- a/api/src/opentrons/protocol_api/core/engine/deck_conflict.py +++ b/api/src/opentrons/protocol_api/core/engine/deck_conflict.py @@ -416,23 +416,48 @@ def _is_within_pipette_extents( pipette_bounding_box_at_loc: Tuple[Point, Point, Point, Point], ) -> bool: """Whether a given point is within the extents of a configured pipette on the specified robot.""" - mount = engine_state.pipettes.get_mount(pipette_id) - robot_extent_per_mount = engine_state.geometry.absolute_deck_extents - pip_back_left_bound, pip_front_right_bound, _, _ = pipette_bounding_box_at_loc - pipette_bounds_offsets = engine_state.pipettes.get_pipette_bounding_box(pipette_id) - from_back_right = ( - robot_extent_per_mount.back_right[mount] - + pipette_bounds_offsets.back_right_corner - ) - from_front_left = ( - robot_extent_per_mount.front_left[mount] - + pipette_bounds_offsets.front_left_corner - ) + channels = engine_state.pipettes.get_channels(pipette_id) + robot_extents = engine_state.geometry.absolute_deck_extents + ( + pip_back_left_bound, + pip_front_right_bound, + pip_back_right_bound, + pip_front_left_bound, + ) = pipette_bounding_box_at_loc + + # Given the padding values accounted for against the deck extents, + # a pipette is within extents when all of the following are true: + + # Each corner slot full pickup case: + # A1: Front right nozzle is within the rear and left-side padding limits + # D1: Back right nozzle is within the front and left-side padding limits + # A3 Front left nozzle is within the rear and right-side padding limits + # D3: Back left nozzle is within the front and right-side padding limits + # Thermocycler Column A2: Front right nozzle is within padding limits + + if channels == 96: + return ( + pip_front_right_bound.y + <= robot_extents.deck_extents.y + robot_extents.padding_rear + and pip_front_right_bound.x >= robot_extents.padding_left_side + and pip_back_right_bound.y >= robot_extents.padding_front + and pip_back_right_bound.x >= robot_extents.padding_left_side + and pip_front_left_bound.y + <= robot_extents.deck_extents.y + robot_extents.padding_rear + and pip_front_left_bound.x + <= robot_extents.deck_extents.x + robot_extents.padding_right_side + and pip_back_left_bound.y >= robot_extents.padding_front + and pip_back_left_bound.x + <= robot_extents.deck_extents.x + robot_extents.padding_right_side + ) + # For 8ch pipettes we only check the rear and front extents return ( - from_back_right.x >= pip_back_left_bound.x >= from_front_left.x - and from_back_right.y >= pip_back_left_bound.y >= from_front_left.y - and from_back_right.x >= pip_front_right_bound.x >= from_front_left.x - and from_back_right.y >= pip_front_right_bound.y >= from_front_left.y + pip_front_right_bound.y + <= robot_extents.deck_extents.y + robot_extents.padding_rear + and pip_back_right_bound.y >= robot_extents.padding_front + and pip_front_left_bound.y + <= robot_extents.deck_extents.y + robot_extents.padding_rear + and pip_back_left_bound.y >= robot_extents.padding_front ) diff --git a/api/src/opentrons/protocol_engine/state/addressable_areas.py b/api/src/opentrons/protocol_engine/state/addressable_areas.py index ab9c3d8462d..202342ec4d0 100644 --- a/api/src/opentrons/protocol_engine/state/addressable_areas.py +++ b/api/src/opentrons/protocol_engine/state/addressable_areas.py @@ -352,6 +352,20 @@ def mount_offsets(self) -> Dict[str, Point]: "right": Point(x=right_offset[0], y=right_offset[1], z=right_offset[2]), } + @cached_property + def padding_offsets(self) -> Dict[str, float]: + """The padding offsets to be applied to the deck extents of the robot.""" + rear_offset = self.state.robot_definition["paddingOffsets"]["rear"] + front_offset = self.state.robot_definition["paddingOffsets"]["front"] + left_side_offset = self.state.robot_definition["paddingOffsets"]["leftSide"] + right_side_offset = self.state.robot_definition["paddingOffsets"]["rightSide"] + return { + "rear": rear_offset, + "front": front_offset, + "left_side": left_side_offset, + "right_side": right_side_offset, + } + def get_addressable_area(self, addressable_area_name: str) -> AddressableArea: """Get addressable area.""" if not self._state.use_simulated_deck_config: diff --git a/api/src/opentrons/protocol_engine/state/geometry.py b/api/src/opentrons/protocol_engine/state/geometry.py index 7b02e1242da..ab51a896a03 100644 --- a/api/src/opentrons/protocol_engine/state/geometry.py +++ b/api/src/opentrons/protocol_engine/state/geometry.py @@ -77,6 +77,11 @@ class _GripperMoveType(enum.Enum): class _AbsoluteRobotExtents: front_left: Dict[MountType, Point] back_right: Dict[MountType, Point] + deck_extents: Point + padding_rear: float + padding_front: float + padding_left_side: float + padding_right_side: float _LabwareLocation = TypeVar("_LabwareLocation", bound=LabwareLocation) @@ -118,7 +123,13 @@ def absolute_deck_extents(self) -> _AbsoluteRobotExtents: MountType.RIGHT: self._addressable_areas.deck_extents + right_offset, } return _AbsoluteRobotExtents( - front_left=front_left_abs, back_right=back_right_abs + front_left=front_left_abs, + back_right=back_right_abs, + deck_extents=self._addressable_areas.deck_extents, + padding_rear=self._addressable_areas.padding_offsets["rear"], + padding_front=self._addressable_areas.padding_offsets["front"], + padding_left_side=self._addressable_areas.padding_offsets["left_side"], + padding_right_side=self._addressable_areas.padding_offsets["right_side"], ) def get_labware_highest_z(self, labware_id: str) -> float: diff --git a/api/src/opentrons/protocol_engine/state/pipettes.py b/api/src/opentrons/protocol_engine/state/pipettes.py index 58a798e90bd..3c719e546c2 100644 --- a/api/src/opentrons/protocol_engine/state/pipettes.py +++ b/api/src/opentrons/protocol_engine/state/pipettes.py @@ -845,8 +845,6 @@ def get_pipette_bounds_at_specified_move_to_position( - primary_nozzle_offset + pipette_bounds_offsets.front_right_corner ) - # TODO (spp, 2024-02-27): remove back right & front left; - # return only back left and front right points. pip_back_right_bound = Point( pip_front_right_bound.x, pip_back_left_bound.y, pip_front_right_bound.z ) diff --git a/api/tests/opentrons/protocol_api/core/engine/test_deck_conflict.py b/api/tests/opentrons/protocol_api/core/engine/test_deck_conflict.py index d0171bff798..147368e0734 100644 --- a/api/tests/opentrons/protocol_api/core/engine/test_deck_conflict.py +++ b/api/tests/opentrons/protocol_api/core/engine/test_deck_conflict.py @@ -510,6 +510,11 @@ def test_deck_conflict_raises_for_bad_pipette_move( MountType.LEFT: Point(463.7, 433.3, 0.0), MountType.RIGHT: Point(517.7, 433.3), }, + deck_extents=Point(477.2, 493.8, 0.0), + padding_rear=-181.21, + padding_front=55.8, + padding_left_side=31.88, + padding_right_side=-80.32, ) ) decoy.when( @@ -677,6 +682,11 @@ def test_deck_conflict_raises_for_collision_with_tc_lid( MountType.LEFT: Point(463.7, 433.3, 0.0), MountType.RIGHT: Point(517.7, 433.3), }, + deck_extents=Point(477.2, 493.8, 0.0), + padding_rear=-181.21, + padding_front=55.8, + padding_left_side=31.88, + padding_right_side=-80.32, ) ) @@ -696,7 +706,7 @@ def test_deck_conflict_raises_for_collision_with_tc_lid( ) with pytest.raises( deck_conflict.PartialTipMovementNotAllowedError, - match="collision with thermocycler lid in deck slot A1.", + match="Requested motion with the A12 nozzle partial configuration is outside of robot bounds for the pipette.", ): deck_conflict.check_safe_for_pipette_movement( engine_state=mock_state_view, diff --git a/api/tests/opentrons/protocol_api_integration/test_pipette_movement_deck_conflicts.py b/api/tests/opentrons/protocol_api_integration/test_pipette_movement_deck_conflicts.py index 59523fd2c91..1d3388d3d97 100644 --- a/api/tests/opentrons/protocol_api_integration/test_pipette_movement_deck_conflicts.py +++ b/api/tests/opentrons/protocol_api_integration/test_pipette_movement_deck_conflicts.py @@ -61,13 +61,6 @@ def test_deck_conflicts_for_96_ch_a12_column_configuration() -> None: ): instrument.pick_up_tip(badly_placed_tiprack.wells_by_name()["A1"]) - with pytest.raises( - PartialTipMovementNotAllowedError, match="outside of robot bounds" - ): - # Picking up from A1 in an east-most slot using a configuration with column 12 would - # result in a collision with the side of the robot. - instrument.pick_up_tip(well_placed_tiprack.wells_by_name()["A1"]) - instrument.pick_up_tip(well_placed_tiprack.wells_by_name()["A12"]) instrument.aspirate(50, well_placed_labware.wells_by_name()["A4"]) diff --git a/api/tests/opentrons/protocol_engine/state/test_addressable_area_state.py b/api/tests/opentrons/protocol_engine/state/test_addressable_area_state.py index 987db0dcba3..da3e0f3d156 100644 --- a/api/tests/opentrons/protocol_engine/state/test_addressable_area_state.py +++ b/api/tests/opentrons/protocol_engine/state/test_addressable_area_state.py @@ -33,6 +33,12 @@ def test_deck_configuration_setting( "robotType": "OT-3 Standard", "models": ["OT-3 Standard"], "extents": [477.2, 493.8, 0.0], + "paddingOffsets": { + "rear": -177.42, + "front": 51.8, + "leftSide": 31.88, + "rightSide": -80.32, + }, "mountOffsets": { "left": [-13.5, -60.5, 255.675], "right": [40.5, -60.5, 255.675], diff --git a/api/tests/opentrons/protocol_engine/state/test_addressable_area_store.py b/api/tests/opentrons/protocol_engine/state/test_addressable_area_store.py index 9c098cf1c96..5015433d7e0 100644 --- a/api/tests/opentrons/protocol_engine/state/test_addressable_area_store.py +++ b/api/tests/opentrons/protocol_engine/state/test_addressable_area_store.py @@ -74,6 +74,12 @@ def simulated_subject( "robotType": "OT-3 Standard", "models": ["OT-3 Standard"], "extents": [477.2, 493.8, 0.0], + "paddingOffsets": { + "rear": -177.42, + "front": 51.8, + "leftSide": 31.88, + "rightSide": -80.32, + }, "mountOffsets": { "left": [-13.5, -60.5, 255.675], "right": [40.5, -60.5, 255.675], @@ -101,6 +107,12 @@ def subject( "robotType": "OT-3 Standard", "models": ["OT-3 Standard"], "extents": [477.2, 493.8, 0.0], + "paddingOffsets": { + "rear": -177.42, + "front": 51.8, + "leftSide": 31.88, + "rightSide": -80.32, + }, "mountOffsets": { "left": [-13.5, -60.5, 255.675], "right": [40.5, -60.5, 255.675], @@ -127,6 +139,12 @@ def test_initial_state_simulated( "robotType": "OT-3 Standard", "models": ["OT-3 Standard"], "extents": [477.2, 493.8, 0.0], + "paddingOffsets": { + "rear": -177.42, + "front": 51.8, + "leftSide": 31.88, + "rightSide": -80.32, + }, "mountOffsets": { "left": [-13.5, -60.5, 255.675], "right": [40.5, -60.5, 255.675], diff --git a/api/tests/opentrons/protocol_engine/state/test_addressable_area_view.py b/api/tests/opentrons/protocol_engine/state/test_addressable_area_view.py index 07552aa4273..30ca1b9e7c4 100644 --- a/api/tests/opentrons/protocol_engine/state/test_addressable_area_view.py +++ b/api/tests/opentrons/protocol_engine/state/test_addressable_area_view.py @@ -69,6 +69,12 @@ def get_addressable_area_view( "robotType": "OT-3 Standard", "models": ["OT-3 Standard"], "extents": [477.2, 493.8, 0.0], + "paddingOffsets": { + "rear": -177.42, + "front": 51.8, + "leftSide": 31.88, + "rightSide": -80.32, + }, "mountOffsets": { "left": [-13.5, -60.5, 255.675], "right": [40.5, -60.5, 255.675], 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 1f085b526f1..54bd193a050 100644 --- a/api/tests/opentrons/protocol_engine/state/test_geometry_view.py +++ b/api/tests/opentrons/protocol_engine/state/test_geometry_view.py @@ -182,6 +182,12 @@ def addressable_area_store( "robotType": "OT-3 Standard", "models": ["OT-3 Standard"], "extents": [477.2, 493.8, 0.0], + "paddingOffsets": { + "rear": -177.42, + "front": 51.8, + "leftSide": 31.88, + "rightSide": -80.32, + }, "mountOffsets": { "left": [-13.5, -60.5, 255.675], "right": [40.5, -60.5, 255.675], diff --git a/api/tests/opentrons/protocol_engine/state/test_module_store.py b/api/tests/opentrons/protocol_engine/state/test_module_store.py index f052056aa35..5f27c7448db 100644 --- a/api/tests/opentrons/protocol_engine/state/test_module_store.py +++ b/api/tests/opentrons/protocol_engine/state/test_module_store.py @@ -79,6 +79,12 @@ def get_addressable_area_view( "robotType": "OT-3 Standard", "models": ["OT-3 Standard"], "extents": [477.2, 493.8, 0.0], + "paddingOffsets": { + "rear": -177.42, + "front": 51.8, + "leftSide": 31.88, + "rightSide": -80.32, + }, "mountOffsets": { "left": [-13.5, -60.5, 255.675], "right": [40.5, -60.5, 255.675], 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 c7c67aa7e61..ef2b62c46a4 100644 --- a/api/tests/opentrons/protocol_engine/state/test_module_view.py +++ b/api/tests/opentrons/protocol_engine/state/test_module_view.py @@ -92,6 +92,12 @@ def get_addressable_area_view( "robotType": "OT-3 Standard", "models": ["OT-3 Standard"], "extents": [477.2, 493.8, 0.0], + "paddingOffsets": { + "rear": -177.42, + "front": 51.8, + "leftSide": 31.88, + "rightSide": -80.32, + }, "mountOffsets": { "left": [-13.5, -60.5, 255.675], "right": [40.5, -60.5, 255.675], diff --git a/api/tests/opentrons/protocol_engine/state/test_state_store.py b/api/tests/opentrons/protocol_engine/state/test_state_store.py index 6cd24564795..f764928712c 100644 --- a/api/tests/opentrons/protocol_engine/state/test_state_store.py +++ b/api/tests/opentrons/protocol_engine/state/test_state_store.py @@ -48,6 +48,12 @@ def placeholder_error_recovery_policy(*args: object, **kwargs: object) -> Any: "robotType": "OT-2 Standard", "models": ["OT-2 Standard", "OT-2 Refresh"], "extents": [446.75, 347.5, 0.0], + "paddingOffsets": { + "rear": -35.91, + "front": 31.89, + "leftSide": 0, + "rightSide": 0, + }, "mountOffsets": {"left": [-34.0, 0.0, 0.0], "right": [0.0, 0.0, 0.0]}, }, deck_fixed_labware=[], diff --git a/shared-data/python/opentrons_shared_data/robot/types.py b/shared-data/python/opentrons_shared_data/robot/types.py index e478957bc29..81d27fd34d7 100644 --- a/shared-data/python/opentrons_shared_data/robot/types.py +++ b/shared-data/python/opentrons_shared_data/robot/types.py @@ -37,6 +37,15 @@ class mountOffset(TypedDict): gripper: NotRequired[List[float]] +class paddingOffset(TypedDict): + """The padding offsets for a given robot type based off how far the pipettes can travel beyond the deck extents.""" + + rear: float + front: float + leftSide: float + rightSide: float + + class RobotDefinition(TypedDict): """A python version of the robot definition type.""" @@ -44,4 +53,5 @@ class RobotDefinition(TypedDict): robotType: RobotType models: List[str] extents: List[float] + paddingOffsets: paddingOffset mountOffsets: mountOffset diff --git a/shared-data/robot/definitions/1/ot2.json b/shared-data/robot/definitions/1/ot2.json index 50c6eb4256a..c1199f86045 100644 --- a/shared-data/robot/definitions/1/ot2.json +++ b/shared-data/robot/definitions/1/ot2.json @@ -3,6 +3,12 @@ "robotType": "OT-2 Standard", "models": ["OT-2 Standard", "OT-2 Refresh"], "extents": [446.75, 347.5, 0.0], + "paddingOffsets": { + "rear": -35.91, + "front": 31.89, + "leftSide": 0, + "rightSide": 0 + }, "mountOffsets": { "left": [-34.0, 0.0, 0.0], "right": [0.0, 0.0, 0.0] diff --git a/shared-data/robot/definitions/1/ot3.json b/shared-data/robot/definitions/1/ot3.json index eb3a943d886..05b0db6928c 100644 --- a/shared-data/robot/definitions/1/ot3.json +++ b/shared-data/robot/definitions/1/ot3.json @@ -3,6 +3,12 @@ "robotType": "OT-3 Standard", "models": ["OT-3 Standard"], "extents": [477.2, 493.8, 0.0], + "paddingOffsets": { + "rear": -177.42, + "front": 51.8, + "leftSide": 31.88, + "rightSide": -80.32 + }, "mountOffsets": { "left": [-13.5, -60.5, 255.675], "right": [40.5, -60.5, 255.675], diff --git a/shared-data/robot/schemas/1.json b/shared-data/robot/schemas/1.json index 44e25e6caf5..f0c50eb0ca5 100644 --- a/shared-data/robot/schemas/1.json +++ b/shared-data/robot/schemas/1.json @@ -37,6 +37,29 @@ "description": "The maximum addressable coordinates of the deck without instruments.", "$ref": "#/definitions/xyzArray" }, + "paddingOffsets": { + "description": "The distance from a given edge of a deck extent by which the maximum amount of travel is limited.", + "type": "object", + "required": ["rear", "front", "leftSide", "rightSide"], + "properties": { + "rear": { + "description": "The padding distance from the rear edge of the deck extents which the front nozzles of a pipette must not exceed.", + "type": "number" + }, + "front": { + "description": "The padding distance from the front edge of the deck extents which the rear nozzles of a pipette must not exceed.", + "type": "number" + }, + "leftSide": { + "description": "The padding distance from the left edge of the deck extents which the right-most nozzles of a pipette must not exceed.", + "type": "number" + }, + "rightSide": { + "description": "The padding distance from the right edge of the deck extents which the left-most nozzles of a pipette must not exceed.", + "type": "number" + } + } + }, "mountOffsets": { "description": "The physical mount offsets from the center of the instrument carriage.", "type": "object",