diff --git a/api/src/opentrons/protocol_engine/commands/aspirate_in_place.py b/api/src/opentrons/protocol_engine/commands/aspirate_in_place.py index 9a6306955f5..8caf9173d11 100644 --- a/api/src/opentrons/protocol_engine/commands/aspirate_in_place.py +++ b/api/src/opentrons/protocol_engine/commands/aspirate_in_place.py @@ -93,6 +93,10 @@ async def execute(self, params: AspirateInPlaceParams) -> _ExecuteReturn: " The first aspirate following a blow-out must be from a specific well" " so the plunger can be reset in a known safe position." ) + + state_update = StateUpdate() + current_location = self._state_view.pipettes.get_current_location() + try: current_position = await self._gantry_mover.get_position(params.pipetteId) volume = await self._pipetting.aspirate_in_place( @@ -103,6 +107,15 @@ async def execute(self, params: AspirateInPlaceParams) -> _ExecuteReturn: ) except PipetteOverpressureError as e: # TODO(pbm, 10-24-24): if location is a well, get new tip and LiquidProbe in error recovery to reestablish well liquid level + if ( + isinstance(current_location, CurrentWell) + and current_location.pipette_id == params.pipetteId + ): + state_update.set_liquid_operated( + labware_id=current_location.labware_id, + well_name=current_location.well_name, + volume=None, + ) return DefinedErrorData( public=OverpressureError( id=self._model_utils.generate_id(), @@ -124,14 +137,13 @@ async def execute(self, params: AspirateInPlaceParams) -> _ExecuteReturn: } ), ), + state_update=state_update, ) else: - current_location = self._state_view.pipettes.get_current_location() if ( isinstance(current_location, CurrentWell) and current_location.pipette_id == params.pipetteId ): - state_update = StateUpdate() state_update.set_liquid_operated( labware_id=current_location.labware_id, well_name=current_location.well_name, diff --git a/api/src/opentrons/protocol_engine/commands/dispense_in_place.py b/api/src/opentrons/protocol_engine/commands/dispense_in_place.py index db729676209..999e7d83ac4 100644 --- a/api/src/opentrons/protocol_engine/commands/dispense_in_place.py +++ b/api/src/opentrons/protocol_engine/commands/dispense_in_place.py @@ -74,6 +74,8 @@ def __init__( async def execute(self, params: DispenseInPlaceParams) -> _ExecuteReturn: """Dispense without moving the pipette.""" + state_update = StateUpdate() + current_location = self._state_view.pipettes.get_current_location() try: current_position = await self._gantry_mover.get_position(params.pipetteId) volume = await self._pipetting.dispense_in_place( @@ -84,6 +86,15 @@ async def execute(self, params: DispenseInPlaceParams) -> _ExecuteReturn: ) except PipetteOverpressureError as e: # TODO(pbm, 10-24-24): if location is a well, get new tip and LiquidProbe in error recovery to reestablish well liquid level + if ( + isinstance(current_location, CurrentWell) + and current_location.pipette_id == params.pipetteId + ): + state_update.set_liquid_operated( + labware_id=current_location.labware_id, + well_name=current_location.well_name, + volume=None, + ) return DefinedErrorData( public=OverpressureError( id=self._model_utils.generate_id(), @@ -105,14 +116,13 @@ async def execute(self, params: DispenseInPlaceParams) -> _ExecuteReturn: } ), ), + state_update=state_update, ) else: - current_location = self._state_view.pipettes.get_current_location() if ( isinstance(current_location, CurrentWell) and current_location.pipette_id == params.pipetteId ): - state_update = StateUpdate() state_update.set_liquid_operated( labware_id=current_location.labware_id, well_name=current_location.well_name, diff --git a/api/tests/opentrons/protocol_engine/commands/test_aspirate_in_place.py b/api/tests/opentrons/protocol_engine/commands/test_aspirate_in_place.py index 1af43324d19..645c7ef3fd5 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_aspirate_in_place.py +++ b/api/tests/opentrons/protocol_engine/commands/test_aspirate_in_place.py @@ -21,7 +21,11 @@ from opentrons.protocol_engine.resources import ModelUtils from opentrons.protocol_engine.state.state import StateStore from opentrons.protocol_engine.commands.pipetting_common import OverpressureError -from opentrons.protocol_engine.types import CurrentWell +from opentrons.protocol_engine.types import ( + CurrentWell, + CurrentPipetteLocation, + CurrentAddressableArea, +) from opentrons.protocol_engine.state import update_types @@ -63,6 +67,22 @@ def subject( ) +@pytest.mark.parametrize( + "location,stateupdateLabware,stateupdateWell", + [ + ( + CurrentWell( + pipette_id="pipette-id-abc", + labware_id="labware-id-1", + well_name="well-name-1", + ), + "labware-id-1", + "well-name-1", + ), + (None, None, None), + (CurrentAddressableArea("pipette-id-abc", "addressable-area-1"), None, None), + ], +) async def test_aspirate_in_place_implementation( decoy: Decoy, pipetting: PipettingHandler, @@ -70,6 +90,9 @@ async def test_aspirate_in_place_implementation( hardware_api: HardwareAPI, mock_command_note_adder: CommandNoteAdder, subject: AspirateInPlaceImplementation, + location: CurrentPipetteLocation | None, + stateupdateLabware: str, + stateupdateWell: str, ) -> None: """It should aspirate in place.""" data = AspirateInPlaceParams( @@ -93,25 +116,27 @@ async def test_aspirate_in_place_implementation( ) ).then_return(123) - decoy.when(state_store.pipettes.get_current_location()).then_return( - CurrentWell( - pipette_id="pipette-id-abc", - labware_id="labware-id-1", - well_name="well-name-1", - ) - ) + decoy.when(state_store.pipettes.get_current_location()).then_return(location) result = await subject.execute(params=data) - assert result == SuccessData( - public=AspirateInPlaceResult(volume=123), - private=None, - state_update=update_types.StateUpdate( - liquid_operated=update_types.LiquidOperatedUpdate( - labware_id="labware-id-1", well_name="well-name-1", volume=-123 - ) - ), - ) + if isinstance(location, CurrentWell): + assert result == SuccessData( + public=AspirateInPlaceResult(volume=123), + private=None, + state_update=update_types.StateUpdate( + liquid_operated=update_types.LiquidOperatedUpdate( + labware_id=stateupdateLabware, + well_name=stateupdateWell, + volume=-123, + ) + ), + ) + else: + assert result == SuccessData( + public=AspirateInPlaceResult(volume=123), + private=None, + ) async def test_handle_aspirate_in_place_request_not_ready_to_aspirate( @@ -171,6 +196,22 @@ async def test_aspirate_raises_volume_error( await subject.execute(data) +@pytest.mark.parametrize( + "location,stateupdateLabware,stateupdateWell", + [ + ( + CurrentWell( + pipette_id="pipette-id", + labware_id="labware-id-1", + well_name="well-name-1", + ), + "labware-id-1", + "well-name-1", + ), + (None, None, None), + (CurrentAddressableArea("pipette-id", "addressable-area-1"), None, None), + ], +) async def test_overpressure_error( decoy: Decoy, gantry_mover: GantryMover, @@ -178,6 +219,10 @@ async def test_overpressure_error( subject: AspirateInPlaceImplementation, model_utils: ModelUtils, mock_command_note_adder: CommandNoteAdder, + state_store: StateStore, + location: CurrentPipetteLocation | None, + stateupdateLabware: str, + stateupdateWell: str, ) -> None: """It should return an overpressure error if the hardware API indicates that.""" pipette_id = "pipette-id" @@ -209,14 +254,32 @@ async def test_overpressure_error( decoy.when(model_utils.generate_id()).then_return(error_id) decoy.when(model_utils.get_timestamp()).then_return(error_timestamp) decoy.when(await gantry_mover.get_position(pipette_id)).then_return(position) + decoy.when(state_store.pipettes.get_current_location()).then_return(location) result = await subject.execute(data) - assert result == DefinedErrorData( - public=OverpressureError.construct( - id=error_id, - createdAt=error_timestamp, - wrappedErrors=[matchers.Anything()], - errorInfo={"retryLocation": (position.x, position.y, position.z)}, - ), - ) + if isinstance(location, CurrentWell): + assert result == DefinedErrorData( + public=OverpressureError.construct( + id=error_id, + createdAt=error_timestamp, + wrappedErrors=[matchers.Anything()], + errorInfo={"retryLocation": (position.x, position.y, position.z)}, + ), + state_update=update_types.StateUpdate( + liquid_operated=update_types.LiquidOperatedUpdate( + labware_id=stateupdateLabware, + well_name=stateupdateWell, + volume=None, + ) + ), + ) + else: + assert result == DefinedErrorData( + public=OverpressureError.construct( + id=error_id, + createdAt=error_timestamp, + wrappedErrors=[matchers.Anything()], + errorInfo={"retryLocation": (position.x, position.y, position.z)}, + ), + ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_dispense_in_place.py b/api/tests/opentrons/protocol_engine/commands/test_dispense_in_place.py index 92637e8f300..c0db7eb4bc7 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_dispense_in_place.py +++ b/api/tests/opentrons/protocol_engine/commands/test_dispense_in_place.py @@ -18,7 +18,11 @@ from opentrons.protocol_engine.commands.pipetting_common import OverpressureError from opentrons.protocol_engine.resources import ModelUtils from opentrons.protocol_engine.state.state import StateStore -from opentrons.protocol_engine.types import CurrentWell +from opentrons.protocol_engine.types import ( + CurrentWell, + CurrentPipetteLocation, + CurrentAddressableArea, +) from opentrons.protocol_engine.state import update_types @@ -28,12 +32,31 @@ def state_store(decoy: Decoy) -> StateStore: return decoy.mock(cls=StateStore) +@pytest.mark.parametrize( + "location,stateupdateLabware,stateupdateWell", + [ + ( + CurrentWell( + pipette_id="pipette-id-abc", + labware_id="labware-id-1", + well_name="well-name-1", + ), + "labware-id-1", + "well-name-1", + ), + (None, None, None), + (CurrentAddressableArea("pipette-id-abc", "addressable-area-1"), None, None), + ], +) async def test_dispense_in_place_implementation( decoy: Decoy, pipetting: PipettingHandler, state_store: StateStore, gantry_mover: GantryMover, model_utils: ModelUtils, + location: CurrentPipetteLocation | None, + stateupdateLabware: str, + stateupdateWell: str, ) -> None: """It should dispense in place.""" subject = DispenseInPlaceImplementation( @@ -55,33 +78,52 @@ async def test_dispense_in_place_implementation( ) ).then_return(42) - decoy.when(state_store.pipettes.get_current_location()).then_return( - CurrentWell( - pipette_id="pipette-id-abc", - labware_id="labware-id-1", - well_name="well-name-1", - ) - ) + decoy.when(state_store.pipettes.get_current_location()).then_return(location) result = await subject.execute(data) - assert result == SuccessData( - public=DispenseInPlaceResult(volume=42), - private=None, - state_update=update_types.StateUpdate( - liquid_operated=update_types.LiquidOperatedUpdate( - labware_id="labware-id-1", well_name="well-name-1", volume=42 - ) - ), - ) + if isinstance(location, CurrentWell): + assert result == SuccessData( + public=DispenseInPlaceResult(volume=42), + private=None, + state_update=update_types.StateUpdate( + liquid_operated=update_types.LiquidOperatedUpdate( + labware_id=stateupdateLabware, well_name=stateupdateWell, volume=42 + ) + ), + ) + else: + assert result == SuccessData( + public=DispenseInPlaceResult(volume=42), + private=None, + ) +@pytest.mark.parametrize( + "location,stateupdateLabware,stateupdateWell", + [ + ( + CurrentWell( + pipette_id="pipette-id", + labware_id="labware-id-1", + well_name="well-name-1", + ), + "labware-id-1", + "well-name-1", + ), + (None, None, None), + (CurrentAddressableArea("pipette-id", "addressable-area-1"), None, None), + ], +) async def test_overpressure_error( decoy: Decoy, gantry_mover: GantryMover, pipetting: PipettingHandler, state_store: StateStore, model_utils: ModelUtils, + location: CurrentPipetteLocation | None, + stateupdateLabware: str, + stateupdateWell: str, ) -> None: """It should return an overpressure error if the hardware API indicates that.""" subject = DispenseInPlaceImplementation( @@ -117,14 +159,32 @@ async def test_overpressure_error( decoy.when(model_utils.generate_id()).then_return(error_id) decoy.when(model_utils.get_timestamp()).then_return(error_timestamp) decoy.when(await gantry_mover.get_position(pipette_id)).then_return(position) + decoy.when(state_store.pipettes.get_current_location()).then_return(location) result = await subject.execute(data) - assert result == DefinedErrorData( - public=OverpressureError.construct( - id=error_id, - createdAt=error_timestamp, - wrappedErrors=[matchers.Anything()], - errorInfo={"retryLocation": (position.x, position.y, position.z)}, - ), - ) + if isinstance(location, CurrentWell): + assert result == DefinedErrorData( + public=OverpressureError.construct( + id=error_id, + createdAt=error_timestamp, + wrappedErrors=[matchers.Anything()], + errorInfo={"retryLocation": (position.x, position.y, position.z)}, + ), + state_update=update_types.StateUpdate( + liquid_operated=update_types.LiquidOperatedUpdate( + labware_id=stateupdateLabware, + well_name=stateupdateWell, + volume=None, + ) + ), + ) + else: + assert result == DefinedErrorData( + public=OverpressureError.construct( + id=error_id, + createdAt=error_timestamp, + wrappedErrors=[matchers.Anything()], + errorInfo={"retryLocation": (position.x, position.y, position.z)}, + ) + )