From 4a57bf38450f99cad26c5645a7178641ac3bc16d Mon Sep 17 00:00:00 2001 From: Seth Foster Date: Fri, 28 Mar 2025 16:47:48 -0400 Subject: [PATCH 1/3] feat(api): prepare for aspirate before airgap But do it in the API. --- api/src/opentrons/protocol_api/instrument_context.py | 1 + api/tests/opentrons/protocol_api/test_instrument_context.py | 1 + 2 files changed, 2 insertions(+) diff --git a/api/src/opentrons/protocol_api/instrument_context.py b/api/src/opentrons/protocol_api/instrument_context.py index a655cc372850..0ac6bd994b15 100644 --- a/api/src/opentrons/protocol_api/instrument_context.py +++ b/api/src/opentrons/protocol_api/instrument_context.py @@ -778,6 +778,7 @@ def air_gap( target = loc.labware.as_well().top(height) self.move_to(target, publish=False) if self.api_version >= _AIR_GAP_TRACKING_ADDED_IN: + self._core.prepare_to_aspirate() c_vol = self._core.get_available_volume() if volume is None else volume flow_rate = self._core.get_aspirate_flow_rate() self._core.air_gap_in_place(c_vol, flow_rate) diff --git a/api/tests/opentrons/protocol_api/test_instrument_context.py b/api/tests/opentrons/protocol_api/test_instrument_context.py index c9c82046113a..1b8234c4eb21 100644 --- a/api/tests/opentrons/protocol_api/test_instrument_context.py +++ b/api/tests/opentrons/protocol_api/test_instrument_context.py @@ -1786,6 +1786,7 @@ def test_air_gap_uses_air_gap( subject.air_gap(volume=10, height=5) decoy.verify(mock_move_to(top_location, publish=False)) + decoy.verify(mock_instrument_core.prepare_to_aspirate()) decoy.verify(mock_instrument_core.air_gap_in_place(10, 11)) From ace126d510fc072a31815d6bcf4c3c00145c95ba Mon Sep 17 00:00:00 2001 From: Seth Foster Date: Fri, 28 Mar 2025 17:00:52 -0400 Subject: [PATCH 2/3] fix(api): noop prepare to aspirate if unnecessary Otherwise this could actually cause a spurious dispense. --- .../commands/prepare_to_aspirate.py | 20 ++++++++++-- .../commands/test_prepare_to_aspirate.py | 31 ++++++++++++++++++- 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/api/src/opentrons/protocol_engine/commands/prepare_to_aspirate.py b/api/src/opentrons/protocol_engine/commands/prepare_to_aspirate.py index bf6b23d5d316..d43c906bb35f 100644 --- a/api/src/opentrons/protocol_engine/commands/prepare_to_aspirate.py +++ b/api/src/opentrons/protocol_engine/commands/prepare_to_aspirate.py @@ -5,7 +5,12 @@ from typing import TYPE_CHECKING, Optional, Type, Union from typing_extensions import Literal -from .pipetting_common import OverpressureError, PipetteIdMixin, prepare_for_aspirate +from .pipetting_common import ( + OverpressureError, + PipetteIdMixin, + prepare_for_aspirate, + EmptyResult, +) from .command import ( AbstractCommandImpl, BaseCommand, @@ -66,6 +71,14 @@ def _transform_result( async def execute(self, params: PrepareToAspirateParams) -> _ExecuteReturn: """Prepare the pipette to aspirate.""" + ready_to_aspirate = self._pipetting_handler.get_is_ready_to_aspirate( + pipette_id=params.pipetteId + ) + if ready_to_aspirate: + return SuccessData( + public=PrepareToAspirateResult(), + ) + current_position = await self._gantry_mover.get_position(params.pipetteId) prepare_result = await prepare_for_aspirate( pipette_id=params.pipetteId, @@ -79,6 +92,7 @@ async def execute(self, params: PrepareToAspirateParams) -> _ExecuteReturn: ) }, ) + if isinstance(prepare_result, DefinedErrorData): return prepare_result else: @@ -99,9 +113,9 @@ class PrepareToAspirate( params: PrepareToAspirateParams result: Optional[PrepareToAspirateResult] = None - _ImplementationCls: Type[ + _ImplementationCls: Type[PrepareToAspirateImplementation] = ( PrepareToAspirateImplementation - ] = PrepareToAspirateImplementation + ) class PrepareToAspirateCreate(BaseCommandCreate[PrepareToAspirateParams]): diff --git a/api/tests/opentrons/protocol_engine/commands/test_prepare_to_aspirate.py b/api/tests/opentrons/protocol_engine/commands/test_prepare_to_aspirate.py index a113d2670fa8..70edc48b6507 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_prepare_to_aspirate.py +++ b/api/tests/opentrons/protocol_engine/commands/test_prepare_to_aspirate.py @@ -1,4 +1,5 @@ """Test prepare to aspirate commands.""" + from datetime import datetime from opentrons.types import Point import pytest @@ -42,7 +43,9 @@ async def test_prepare_to_aspirate_implementation( """A PrepareToAspirate command should have an executing implementation.""" data = PrepareToAspirateParams(pipetteId="some id") position = Point(x=1, y=2, z=3) - + decoy.when(pipetting.get_is_ready_to_aspirate(pipette_id="some id")).then_return( + False + ) decoy.when(await pipetting.prepare_for_aspirate(pipette_id="some id")).then_return( None ) @@ -81,6 +84,9 @@ async def test_overpressure_error( data = PrepareToAspirateParams( pipetteId=pipette_id, ) + decoy.when(pipetting.get_is_ready_to_aspirate(pipette_id="pipette-id")).then_return( + False + ) decoy.when( await pipetting.prepare_for_aspirate( @@ -107,3 +113,26 @@ async def test_overpressure_error( ) ), ) + + +async def test_prepare_noops_if_prepared( + decoy: Decoy, + gantry_mover: GantryMover, + pipetting: PipettingHandler, + subject: PrepareToAspirateImplementation, + model_utils: ModelUtils, +) -> None: + """It should do nothing if the pipette does not need to be prepared.""" + data = PrepareToAspirateParams(pipetteId="some id") + position = Point(x=1, y=2, z=3) + decoy.when(pipetting.get_is_ready_to_aspirate(pipette_id="some id")).then_return( + True + ) + decoy.when(await gantry_mover.get_position("some id")).then_return(position) + + result = await subject.execute(data) + decoy.verify(await pipetting.prepare_for_aspirate(pipette_id="some id"), times=0) + assert result == SuccessData( + public=PrepareToAspirateResult(), + state_update=update_types.StateUpdate(), + ) From 6a55882ba74f5671bb0317077ab0bbd43f5c0bba Mon Sep 17 00:00:00 2001 From: Seth Foster Date: Mon, 31 Mar 2025 09:30:57 -0400 Subject: [PATCH 3/3] format --- .../protocol_engine/commands/prepare_to_aspirate.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/api/src/opentrons/protocol_engine/commands/prepare_to_aspirate.py b/api/src/opentrons/protocol_engine/commands/prepare_to_aspirate.py index d43c906bb35f..46be3e0c6d2a 100644 --- a/api/src/opentrons/protocol_engine/commands/prepare_to_aspirate.py +++ b/api/src/opentrons/protocol_engine/commands/prepare_to_aspirate.py @@ -9,7 +9,6 @@ OverpressureError, PipetteIdMixin, prepare_for_aspirate, - EmptyResult, ) from .command import ( AbstractCommandImpl, @@ -113,9 +112,9 @@ class PrepareToAspirate( params: PrepareToAspirateParams result: Optional[PrepareToAspirateResult] = None - _ImplementationCls: Type[PrepareToAspirateImplementation] = ( + _ImplementationCls: Type[ PrepareToAspirateImplementation - ) + ] = PrepareToAspirateImplementation class PrepareToAspirateCreate(BaseCommandCreate[PrepareToAspirateParams]):