diff --git a/api/src/opentrons/protocol_api/core/engine/instrument.py b/api/src/opentrons/protocol_api/core/engine/instrument.py index 6f9fed991f9..2ed46bd4b91 100644 --- a/api/src/opentrons/protocol_api/core/engine/instrument.py +++ b/api/src/opentrons/protocol_api/core/engine/instrument.py @@ -391,6 +391,7 @@ def touch_tip( radius: float, z_offset: float, speed: float, + mm_from_edge: Optional[float] = None, ) -> None: """Touch pipette tip to edges of the well @@ -400,7 +401,11 @@ def touch_tip( radius: Percentage modifier for well radius to touch. z_offset: Vertical offset for pipette tip during touch tip. speed: Speed for the touch tip movements. + mm_from_edge: Offset from the edge of the well to move to. Requires a radius of 1. """ + if mm_from_edge is not None and radius != 1.0: + raise ValueError("radius must be set to 1.0 if mm_from_edge is provided.") + well_name = well_core.get_name() labware_id = well_core.labware_id @@ -422,6 +427,7 @@ def touch_tip( wellName=well_name, wellLocation=well_location, radius=radius, + mmFromEdge=mm_from_edge, speed=speed, ) ) diff --git a/api/src/opentrons/protocol_api/core/engine/transfer_components_executor.py b/api/src/opentrons/protocol_api/core/engine/transfer_components_executor.py index 87e41ee98dc..7dc332e6a37 100644 --- a/api/src/opentrons/protocol_api/core/engine/transfer_components_executor.py +++ b/api/src/opentrons/protocol_api/core/engine/transfer_components_executor.py @@ -293,13 +293,13 @@ def retract_after_aspiration(self, volume: float) -> None: and touch_tip_props.z_offset is not None and touch_tip_props.mm_to_edge is not None ) - # TODO: update this once touch tip has mmToEdge self._instrument.touch_tip( location=retract_location, well_core=self._target_well, radius=1, z_offset=touch_tip_props.z_offset, speed=touch_tip_props.speed, + mm_from_edge=touch_tip_props.mm_to_edge, ) self._instrument.move_to( location=retract_location, @@ -460,8 +460,7 @@ def _do_touch_tip_and_air_gap( and touch_tip_props.z_offset is not None and touch_tip_props.mm_to_edge is not None ) - # TODO: update this once touch tip has mmToEdge - # Also, check that when blow out is a non-dest-well, + # TODO:, check that when blow out is a non-dest-well, # whether the touch tip params from transfer props should be used for # both dest-well touch tip and non-dest-well touch tip. if well is not None and location is not None: @@ -472,6 +471,7 @@ def _do_touch_tip_and_air_gap( radius=1, z_offset=touch_tip_props.z_offset, speed=touch_tip_props.speed, + mm_from_edge=touch_tip_props.mm_to_edge, ) except TouchTipDisabledError: # TODO: log a warning diff --git a/api/src/opentrons/protocol_api/core/instrument.py b/api/src/opentrons/protocol_api/core/instrument.py index de918405fdc..04f069e75f4 100644 --- a/api/src/opentrons/protocol_api/core/instrument.py +++ b/api/src/opentrons/protocol_api/core/instrument.py @@ -103,6 +103,7 @@ def touch_tip( radius: float, z_offset: float, speed: float, + mm_from_edge: Optional[float] = None, ) -> None: ... diff --git a/api/src/opentrons/protocol_api/core/legacy/legacy_instrument_core.py b/api/src/opentrons/protocol_api/core/legacy/legacy_instrument_core.py index bbc02702f93..ff25a20d77c 100644 --- a/api/src/opentrons/protocol_api/core/legacy/legacy_instrument_core.py +++ b/api/src/opentrons/protocol_api/core/legacy/legacy_instrument_core.py @@ -180,11 +180,14 @@ def touch_tip( radius: float, z_offset: float, speed: float, + mm_from_edge: Optional[float] = None, ) -> None: """ Touch the pipette tip to the sides of a well, with the intent of removing left-over droplets """ + if mm_from_edge is not None: + raise APIVersionError(api_element="mm_from_edge argument") # TODO al 20201110 - build_edges relies on where being a Well. This is # an unpleasant compromise until refactoring build_edges to support # LegacyWellCore. diff --git a/api/src/opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py b/api/src/opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py index 3a898c27a42..4c8f68ecb86 100644 --- a/api/src/opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py +++ b/api/src/opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py @@ -180,7 +180,10 @@ def touch_tip( radius: float, z_offset: float, speed: float, + mm_from_edge: Optional[float] = None, ) -> None: + if mm_from_edge is not None: + raise APIVersionError(api_element="mm_from_edge argument") self.move_to(location) def pick_up_tip( diff --git a/api/tests/opentrons/conftest.py b/api/tests/opentrons/conftest.py index eaaf3f030d6..60386d5a140 100755 --- a/api/tests/opentrons/conftest.py +++ b/api/tests/opentrons/conftest.py @@ -947,7 +947,7 @@ def maximal_liquid_class_def() -> LiquidClassSchemaV1: touchTip=TouchTipProperties( enable=True, params=LiquidClassTouchTipParams( - zOffset=-1, mmToEdge=0.5, speed=30 + zOffset=-1, mmToEdge=0.75, speed=30 ), ), delay=DelayProperties( diff --git a/api/tests/opentrons/protocol_api/core/engine/test_instrument_core.py b/api/tests/opentrons/protocol_api/core/engine/test_instrument_core.py index d92e3d6050a..9a3ecaad69b 100644 --- a/api/tests/opentrons/protocol_api/core/engine/test_instrument_core.py +++ b/api/tests/opentrons/protocol_api/core/engine/test_instrument_core.py @@ -1303,12 +1303,84 @@ def test_touch_tip( ), radius=1.23, speed=7.89, + mmFromEdge=None, ) ), mock_protocol_core.set_last_location(location=location, mount=Mount.LEFT), ) +def test_touch_tip_with_mm_from_edge( + decoy: Decoy, + subject: InstrumentCore, + mock_engine_client: EngineClient, + mock_protocol_core: ProtocolCore, +) -> None: + """It should touch the tip to the edges of the well with mm_from_edge.""" + location = Location(point=Point(1, 2, 3), labware=None) + + well_core = WellCore( + name="my cool well", labware_id="123abc", engine_client=mock_engine_client + ) + subject.touch_tip( + location=location, + well_core=well_core, + radius=1.0, + z_offset=4.56, + speed=7.89, + mm_from_edge=9.87, + ) + + decoy.verify( + pipette_movement_conflict.check_safe_for_pipette_movement( + engine_state=mock_engine_client.state, + pipette_id="abc123", + labware_id="123abc", + well_name="my cool well", + well_location=WellLocation( + origin=WellOrigin.TOP, offset=WellOffset(x=0, y=0, z=4.56) + ), + ), + mock_engine_client.execute_command( + cmd.TouchTipParams( + pipetteId="abc123", + labwareId="123abc", + wellName="my cool well", + wellLocation=WellLocation( + origin=WellOrigin.TOP, offset=WellOffset(x=0, y=0, z=4.56) + ), + radius=1.0, + speed=7.89, + mmFromEdge=9.87, + ) + ), + mock_protocol_core.set_last_location(location=location, mount=Mount.LEFT), + ) + + +def test_touch_tip_raises_with_radius_and_mm_from_edge( + decoy: Decoy, + subject: InstrumentCore, + mock_engine_client: EngineClient, + mock_protocol_core: ProtocolCore, +) -> None: + """It should raise if a value of not 1.0 and a mm_from_edge argument is given.""" + location = Location(point=Point(1, 2, 3), labware=None) + + well_core = WellCore( + name="my cool well", labware_id="123abc", engine_client=mock_engine_client + ) + with pytest.raises(ValueError): + subject.touch_tip( + location=location, + well_core=well_core, + radius=1.23, + z_offset=4.56, + speed=7.89, + mm_from_edge=9.87, + ) + + def test_has_tip( decoy: Decoy, subject: InstrumentCore, diff --git a/api/tests/opentrons/protocol_api/core/engine/test_transfer_components_executor.py b/api/tests/opentrons/protocol_api/core/engine/test_transfer_components_executor.py index 6eecfccf882..87870468590 100644 --- a/api/tests/opentrons/protocol_api/core/engine/test_transfer_components_executor.py +++ b/api/tests/opentrons/protocol_api/core/engine/test_transfer_components_executor.py @@ -449,7 +449,8 @@ def test_retract_after_aspiration( mock_instrument_core.touch_tip( location=Location(Point(x=4, y=4, z=4), labware=None), well_core=source_well, - radius=1, # Update this to use mmToEdge once implemented + radius=1, + mm_from_edge=0.5, z_offset=-1, speed=30, ), @@ -599,6 +600,7 @@ def test_retract_after_dispense_with_blowout_in_source( location=Location(Point(12, 24, 36), labware=None), well_core=dest_well, radius=1, + mm_from_edge=0.75, z_offset=-1, speed=30, ), @@ -623,6 +625,7 @@ def test_retract_after_dispense_with_blowout_in_source( location=Location(Point(10, 20, 30), labware=None), well_core=source_well, radius=1, + mm_from_edge=0.75, z_offset=-1, speed=30, ), @@ -711,6 +714,7 @@ def test_retract_after_dispense_with_blowout_in_destination( location=Location(Point(12, 24, 36), labware=None), well_core=dest_well, radius=1, + mm_from_edge=0.75, z_offset=-1, speed=30, ), @@ -788,6 +792,7 @@ def test_retract_after_dispense_with_blowout_in_trash_well( location=Location(Point(12, 24, 36), labware=None), well_core=dest_well, radius=1, + mm_from_edge=0.75, z_offset=-1, speed=30, ), @@ -812,6 +817,7 @@ def test_retract_after_dispense_with_blowout_in_trash_well( location=trash_location, well_core=trash_well_core, radius=1, + mm_from_edge=0.75, z_offset=-1, speed=30, ), @@ -886,6 +892,7 @@ def test_retract_after_dispense_with_blowout_in_disposal_location( location=Location(Point(12, 24, 36), labware=None), well_core=dest_well, radius=1, + mm_from_edge=0.75, z_offset=-1, speed=30, ),