From 1e268d37237bc8bdc39a62d2b921bf8a50884541 Mon Sep 17 00:00:00 2001 From: CaseyBatten Date: Fri, 20 Sep 2024 11:30:40 -0400 Subject: [PATCH] fix(api): Disable tip presence check on 8ch single and partial 2 thru 3 nozzle (#16312) Covers RABR-623, RABR-624 Disable tip presence sensing on the 8ch Flex pipette for Single tip configuration and for Partial Column for 1-3 tips. --- .../protocol_engine/execution/tip_handler.py | 25 ++++++++++++--- .../execution/test_tip_handler.py | 32 +++++++++++++++++-- 2 files changed, 51 insertions(+), 6 deletions(-) diff --git a/api/src/opentrons/protocol_engine/execution/tip_handler.py b/api/src/opentrons/protocol_engine/execution/tip_handler.py index 937e4abf9d8..af36137c842 100644 --- a/api/src/opentrons/protocol_engine/execution/tip_handler.py +++ b/api/src/opentrons/protocol_engine/execution/tip_handler.py @@ -302,12 +302,29 @@ async def verify_tip_presence( This function will raise an exception if the specified tip presence status isn't matched. """ + nozzle_configuration = ( + self._state_view.pipettes.state.nozzle_configuration_by_id[pipette_id] + ) + + # Configuration metrics by which tip presence checking is ignored + unsupported_pipette_types = [8, 96] + unsupported_layout_types = [ + NozzleConfigurationType.SINGLE, + NozzleConfigurationType.COLUMN, + ] + # NOTE: (09-20-2024) Current on multi-channel pipettes, utilizing less than 4 nozzles risks false positives on the tip presence sensor + supported_partial_nozzle_minimum = 4 + if ( - self._state_view.pipettes.get_nozzle_layout_type(pipette_id) - == NozzleConfigurationType.SINGLE - and self._state_view.pipettes.get_channels(pipette_id) == 96 + nozzle_configuration is not None + and self._state_view.pipettes.get_channels(pipette_id) + in unsupported_pipette_types + and nozzle_configuration.configuration in unsupported_layout_types + and len(nozzle_configuration.map_store) < supported_partial_nozzle_minimum ): - # Tip presence sensing is not supported for single tip pick up on the 96ch Flex Pipette + # Tip presence sensing is not supported for single tip pick up on the 96ch Flex Pipette, nor with single and some partial layous of the 8ch Flex Pipette. + # This is due in part to a press distance tolerance which creates a risk case for false positives. In the case of single tip, the mechanical tolerance + # for presses with 100% success is below the minimum average achieved press distance for a given multi channel pipette in that configuration. return try: ot3api = ensure_ot3_hardware(hardware_api=self._hardware_api) diff --git a/api/tests/opentrons/protocol_engine/execution/test_tip_handler.py b/api/tests/opentrons/protocol_engine/execution/test_tip_handler.py index dfd02e9dfd5..b49f48bdb21 100644 --- a/api/tests/opentrons/protocol_engine/execution/test_tip_handler.py +++ b/api/tests/opentrons/protocol_engine/execution/test_tip_handler.py @@ -3,10 +3,10 @@ from decoy import Decoy from mock import AsyncMock, patch -from typing import Dict, ContextManager, Optional +from typing import Dict, ContextManager, Optional, OrderedDict from contextlib import nullcontext as does_not_raise -from opentrons.types import Mount, MountType +from opentrons.types import Mount, MountType, Point from opentrons.hardware_control import API as HardwareAPI from opentrons.hardware_control.types import TipStateType from opentrons.hardware_control.protocols.types import OT2RobotType, FlexRobotType @@ -25,6 +25,8 @@ VirtualTipHandler, create_tip_handler, ) +from opentrons.hardware_control.nozzle_manager import NozzleMap +from opentrons_shared_data.pipette.pipette_definition import ValidNozzleMaps @pytest.fixture @@ -53,6 +55,17 @@ def tip_rack_definition() -> LabwareDefinition: return LabwareDefinition.construct(namespace="test", version=42) # type: ignore[call-arg] +MOCK_MAP = NozzleMap.build( + physical_nozzles=OrderedDict({"A1": Point(0, 0, 0)}), + physical_rows=OrderedDict({"A": ["A1"]}), + physical_columns=OrderedDict({"1": ["A1"]}), + starting_nozzle="A1", + back_left_nozzle="A1", + front_right_nozzle="A1", + valid_nozzle_maps=ValidNozzleMaps(maps={"Full": ["A1"]}), +) + + async def test_create_tip_handler( decoy: Decoy, mock_state_view: StateView, @@ -102,6 +115,9 @@ async def test_flex_pick_up_tip_state( decoy.when(mock_state_view.pipettes.get_mount("pipette-id")).then_return( MountType.LEFT ) + decoy.when(mock_state_view.pipettes.state.nozzle_configuration_by_id).then_return( + {"pipette-id": MOCK_MAP} + ) decoy.when( mock_state_view.geometry.get_nominal_tip_geometry( pipette_id="pipette-id", @@ -171,6 +187,10 @@ async def test_pick_up_tip( MountType.LEFT ) + decoy.when(mock_state_view.pipettes.state.nozzle_configuration_by_id).then_return( + {"pipette-id": MOCK_MAP} + ) + decoy.when( mock_state_view.geometry.get_nominal_tip_geometry( pipette_id="pipette-id", @@ -225,6 +245,9 @@ async def test_drop_tip( decoy.when(mock_state_view.pipettes.get_mount("pipette-id")).then_return( MountType.RIGHT ) + decoy.when(mock_state_view.pipettes.state.nozzle_configuration_by_id).then_return( + {"pipette-id": MOCK_MAP} + ) await subject.drop_tip(pipette_id="pipette-id", home_after=True) @@ -499,6 +522,11 @@ async def test_verify_tip_presence_on_ot3( decoy.when(mock_state_view.pipettes.get_mount("pipette-id")).then_return( MountType.LEFT ) + + decoy.when( + mock_state_view.pipettes.state.nozzle_configuration_by_id + ).then_return({"pipette-id": MOCK_MAP}) + await subject.verify_tip_presence("pipette-id", expected, None) decoy.verify(