Skip to content

Commit 1e268d3

Browse files
authored
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.
1 parent 5e1dabf commit 1e268d3

File tree

2 files changed

+51
-6
lines changed

2 files changed

+51
-6
lines changed

api/src/opentrons/protocol_engine/execution/tip_handler.py

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -302,12 +302,29 @@ async def verify_tip_presence(
302302
This function will raise an exception if the specified tip presence status
303303
isn't matched.
304304
"""
305+
nozzle_configuration = (
306+
self._state_view.pipettes.state.nozzle_configuration_by_id[pipette_id]
307+
)
308+
309+
# Configuration metrics by which tip presence checking is ignored
310+
unsupported_pipette_types = [8, 96]
311+
unsupported_layout_types = [
312+
NozzleConfigurationType.SINGLE,
313+
NozzleConfigurationType.COLUMN,
314+
]
315+
# NOTE: (09-20-2024) Current on multi-channel pipettes, utilizing less than 4 nozzles risks false positives on the tip presence sensor
316+
supported_partial_nozzle_minimum = 4
317+
305318
if (
306-
self._state_view.pipettes.get_nozzle_layout_type(pipette_id)
307-
== NozzleConfigurationType.SINGLE
308-
and self._state_view.pipettes.get_channels(pipette_id) == 96
319+
nozzle_configuration is not None
320+
and self._state_view.pipettes.get_channels(pipette_id)
321+
in unsupported_pipette_types
322+
and nozzle_configuration.configuration in unsupported_layout_types
323+
and len(nozzle_configuration.map_store) < supported_partial_nozzle_minimum
309324
):
310-
# Tip presence sensing is not supported for single tip pick up on the 96ch Flex Pipette
325+
# 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.
326+
# 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
327+
# for presses with 100% success is below the minimum average achieved press distance for a given multi channel pipette in that configuration.
311328
return
312329
try:
313330
ot3api = ensure_ot3_hardware(hardware_api=self._hardware_api)

api/tests/opentrons/protocol_engine/execution/test_tip_handler.py

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33
from decoy import Decoy
44
from mock import AsyncMock, patch
55

6-
from typing import Dict, ContextManager, Optional
6+
from typing import Dict, ContextManager, Optional, OrderedDict
77
from contextlib import nullcontext as does_not_raise
88

9-
from opentrons.types import Mount, MountType
9+
from opentrons.types import Mount, MountType, Point
1010
from opentrons.hardware_control import API as HardwareAPI
1111
from opentrons.hardware_control.types import TipStateType
1212
from opentrons.hardware_control.protocols.types import OT2RobotType, FlexRobotType
@@ -25,6 +25,8 @@
2525
VirtualTipHandler,
2626
create_tip_handler,
2727
)
28+
from opentrons.hardware_control.nozzle_manager import NozzleMap
29+
from opentrons_shared_data.pipette.pipette_definition import ValidNozzleMaps
2830

2931

3032
@pytest.fixture
@@ -53,6 +55,17 @@ def tip_rack_definition() -> LabwareDefinition:
5355
return LabwareDefinition.construct(namespace="test", version=42) # type: ignore[call-arg]
5456

5557

58+
MOCK_MAP = NozzleMap.build(
59+
physical_nozzles=OrderedDict({"A1": Point(0, 0, 0)}),
60+
physical_rows=OrderedDict({"A": ["A1"]}),
61+
physical_columns=OrderedDict({"1": ["A1"]}),
62+
starting_nozzle="A1",
63+
back_left_nozzle="A1",
64+
front_right_nozzle="A1",
65+
valid_nozzle_maps=ValidNozzleMaps(maps={"Full": ["A1"]}),
66+
)
67+
68+
5669
async def test_create_tip_handler(
5770
decoy: Decoy,
5871
mock_state_view: StateView,
@@ -102,6 +115,9 @@ async def test_flex_pick_up_tip_state(
102115
decoy.when(mock_state_view.pipettes.get_mount("pipette-id")).then_return(
103116
MountType.LEFT
104117
)
118+
decoy.when(mock_state_view.pipettes.state.nozzle_configuration_by_id).then_return(
119+
{"pipette-id": MOCK_MAP}
120+
)
105121
decoy.when(
106122
mock_state_view.geometry.get_nominal_tip_geometry(
107123
pipette_id="pipette-id",
@@ -171,6 +187,10 @@ async def test_pick_up_tip(
171187
MountType.LEFT
172188
)
173189

190+
decoy.when(mock_state_view.pipettes.state.nozzle_configuration_by_id).then_return(
191+
{"pipette-id": MOCK_MAP}
192+
)
193+
174194
decoy.when(
175195
mock_state_view.geometry.get_nominal_tip_geometry(
176196
pipette_id="pipette-id",
@@ -225,6 +245,9 @@ async def test_drop_tip(
225245
decoy.when(mock_state_view.pipettes.get_mount("pipette-id")).then_return(
226246
MountType.RIGHT
227247
)
248+
decoy.when(mock_state_view.pipettes.state.nozzle_configuration_by_id).then_return(
249+
{"pipette-id": MOCK_MAP}
250+
)
228251

229252
await subject.drop_tip(pipette_id="pipette-id", home_after=True)
230253

@@ -499,6 +522,11 @@ async def test_verify_tip_presence_on_ot3(
499522
decoy.when(mock_state_view.pipettes.get_mount("pipette-id")).then_return(
500523
MountType.LEFT
501524
)
525+
526+
decoy.when(
527+
mock_state_view.pipettes.state.nozzle_configuration_by_id
528+
).then_return({"pipette-id": MOCK_MAP})
529+
502530
await subject.verify_tip_presence("pipette-id", expected, None)
503531

504532
decoy.verify(

0 commit comments

Comments
 (0)